The purpose of the code below is that a thumb follows a horizontal mouse movement. The code is called upon a mouse event, so the target value of the animation gets updated continuously.
In the code, offset is the current mouse horizontal position. The problem is, that the animation of the thumb doesn't fully animate to the specified offset, but always seems to be stopping at a value smaller or higher (depending if the mouse is dragged left or right).
The SeekAlignedToLastTick() influences the behavior of the animation, although I couldn't figure out what this function does by reading the documentation.
How can I animate the thumb, so that it follows smoothly the drag event?
private Storyboard _thumbStoryboard;
private DoubleAnimation _thumbAnimation = new DoubleAnimation();;
private CompositeTransform _thumbTransform = new CompositeTransform();
private void UpdateUserInterface(double offset)
{
var thumbItem = Thumb as FrameworkElement;
if (_thumbStoryboard == null)
{
Storyboard.SetTarget(_thumbAnimation, _thumbTransform);
_thumbStoryboard = new Storyboard();
_thumbStoryboard.Children.Add(_thumbAnimation);
thumbItem.RenderTransform = _thumbTransform;
_thumbStoryboard.Duration = new Duration(TimeSpan.FromMilliseconds(100));
_thumbAnimation.EasingFunction = new ExponentialEase();
}
double from = _thumbTransform.TranslateX;
_thumbStoryboard.Stop();
Storyboard.SetTargetProperty(_thumbAnimation, new PropertyPath("TranslateX"));
_thumbAnimation.From = from;
_thumbAnimation.To = offset;
_thumbStoryboard.Begin();
_thumbStoryboard.SeekAlignedToLastTick(TimeSpan.Zero);
}
I've tried to solve your issue, So I've created a Silverlight application and added a Border element for testing.
<Border x:Name="Thumb" VerticalAlignment="Top" HorizontalAlignment="Left" Width="50" height="25" Background="#ff0000" />
There was no need to set the "From" Property, since the DoubleAnimation object could automatically continue from the current Value to the "To" Property.
And you were setting the Duration to the Storyboard, which causes the DoubleAnimation to Cutoff its animation without reaching the "To" Value, You need to set the Duration Property to the DoubleAnimation itself instead.
Also there was no need to call _thumbStoryboard.Stop(), because it will reset the current animation to the first TranslateX Value.
Here is the updated "UpdateUserInterface" function code with comments:
private void UpdateUserInterface(double offset) {
var thumbItem = Thumb as FrameworkElement;
if ( _thumbStoryboard == null ) {
// UpdateLayout Method is update the ActualWidth Properity of the UI Elements
this.UpdateLayout();
// Applying the CompositeTransform on "thumbItem" UI Element
thumbItem.RenderTransform = _thumbTransform;
// Setting the Render Transform Origin to be the Center of X and Y
thumbItem.RenderTransformOrigin = new Point(0.5d, 0.5d);
// Setting the target of the DoubleAnimation to be the Thumb CompositeTransform
Storyboard.SetTarget(_thumbAnimation, _thumbTransform);
// Setting the Targeted Properity of the DoubleAnimation to be The "TranslateX" Properity
Storyboard.SetTargetProperty(_thumbAnimation, new PropertyPath("TranslateX"));
// Used QuinticEase instead of ExponentialEase
// and Added EaseOut to make the animation be more smoother.
_thumbAnimation.EasingFunction = new QuinticEase(){ EasingMode = EasingMode.EaseOut };
// Initializing the Storyboard
_thumbStoryboard = new Storyboard();
// Specifing the Duration of the DoubleAnimation not the StoryBoard
_thumbAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(500));
// Adding the DoubleAnimation to the Children of the Storyboard
_thumbStoryboard.Children.Add(_thumbAnimation);
}
// Calculate the New Centered Position
double newPos = offset - (thumbItem.ActualWidth / 2);
// Set the New DoubleAnimation "To" Value,
// There is no need to set the "From" Value since it'll automatically continue from the current TranslateX Value
_thumbAnimation.To = newPos;
// Begin the animation.
_thumbStoryboard.Begin();
}
Hope that helps you :)
Regards,
Monir Abu Hilal
Related
I have a very simple project where I need to animate the RenderTransform of an element, and then further manipulate such transform.
Please find as a reference an MVCE here: https://github.com/cghersi/UWPExamples/tree/master/RenderTransformAnimation.
The scenario is the following: there is a ScrollViewer m_scrollView, with a Canvas content m_zoomView.
For the sake of the example we also have a CompositeTransform m_zoomViewTransform = m_zoomView.RenderTransform.
I use the following method to manipulate the RenderTransform, either with or without an animation:
private void SetEffectiveOffsetOfScrollView(Point newOffset, bool isAnimated)
{
if (isAnimated)
{
TimeSpan dur = TimeSpan.FromSeconds(0.2);
Storyboard sb = new Storyboard { Duration = dur };
DoubleAnimation animationX = new DoubleAnimation
{
To = newOffset.X,
Duration = dur,
AutoReverse = false
};
DoubleAnimation animationY = new DoubleAnimation
{
To = newOffset.Y,
Duration = dur,
AutoReverse = false
};
sb.Children.Add(animationX);
sb.Children.Add(animationY);
Storyboard.SetTarget(animationX, m_zoomViewTransform);
Storyboard.SetTarget(animationY, m_zoomViewTransform);
Storyboard.SetTargetProperty(animationX, "CompositeTransform.TranslateX");
Storyboard.SetTargetProperty(animationY, "CompositeTransform.TranslateY");
sb.Begin();
sb.Completed += (sender, o) =>
{
m_zoomViewTransform.TranslateX = newOffset.X;
m_zoomViewTransform.TranslateY = newOffset.Y;
};
}
else
{
m_zoomViewTransform.TranslateX = newOffset.X;
m_zoomViewTransform.TranslateY = newOffset.Y;
}
}
Now, if I use SetEffectiveOffsetOfScrollView() with isAnimated = true, I am not able to change the RenderTransform anymore, or at least I don't see any update to the UI anymore.
In the MVCE I added a button that invokes the SetEffectiveOffsetOfScrollView() method with isAnimated = true, and I added a Manipulation event to pan the m_zoomView Canvas: as soon as I click on the button, I am no more able to pan the Canvas.
How can I animate the transformation, still being able to see the updates to the UI after this action, using SetEffectiveOffsetOfScrollView() with animate=false?
This is due to dependency property value precedence, as described here:
https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/dependency-properties-overview#dependency-property-value-precedence
In this repro, the Storyboard is still active, due to the default FillBehavior=HoldEnd on the DoubleAnimations. Since those animations are still alive, the animated value gets used, even as new local values get set on the isAnimated=false case.
The easy fix is to call sb.Stop() in the Storyboard's Completed handler, after you've set the new local values to hold. This will stop the animations, removing the Animated values they are holding, and allow the Local values to be used.
I have a question about animations in UWP. I want to have a menu on the bottom of my app that, when tapped on the top, slides up (shows) or down (hides almost entirely). I was learning WPF before and there I know I can use ThicknessAnimation to move the margin of my control and have it slide. Unfortunately, in UWP I can't use ThicknessAnimations, so I tried to find another way. I want this to work for an arbitrary FrameworkElement (so as to be able to reuse it). Eventually, I came up with this solution:
/// <summary>
/// Adds a vertical slide animation
/// </summary>
/// <param name="storyboard">The storyboard to add the animation to</param>
/// <param name="seconds">The time the animation will take</param>
/// <param name="offset">The distance the element will cover (nagative is up, positive is down)</param>
public static void AddVerticalSlide(this Storyboard storyboard, FrameworkElement element, float seconds, double offset)
{
var slideAnimation = new ObjectAnimationUsingKeyFrames();
for (int i = 0; i <= 100; ++i)
{
double scalar = (double)i / 100;
slideAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame
{
Value = new Thickness(0, scalar*offset, 0, -scalar*offset),
KeyTime = TimeSpan.FromSeconds(scalar*seconds),
});
}
//slideAnimation.Duration = TimeSpan.FromSeconds(seconds);
// Set the target and target property
Storyboard.SetTarget(slideAnimation, element);
Storyboard.SetTargetProperty(slideAnimation, "(FrameworkElement.Margin)");
// Add the animation to the storyboard
storyboard.Children.Add(slideAnimation);
}
It works, looks nice, but here's the reason I'm asking this question: I don't know if it's proper. My guees is that there's a better way to slide objects than manually define 100 points on the way and move the object to each point using this animation.
This works but is just not the ideal way to offset an element. Why? Because you are creating two many DiscreteObjectKeyFrames when you really just need one DoubleAnimation.
You should almost never animate the Margin of an element. To change its position, a better approach is to animate the translate values (i.e. TranslateX/TranslateY) of its transform (RenderTransform) instead.
Animating anything in transform is efficient. They are off the UI thread. Traditionally, they were running in a special thread called Compositor thread (I think), but ever since the Creators Update, they have become even more performant according to the Windows UI team -
When you use Storyboard and Transition animations in XAML, you’re
using Composition under the hood. Animations run at 60 frames per
second!
The following is an example of using such technique
public static void Slide(this UIElement target, Orientation orientation, double? from, double to, int duration = 400, int startTime = 0, EasingFunctionBase easing = null)
{
if (easing == null)
{
easing = new ExponentialEase();
}
var transform = target.RenderTransform as CompositeTransform;
if (transform == null)
{
transform = new CompositeTransform();
target.RenderTransform = transform;
}
target.RenderTransformOrigin = new Point(0.5, 0.5);
var db = new DoubleAnimation
{
To = to,
From = from,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(duration)
};
Storyboard.SetTarget(db, target);
var axis = orientation == Orientation.Horizontal ? "X" : "Y";
Storyboard.SetTargetProperty(db, $"(UIElement.RenderTransform).(CompositeTransform.Translate{axis})");
var sb = new Storyboard
{
BeginTime = TimeSpan.FromMilliseconds(startTime)
};
sb.Children.Add(db);
sb.Begin();
}
Note as performant as this approach gets, there is even more powerful animation support in UWP, thanks to the new Composition API. But offset animation in Composition can be a bit tricky.
However, the UWP Community Tookit has done a great job wrapping some useful animations like Blur, Offset, etc. Feel free to check them out.
I am trying to implement a translation animation on a Grid in Windows Phone 8.
The behavior that I want to implement is that when the user drags from left to right a new panel comes from the left(in animation) and reverse happens when the user drags from right to left.
For this I have implemented the following code which is called on Manipulation_Completed :
public Storyboard AnimateContent(int direction)
{
DoubleAnimation animation = new DoubleAnimation();
if (direction == 1)
{ //content_trans is the object of composite transform of grid
animation.From =content_trans.TranslateX;
animation.To = 370;
}
else if(direction==0)
{
animation.From = content_trans.TranslateX;
animation.To = 0;
}
animation.Duration = new TimeSpan(0,0,0,0,500);
Storyboard.SetTarget(animation, Content);
Storyboard.SetTargetProperty(animation, new PropertyPath("(UIElement.RenderTransform).(CompositeTransform.TranslateX)"));
Storyboard sb = new Storyboard();
sb.Children.Add(animation);
return sb;
}
Now problem I am facing is that after calling this function(and calling begin() on storyboard object), when I am writing
content_trans.TranslateX=250;//or any other value of my choice
it is not being reflected on the screen.
I want to change these values because I writing this line of code in Manipulation_Delta , so that the user can have a feeling of dragging something, but it is not being reflected after animation.
This is because animated values take precedence over local values. See Dependency Property Value Precedence
If you want to set the value yourself, you will need to stop the animation first.
EDIT: If you want to be able to drag after the animation, then set the END location manually just before animating, and tell the animation to stop applying itself when it finishes with:
animation.FillBehaviour = FillBehaviour.Stop;
This means that when the animation finishes, your local values will apply.
I have a mini-game as part of a larger game I'm writing for Windows phone which uses a storyboard animation to move an arrow back and forth along a bar, which the user trying to stop it in the center.
The issue I'm having is when I stop the animation, you can visibily see the arrow being animated move back a frame.
I've tried many things:
animating with keyframes and without
animating by canvas.left or translate.X
after pausing/stopping the animation, manually setting the canvas.left to the getcanvas of the arrow
With the final option not working, I wonder if the animation is actually drawing a frame ahead before it actually sets the X/Canvas.Left, and the bounce back position is the true one
Here is my animation code:
double speed = .75;
miniGameStoryboard.Stop();
miniGameStoryboard.Children.Clear();
// setup
var _Translate = new TranslateTransform();
this.MiniGame1Arrow.RenderTransform = _Translate;
// translate (location X)
DoubleAnimationUsingKeyFrames _TranslateAnimateX = new DoubleAnimationUsingKeyFrames();
System.Windows.Media.Animation.Storyboard.SetTarget(_TranslateAnimateX, _Translate);
System.Windows.Media.Animation.Storyboard.SetTargetProperty(_TranslateAnimateX, new PropertyPath(TranslateTransform.XProperty));
_TranslateAnimateX.KeyFrames.Add(new LinearDoubleKeyFrame
{
KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)),
Value = 0
});
_TranslateAnimateX.KeyFrames.Add(new LinearDoubleKeyFrame
{
KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(speed)),
Value = 660
});
_TranslateAnimateX.AutoReverse = true;
_TranslateAnimateX.RepeatBehavior = RepeatBehavior.Forever;
_TranslateAnimateX.FillBehavior = FillBehavior.HoldEnd;
miniGameStoryboard.Children.Add(_TranslateAnimateX);
miniGameStoryboard.Begin();
It seems like it should be so simple. I've read dozens of links and I can't get anything to animate the position. I believe the closest code I can write so far is this:
Storyboard storyboard = new Storyboard();
TranslateTransform trans = new TranslateTransform() { X = 1.0, Y = 1.0 };
myCheckbox.RenderTransformOrigin = new Point(0.5, 0.5);
myCheckbox.RenderTransform = trans;
DoubleAnimation moveAnim = new DoubleAnimation();
moveAnim.Duration = TimeSpan.FromMilliseconds(1200);
moveAnim.From = -1;
moveAnim.To = 1;
Storyboard.SetTarget(moveAnim, myCheckbox);
Storyboard.SetTargetProperty(moveAnim, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
storyboard.Completed += new System.EventHandler(storyboard_Completed);
storyboard.Children.Add(moveAnim);
storyboard.Begin();
No errors are thrown.
The completion callback does get called.
If I animate opacity in a similar fashion it works fine.
How can I simply animate a UIElement's position with code??
The comment from xyzzer was correct. The cause of the confusion was because the coordinates for RenderTransformOrigin use (0,1) relative to the element. The actual transforms (e.g. TranslateTransform) use pixels as units.