Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android's Ripple Effect in WPF

Tags:

c#

wpf

xaml

I love Androids new animation where you touch a control (listviewitem, button etc etc) and it does a neat animation like this:

enter image description here

I'm wondering how this can be implemented in a nice way globally for all the 'clickable' controls in WPF.


What I specifically need help with is how the circles should be created on the control. The only thing I've thought of was to create own user-controls for each other control (buttons, radiobuttons, etc) where I have a parent for the ellipse as well as the original control itself.

<UserControl>
   <Grid MouseLeftButtonDown="handler">
      <Button/> <--- this would be the button which you normally would place
   </Grid >
</UserControl>

And in the handler-method then create an ellipse on the point e.GetPosition(handler) using the margin-properties and later animate it. This solution would work. But it would be a hassle to do this for every control I would want the ripple effect on. Basically something like this:

void handler(object sender, MouseButtonEventArgs e)
{
   Grid parent = (Grid)sender;
   Ellipse ellipse = new Ellipse();
   ellipse.Height = 10; // would be animated
   ellipse.Width = 10; // would be animated

   Point p = e.GetPosition(parent);

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

   parent.Children.Add(ellipse);

   // do the animation parts to later remove the ellipse
}

Is there a cleaner, more expandable way to place ellipses on my controls other than the way I earlier demonstrated since not all controls support having children?

like image 933
Tokfrans Avatar asked Apr 24 '15 21:04

Tokfrans


2 Answers

UPDATE: This problem was so interesting to me that I implemented it. You can find it on my Github page: https://github.com/Domysee/WpfCustomControls. There are multiple custom controls, the one you are looking for is RippleEffectDecorator.


Now I explain what I did:

I created a custom control that inherits from ContentControl, RippleEffectDecorator. It defines an additional dependency property HighlightBackground, which is used for the background after you clicked the element.

The ControlTemplate of RippleEffectDecorator consists of a Grid, an Ellipse and a ContentPresenter.

<ControlTemplate TargetType="{x:Type l:RippleEffectDecorator}">
    <Grid x:Name="PART_grid" ClipToBounds="True" Background="{TemplateBinding Background}"
            Width="{Binding ElementName=PART_contentpresenter, Path=ActualWidth}"
            Height="{Binding ElementName=PART_contentpresenter, Path=ActualHeight}">
        <Ellipse x:Name="PART_ellipse"
                        Fill="{Binding Path=HighlightBackground, RelativeSource={RelativeSource TemplatedParent}}" 
                        Width="0" Height="{Binding Path=Width, RelativeSource={RelativeSource Self}}" 
                        HorizontalAlignment="Left" VerticalAlignment="Top"/>

        <ContentPresenter x:Name="PART_contentpresenter" />
    </Grid>
</ControlTemplate>

I used a Grid instead of a Border so that I can add multiple child elements (necessary that Ellipse and ContentPresenter can overlap). The ellipse binds its Height property to its own width, so that it is always a circle.

Now to the important part: the animation.

The Grid defines in its resources a Storyboard, which is played on every MouseDown event.

<Storyboard x:Key="PART_animation" Storyboard.TargetName="PART_ellipse">
    <DoubleAnimation Storyboard.TargetProperty="Width" From="0" />
    <ThicknessAnimation Storyboard.TargetProperty="Margin" />
    <DoubleAnimation BeginTime="0:0:1" Duration="0:0:0.25" Storyboard.TargetProperty="Opacity"
                From="1" To="0" />
    <DoubleAnimation Storyboard.TargetProperty="Width" To="0" BeginTime="0:0:1.25" Duration="0:0:0" />
    <DoubleAnimation BeginTime="0:0:1.25" Duration="0:0:0" Storyboard.TargetProperty="Opacity" To="1" />
</Storyboard>

The storyboard animates the width property of the ellipse so that it fills the area completely. It also has to animate the Margin, because the ellipse positions itself relative to the upper left point (not around its center).

The start position of the ellipse, its target width and its position in the container throughout the effect has to be set programmatically. I overwrite the OnApplyTemplate() method to add an event handler to the mouse down event, which starts the storyboard and sets all necessary values.

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

    ellipse = GetTemplateChild("PART_ellipse") as Ellipse;
    grid = GetTemplateChild("PART_grid") as Grid;
    animation = grid.FindResource("PART_animation") as Storyboard;

    this.AddHandler(MouseDownEvent, new RoutedEventHandler((sender, e) =>
    {
        var targetWidth = Math.Max(ActualWidth, ActualHeight) * 2;
        var mousePosition = (e as MouseButtonEventArgs).GetPosition(this);
        var startMargin = new Thickness(mousePosition.X, mousePosition.Y, 0, 0);
        //set initial margin to mouse position
        ellipse.Margin = startMargin;
        //set the to value of the animation that animates the width to the target width
        (animation.Children[0] as DoubleAnimation).To = targetWidth;
        //set the to and from values of the animation that animates the distance relative to the container (grid)
        (animation.Children[1] as ThicknessAnimation).From = startMargin;
        (animation.Children[1] as ThicknessAnimation).To = new Thickness(mousePosition.X - targetWidth / 2, mousePosition.Y - targetWidth / 2, 0, 0);
        ellipse.BeginStoryboard(animation);
    }), true);
}

Note: the last parameter of AddHandler() determines whether or not you want to receive handled events. It is important to set this to true, because some UiElements handle mouse events (e.g. Button). Otherwise the MouseDownEvent would not fire and therefore the animation not executed.

To use it simply add the element on which you want to have this effect as child of RippleEffectDecorator, and the Background to Transparent:

<cc:RippleEffectDecorator Background="Green" HighlightBackground="LightGreen">
    <Button FontSize="60" Background="Transparent">stuff</Button>
</cc:RippleEffectDecorator>

Note2: some elements include triggers which set the template on MouseOver (e.g. Button) and therefore hide the effect. If you dont want that you have to set the template of the button and remove these triggers. The easiest way is to use Blend, get the template of the button from it, remove all triggers and add it as template of your button.

like image 115
Domysee Avatar answered Nov 11 '22 02:11

Domysee


there also a very cool WPF material design library http://materialdesigninxaml.net/

like image 24
user2475096 Avatar answered Nov 11 '22 03:11

user2475096