WPFでクリック時に波紋を出すエフェクト(Ripple Effect)を実装する

Androidのマテリアルデザインに、ボタンを押すと波紋が広がったようなアニメーションをする、Ripple Effectというものがあります。

WPFでも同じようなことができないかカスタムコントロールを作成してみました。

f:id:Takachan:20171221234635p:plain

動いているところ

実際に表示を行うと以下のようになります。右クリックでボタン押すとアニメーションが開始されます。*1

youtu.be

要素の説明

今回、このアニメーションをつけるにあたりボタンを継承したRippleButtonというカスタムコントロールを作成しました。

XAML

スタイルの指定。自作のコントロールのボタンの見た目をControlTemplateで丸ごと変更しています。中身にEllipseを置いてクリックした位置で大きくなっていくようにしています。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CommonControls">
    <Style TargetType="{x:Type local:RippleButton}" BasedOn="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
        <Setter Property="OverridesDefaultStyle" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:RippleButton}">
                    <ControlTemplate.Resources>
                        <Storyboard x:Key="RippleAnimation" Storyboard.TargetName="CircleEffect">
                            <!-- Init -->
                            <DoubleAnimation Storyboard.TargetProperty="Width"
                                             To="0" Duration="0:0:0"/>
                            <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                             To=".5" Duration="0:0:0"/>
                            
                            <!-- ClickPosition Offset -->
                            <ThicknessAnimation Storyboard.TargetProperty="Margin" 
                                                Duration="0:0:0.8" FillBehavior="HoldEnd"/>
                            
                            <!-- だんだん大きくなっていくアニメーション -->
                            <DoubleAnimation Storyboard.TargetProperty="Width" 
                                             BeginTime="0:0:0" Duration="0:0:0.8" From="0" />
                            <DoubleAnimation Storyboard.TargetProperty="Opacity" 
                                             BeginTime="0:0:0.2" Duration="0:0:0.6"  From=".5" To="0" />
                        </Storyboard>
                    </ControlTemplate.Resources>
                    
                    <Grid ClipToBounds="True">

                        <Border Background="{TemplateBinding Background}">
                            <ContentPresenter />
                        </Border>
                        <!-- Ripple Effect Body -->
                        <Ellipse x:Name="CircleEffect"
                                             HorizontalAlignment="Left"
                                             VerticalAlignment="Top"
                                             Opacity="0.5"
                                             Width="0"
                                             Panel.ZIndex="0"
                                             Height="{Binding Path=Width, RelativeSource={RelativeSource Self}}"
                                             Fill="{TemplateBinding RippleColor}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
                <Setter Property="Background" Value="#CDD2D4"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

C#のコード

自作のRippleButtonクラス側です。クリック位置を取得しアニメーションに設定し開始しています。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace CustomControls
{
    /// <summary>
    /// リップル効果の付いたボタン
    /// </summary>
    public class RippleButton : Button
    {
        //
        // Props
        // - - - - - - - - - - - - - - - - - - - -

        public Brush RippleColor
        {
            get { return (Brush)GetValue(RippleColorProperty); }
            set { SetValue(RippleColorProperty, value); }
        }

        //
        // Dependency props
        // - - - - - - - - - - - - - - - - - - - -

        public static readonly DependencyProperty RippleColorProperty =
            DependencyProperty.Register("RippleColor", typeof(Brush), 
                typeof(RippleButton), new PropertyMetadata(Brushes.White));

        //
        // Public Methods
        // - - - - - - - - - - - - - - - - - - - -

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            this.AddHandler(MouseDownEvent, new RoutedEventHandler(this.OnMouseDown));
        }

        public void OnMouseDown(object sender, RoutedEventArgs e)
        {
            // クリック位置からRippleの中心を取る
            Point mousePos = (e as MouseButtonEventArgs).GetPosition(this);

            var ellipse = this.GetTemplateChild("CircleEffect") as Ellipse;

            ellipse.Margin = new Thickness(mousePos.X, mousePos.Y, 0, 0);

            // アニメーションの動作の指定
            Storyboard storyboard = (this.FindResource("RippleAnimation") as Storyboard).Clone();

            // 円の最大の大きさ -> コントロールの大きさの倍
            double effectMaxSize = Math.Max(this.ActualWidth, this.ActualHeight) * 3;

            (storyboard.Children[2] as ThicknessAnimation).From = 
                new Thickness(mousePos.X, mousePos.Y, 0, 0);
            (storyboard.Children[2] as ThicknessAnimation).To = 
                new Thickness(mousePos.X - effectMaxSize / 2, mousePos.Y - effectMaxSize / 2, 0, 0);
            (storyboard.Children[3] as DoubleAnimation).To = 
                effectMaxSize;

            ellipse.BeginStoryboard(storyboard);
        }
    }
}

使い方

GitHubに挙げているコードはApp.xmlに以下を追加してから

App.xml

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <!-- 自分で定義したスタイルの取り込み -->
            <ResourceDictionary Source="/Resource/ButtonStyle.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

メインのウインドウで以下のコードを宣言します。

<Window x:Class="CustomControls.TestEffectWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomControls"
        mc:Ignorable="d"
        Title="MainWindow"
    Height="350"
    Width="700"
    Background="#EDEDED">
    <WrapPanel Margin="20">
        <local:RippleButton Margin="10" Width="140" Height="40" 
                            HorizontalAlignment="Left" VerticalAlignment="Top"
                            Background="#6739B6" Foreground="White" RippleColor="White">
            <TextBlock Text="Hellow world!"
                       HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </local:RippleButton>
  </WrapPanel>
</Window>

広がる波紋の色をRippleColorで指定できますが、Whiteで十分きれいな効果が出ているかと思います。

コード全体

作成したコードの全体をGitHubにアップしました。

github.com

*1:ちゃんと反応するようにするためにはイベント変えないといけません。