Android's Ripple Effect in WPF - c#

I love Androids new animation where you touch a control (listviewitem, button etc etc) and it does a neat animation like this:
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?

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.

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

I suggest using Custom Controls over UserControls for this. Almost everything could be handled in xaml that way. Once you have your control styled then all you have to do is add an Ellipse and set a MouseDown or Button.Pressed trigger to your ControlTemplate.Triggers. Then your animation would only need to increase the height and width of the Ellipse until the ripple effect was completed then fade the Opacity to 0.
For the Ellipse make sure you have the position fixed and for the same color scheme try a white fill and an opacity of 0.3-5.
For the ring effect on the button in the top corner of your add a 2nd Ellipse set the Fill to Transparent and the Stroke to White.

Related

How can I get the UI to update when animating a color change

I am writing a UWP WinRT app in C#. I found the very useful BitmapIcon class and use it for a grid of icons on the main page of my app. The BitmapIcon class has a Foreground brush that can be used to override the color of the original bitmap which is handy for when something controls the icon colors separate from the picture itself (like a server dictating that an icon be red to show that it's important).
I am using a storyboard animation to change the icon colors (for a flashing effect that I know is not liked these days but I am forced to do it). Here's the code:
ColorAnimationUsingKeyFrames colorAnimation = new ColorAnimationUsingKeyFrames();
colorAnimation.Duration = TimeSpan.FromSeconds( 3 );
colorAnimation.EnableDependentAnimation = true;
IconImage.Foreground = new SolidColorBrush( iconColor ?? AppSettings.appColor );
DiscreteColorKeyFrame key1 = new DiscreteColorKeyFrame();
key1.Value = finishColor;
key1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds( 0 ) );
colorAnimation.KeyFrames.Add( key1 );
LinearColorKeyFrame key2 = new LinearColorKeyFrame();
key2.Value = startColor;
key2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds( 3.0 ) );
colorAnimation.KeyFrames.Add( key2 );
colorAnimation.RepeatBehavior = RepeatBehavior.Forever;
Storyboard.SetTargetProperty( colorAnimation, "(BitmapIcon.Foreground).Color" );
Storyboard.SetTarget( colorAnimation, IconImage );
// Create a storyboard to apply the animation.
//Storyboard myStoryboard = new Storyboard();
animationStoryboard.Children.Add( colorAnimation );
animationStoryboard.Duration = colorAnimation.Duration;
animationStoryboard.RepeatBehavior = colorAnimation.RepeatBehavior;
animationStoryboard.Begin();
This seems to somewhat work. I get no exceptions and the colors do change. The problem is that the changes do not show up in the window. If I resize the app window, the colors can be seen to change as I drag the edge of the window. When I stop, they stop showing a change but the change appears to stil lbe happening in the background invisibly. I can tell that the numbers are changing in the background because of the jump in colors that happen when I stop changing the size of the window for a moment.
It's been a while since I worked on C# UWP WinRT code and I think I'm missing some sort of attribute or property of the Imageicon object, it's Foreground SolidColorBrush, or the Color associated with the SolidColorBrush.
Elsewhere in my code, I use similar methods to animate an opening and closing sidebar on the window. That works and the animation is smooth, as expected.
I need to figure out why the color change seems to happen but without the UI getting updated constantly.
How can I get the UI to update when animating a color change
I could reproduce this issue, and I have discussed with team, it is a bug, I reported it to related team just now. And you could also post this with Windows feed back hub app.
If I receive any info about this, I will update below. thx!
Update
This isn’t ideal, as it involves 2 animations and 2 separate BitmapIcons, we may be able to tweak it to our liking by only using 1 Animation and having a static image behind the BitmapIcon. Please check the following code.
<Grid>
<BitmapIcon
x:Name="IconImageOne"
Foreground="Red"
UriSource="ms-appx:///Assets/StoreLogo.png">
<BitmapIcon.Resources>
<Storyboard x:Name="animationStoryboard">
<DoubleAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetName="IconImageOne"
Storyboard.TargetProperty="Opacity"
From="1.0"
To="0.0"
Duration="0:0:3" />
<DoubleAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetName="IconImageTwo"
Storyboard.TargetProperty="Opacity"
From="0.0"
To="1.0"
Duration="0:0:3" />
</Storyboard>
</BitmapIcon.Resources>
</BitmapIcon>
<BitmapIcon
x:Name="IconImageTwo"
Foreground="Green"
Tapped="IconImage_Tapped"
UriSource="ms-appx:///Assets/StoreLogo.png" />
</Grid>
Code behind
private void StatAnimation()
{
animationStoryboard.Begin();
}
private void IconImage_Tapped(object sender, TappedRoutedEventArgs e)
{
StatAnimation();
}
I'm posting this untested answer because I think it is a viable option and might work as well as the workaround that I used (and mentioned as a comment on my original post)...
Set up the code to animate the color in case that ever works in the future. Then also add an animation to alter the opacity of the BitmapIcon between 1.0 and 0.99999, or some other similar values. The Opacity change should cause a refresh of the view/screen while not actually changing the opacity enough for the user to see it. The color change will then be visible because of the opacity update.
I have not tested this since I had to move on after using the two-Bitmapicon workaround.

How to programically change ScaleY RenderTransform of a control in a XAML C# Windows App?

I have several instances of the following xaml code. Essentially it's a bunch of grids which show up in a stackpanel in a small (24 pixel tall) state, showing one line of the textblock. When you click on the arrow image (or rather the border around the image as there's transparency in the image) then the grid expands to show all the details within it. I have 15 of these in total:
<Grid x:Name="borLecSec1" Style="{StaticResource SearchedSectionGrid}">
<TextBlock x:Name="txtLecSec1" Style="{StaticResource SearchedSectionText}"
PointerEntered="SearchSectionEntered" PointerExited="SearchSectionExited"
Tapped="SearchSectionTapped"/>
<Border x:Name="backArrowSection_Lec_1" Style="{StaticResource ExpandSectionButton}"
PointerEntered="backArrowSectionEnter" PointerExited="backArrowSectionExit"
Tapped="backArrowSectionTapped">
<Image x:Name="arrowSection_Lec_1" Style="{StaticResource ExpandSectionImage}">
<Image.RenderTransform>
<CompositeTransform ScaleY="1"/>
</Image.RenderTransform>
</Image>
</Border>
</Grid>
I haven't figured out how to animate the grids themselves expanding yet as they would have to go from 24 pixels to Auto which I haven't gotten to work. What I have gotten to work is that when clicked, this arrow flips vertically so that it now signifies that another click will cause the grid to collapse down. The storyboard for this animation is this:
<Storyboard x:Name="SearchSectionArrowExpand">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)"
From="1" To="-1" Duration="00:00:0.15"/>
</Storyboard>
<Storyboard x:Name="SearchSectionArrowCollapse">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)"
From="-1" To="1" Duration="00:00:0.15"/>
</Storyboard>
As there are many instances of this arrow image control, the target property of the storyboard is changed before every Begin() statement in the C# code behind. The code I have follows. In it, the working_grid and working_image objects correspond to borLecSec1 and arrowSection_Lec_1 respectively in the xaml above.
if (working_grid.Height == 24)
{
System.Diagnostics.Debug.WriteLine("expand");
working_grid.Height = double.NaN;
SearchSectionArrowCollapse.Stop();
SearchSectionArrowExpand.Stop();
Storyboard.SetTargetName(SearchSectionArrowExpand, working_image_name);
SearchSectionArrowExpand.Begin();
}
The collapse part of the code is very similar in the subsequent else. The Stop() commands are necessary as I get an error if they're not there saying the root storyboard must be stopped before re-targeting. So everything I've said works fine. What doesn't work is that if I expand the first grid, so arrowSection_Lec_1 has a ScaleY of -1, if I then expand the second grid giving arrowSection_Lec_2 a ScaleY of -1 as well, the first image reverts back to having a ScaleY of 1 even though its corresponding grid is still expanded.
The solution I thought up is to have the storyboard Completed event set the ScaleY of the appropriate arrow explicitly so that it would maintain this position even if the storyboard is run again for a different arrow. I can't figure out how to reference this property in C#.
So for clarity, my question is how would I set the ScaleY transform of arrowSection_Lec_1 to -1 from the code behind?
Here is how to access the Scale from the codebehind:
var transform = (CompositeTransform)arrowSection_Lec_1.RenderTransform;
transform.ScaleY = -1;

Flash animation for my textblock

I have set up a textblock to set the string "ALARM" added to it when the alrm clock goes off. The code is working fine for this process. I am trying to make this string "ALARM" (or the textblock itself) flash when the alarm goes off.
I am able to work out the codes to make the string "ALARM" fade in fade out using mouse events but cant figure out how to make it happen w/o the need for mouse events. I tried textBlock_Loaded event and that doesn't work. I want the fade in fade out to be ongoing forever in a loop to create a flashing effect.
Please advice if there is an event that would fit my need. Been trying one by one down the list of available events with no success. My codes for the mouse events is below. Appreciate any advice. Thanks.
private void textBlock3_MouseLeave(object sender, MouseEventArgs e)
{
TextBlock textblk = (TextBlock)sender;
DoubleAnimation animation = new DoubleAnimation(0, TimeSpan.FromSeconds(2));
textblk.BeginAnimation(TextBlock.OpacityProperty, animation);
}
private void textBlock3_MouseEnter(object sender, MouseEventArgs e)
{
TextBlock textblk = (TextBlock)sender;
DoubleAnimation animation = new DoubleAnimation(1, TimeSpan.FromSeconds(2));
textblk.BeginAnimation(TextBlock.OpacityProperty, animation);
}
All you need is an EventTrigger with the "Loaded" event. Set the RepeatBehavior to "Forever" so that the Storyboard keeps repeating, and AutoReverse to "True":
<TextBlock x:Name="textBlock3" Text="hello world">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever" AutoReverse="True">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Duration="0:0:1"
To="0"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>

Dynamically changing properties of a Storyboard (created in XAML) in the C# code-behind

I admit I'm new to Silverlight, but you have to start somewhere.
Here's my problem: I have XAML code which creates a Canvas to be used on my web page. I dynamiclly (in the code behind) create 24 smaller canvas objects (called tiles) and can correctly position and move these tiles inside the larger canvas. I want to now animate the movement of the tiles rather than have them just "jump" from one location to the next. Inside the XAML I created a Storyboard and a DoubleAnimation for one of the tiles. Clicking on the specific tile named in the DoubleAnimation/Storyboard produces the correct animation. Now I want to be able to change the properties of the animation in the XAML dynamically by the code in the code-behind. Specifically, I want to change the "From", "To", "Storyboard.TargetName", and "Storyboard.TargetProperty" values. This would allow me to use the single animation to control the movement of all 24 tiles (one at a time). Below is the XAML and below that is the code I've been attempting to get to work correctly.
XAML
<Canvas x:Name="LayoutRoot" Background="BlanchedAlmond" Height="700" Width="1405">
<Image Source="bkcolor.png" Canvas.Left="600" Height="500" Width="500" Stretch="UniformToFill" Canvas.Top="100"></Image>
<Canvas x:Name="myContainer" Canvas.Left = "50" Canvas.Top="100">
<!---->
<Canvas.Resources>
<Storyboard x:Name="MoveTileAnimation">
<DoubleAnimation x:Name="myDoubleAnimation"
From="400" To="300"
Duration="00:00:1"
Storyboard.TargetName="Tile23"
Storyboard.TargetProperty="(Canvas.Top)">
<DoubleAnimation.EasingFunction>
<PowerEase Power="3" EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Canvas.Resources>
<!---->
</Canvas>
</Canvas>
C# Code in Code Behind
private void MainPage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Canvas c = sender as Canvas;
// New location is specified by nx, ny
int nx = 200;
int ny = 300;
// Old Location
int ox = 200;
int oy = 200;
// "Tile Moves Up" -- Other case removed for clarity
//----------------------------------------------
// Code below is known to work correctly
//----------------------------------------------
// Set the "To" and "From" properties
myDoubleAnimation.From = Convert.ToDouble(oy);
myDoubleAnimation.To = Convert.ToDouble(ny);
//----------------------------------------------
// Code below does not function correctly
//----------------------------------------------
MoveTileAnimation.SetValue(Storyboard.TargetNameProperty, c.Name); // c.Name is the
// name of the tile
// that was clicked
MoveTileAnimation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath(Canvas.TopProperty)); // Need to switch between Top and Left
//----------------------------------------------
MoveTileAnimation.Begin(); // This works if the TargetNameProperty in
// the XAML matches the Tile Name
//----------------------------------------------
// Code below is known to work correctly
//----------------------------------------------
// Move the Tile to its new position
tileCanvas[nCanvasID].SetValue(Canvas.TopProperty, Convert.ToDouble(ny));
tileCanvas[nCanvasID].SetValue(Canvas.LeftProperty, Convert.ToDouble(nx));
}
I don't want to have to create 96 storyboards to be able to move 24 tiles in each of 4 directions. If I can get changing the "TargetNameProperty" working, that reduces to 4 storyboards. If I can also get changing the "TargetPropertyProperty" working, I'm done to a single storyboard.
Thanks in advance,
John
This link points to an article with code for reusable storyboards. It's an old article, but still applies today - maybe these classes will help you. http://www.codeproject.com/Articles/24543/Creating-and-Reusing-Dynamic-Animations-in-Silverl

MouseEnter event is not always triggered

In my WPF project I have a canvas on which I draw an ellipse in the XAML and add a MouseEnter event to it:
<Canvas Width="600" Height="480" Name="canvas1" HorizontalAlignment="Left">
<Ellipse Height="20" Width="20" Canvas.Left="50" Canvas.Top="50" Fill="blue" Name="ellipse1" Mouse.MouseEnter="ellipse1_MouseEnter" MouseLeave ="ellipse1_MouseLeave"/>
</Canvas>
In the codebehind I've got this code:
private void ellipse1_MouseEnter(object sender, MouseEventArgs e)
{
ellipse1.Fill = Brushes.Red;
}
When I enter the ellipse with my mouse, it turns red as expected.
I also have code to draw an ellipse on the canvas where I click my mouse. I have a class called Vertex in which I create a ellipse, which has a reference to the canvas.
When I instantiate a new Vertex (and so an ellipse), I add the ellipse to the canvas' children. Before adding it to the canvas, I add a handler to the MouseEnter event:
MyEllipse.MouseEnter += new System.Windows.Input.MouseEventHandler(MyEllipse_MouseEnter);
The "MyEllipse_MouseEnter" handler looks like this:
private void MyEllipse_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
this.MyEllipse.Fill = Brushes.Red;
}
I expected this to work as it is the same as the first example which works.
However, when I enter the drawn ellipse with the mouse, my handler is not caleld. BUT, when go back and forth over the ellipse many times, it will eventually trigger and color the ellipse red. But this only happens on one of the many ellipses I draw, which also seems very strange.
What could be causing this strange behaviour?
Solved the problem!
When drawing the ellipse as a part of a Vertex, I add a label to the ellipse.
Better said, I put a label on top of the ellipse:
Canvas.SetZIndex(myEllipse, 10);
Canvas.SetLeft(myEllipse, coordinates.X);
Canvas.SetTop(myEllipse, coordinates.Y);
Canvas.SetZIndex(myLabel, 10);
Canvas.SetLeft(myLabel, coordinates.X - 1);
Canvas.SetTop(myLabel, coordinates.Y - 5);
canvas.Children.Add(myEllipse);
canvas.Children.Add(myLabel);
So when I clicked an ellipse on the canvas I actually clicked the label rather then the ellipse. The solution for his was simple:
myLabel.IsHitTestVisible = false;
Now the label cannot be hitted :D
Thx everbody!
Can you share the code you are using to programatically draw the ellipse. I suspect that your programatic ellipse is not filled initially making it transparent, the transparent region is not treated as part of the ellipse and the messages are therefore not raised to the ellipse.
If my abovew assumption is correct, the occasions when you do get the messages, it is when the mouse pointer hits the outline of the ellipse.

Categories

Resources