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.
Related
I know that there are lot of different threads about horizontal text animation/text scrolling, but unfortunately none of them give smooth scrolling with repeatable text. I have tried double/thickness animation using various WPF controls containing text. Also tried animating with visual brush which gives me by far the most elegant scrolling compared to other approaches (for e.g. playing with Canvas.Left property etc.) but that too goes blur the text, if the text length or the animation speed is too high.
I'm over to a pure DirectX C# implementation using SharpDX library. Should also mention that I'm a beginner with DirectX programming. Here is the code:
public void RunMethod()
{
// Make window active and hide mouse cursor.
window.PointerCursor = null;
window.Activate();
var str = "This is an example of a moving TextLayout object with no snapped pixel boundaries.";
// Infinite loop to prevent the application from exiting.
while (true)
{
// Dispatch all pending events in the queue.
window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessAllIfPresent);
// Quit if the users presses Escape key.
if (window.GetAsyncKeyState(VirtualKey.Escape) == CoreVirtualKeyStates.Down)
{
return;
}
// Set the Direct2D drawing target.
d2dContext.Target = d2dTarget;
// Clear the target.
d2dContext.BeginDraw();
d2dContext.Clear(Color.CornflowerBlue);
//float layoutXOffset = 0;
float layoutXOffset = layoutX;
// Create the DirectWrite factory objet.
SharpDX.DirectWrite.Factory fontFactory = new SharpDX.DirectWrite.Factory();
// Create a TextFormat object that will use the Segoe UI font with a size of 24 DIPs.
textFormat = new TextFormat(fontFactory, "Verdana", 100.0f);
textLayout2 = new TextLayout(fontFactory, str, textFormat, 2000.0f, 100.0f);
// Draw moving text without pixel snapping, thus giving a smoother movement.
// d2dContext.FillRectangle(new RectangleF(layoutXOffset, 1000, 1000, 100), backgroundBrush);
d2dContext.DrawTextLayout(new Vector2(layoutXOffset, 0), textLayout2, textBrush, DrawTextOptions.NoSnap);
d2dContext.EndDraw();
//var character = str.Substring(0, 1);
//str = str.Remove(0, 1);
//str += character;
layoutX -= 3.0f;
if (layoutX <= -1000)
{
layoutX = 0;
}
// Present the current buffer to the screen.
swapChain.Present(1, PresentFlags.None);
}
}
Basically it creates an endless loop and subtracts the horizontal offset. Here are the challenges: I need repeatable text similar to HTML marquee without any gaps, Would probably need to extend it to multiple monitors.
Please suggest.
I don't know neither how to use DirectX nor sharpdx, but if you want you can consider this solution
I had a similar problem a while ago, but with the text inside a combobox. After a bounty i got what i was looking for. I'm posting the relevant piece of code as an example, but you can check the complete answer here
Basically, whenever you have a textblock/textbox that contain a string that cannot be displayed completely, cause the length exceed the textblock/box lenght you can use this kind of approach. You can define a custom usercontrol derived from the base you need (e.g. SlidingComboBox : Combobox) and define an animation for you storyboard like the following
_animation = new DoubleAnimation()
{
From = 0,
RepeatBehavior = SlideForever ? RepeatBehavior.Forever : new RepeatBehavior(1), //repeat only if slide-forever is true
AutoReverse = SlideForever
};
In my example i wanted this behaviour to be active only when the mouse was on the combobox, so in my custom OnMouse enter i had this piece of code
if (_parent.ActualWidth < textBlock.ActualWidth)
{
_animation.Duration = TimeSpan.FromMilliseconds(((int)textBlock.Text?.Length * 100));
_animation.To = _parent.ActualWidth - textBlock.ActualWidth;
_storyBoard.Begin(textBlock);
}
Where _parent represent the container of the selected item. After a check on the text lenght vs combobox lenght i start the animation and end it at the end of the text to be displayed
Note that in the question i mentioned there are also other soltions. I'm posting the one that worked for me
I have a control in WPF (which is a custom control containing a circle).
And I need to move it every 60ms.
I have an array of "Position" (Class with 2 attributes : X and Y) and I do this to move it :
timer_tick()
{
myControl.Margin = new Thickness { Left = MyArray[i].X, Top = MyArray[i].Y};
i++;
}
with a global variable.
But can I do it in a better way ? Using something like :
public static void MoveTo(this Image target, double newX, double newY)
{
var top = Canvas.GetTop(target);
var left = Canvas.GetLeft(target);
TranslateTransform trans = new TranslateTransform();
target.RenderTransform = trans;
DoubleAnimation anim1 = new DoubleAnimation(top, newY - top, TimeSpan.FromMilliseconds(60));
DoubleAnimation anim2 = new DoubleAnimation(left, newX - left, TimeSpan.FromMilliseconds(60));
trans.BeginAnimation(TranslateTransform.XProperty,anim1);
trans.BeginAnimation(TranslateTransform.YProperty,anim2);
}
in every tick ?
Thank you
Very close. But there is no need to call this in a timer. Let the animation do the work; that is what it is for. Just set the animations to go from the origin to the destination in a timespan for the desired speed. You can always interrupt the animation at any point if you need it to end early.
Someone else already posted the best way, which is to use an animation, so here's a different way that's less "good" but probably works ok.
If you're using variables like that indexer "i" (careful about calling it global - be specific about its scope) then why not make another one?
At the class level, create a variable...
Thickness awful_marg_variable;
And in your initializer, initialize it and assign the reference to the control's Margin:
awful_marg_variable = new Thickness { Left = MyArray[0].X, Top = MyArray[0].Y};
myControl.Margin = awful_marg_variable;
And then in your timer tick procedure, you can just manipulate that variable...
awful_marg_variable.Left = MyArray[i].X;
awful_marg_variable.Top = MyArray[i].Y;
Finally, you can revel in the horrible thing you've just done, or whatever. Never do this in large quantities. Some people would say never do this ever. But sometimes you just gotta get work done and this is one more way to do it.
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
I'm pretty new to WPF (and completely new to animations), so I would imagine there's just something that I'm missing here. I'm in the process of creating a new layout Panel that lays the controls out in a particular way; the details aren't (or shouldn't be) especially important here. What I'd like to do is animate the movement of the elements that I'm managing when I call Arrange on them.
In general, what I do is keep track of the last Rect that I used to Arrange each child element. When ArrangeOverride is called...
If animation is enabled and there is a previous bounding Rect for the element, I Arrange it to the X and Y of that bounding rect with the new Width and Height, then use two DoubleAnimations (one for X and one for Y) to animate it to the X and Y of the new bounds
If animation is disabled or there is no existing bounding Rect for the element, I just Arrange it to the new bounds
When I disable animation, everything renders correctly (which is a good thing). When I enable animation, the elements render in the incorrect locations. If I resize the container, they generally animate themselves back into the correct spot.
Edit for clarification
In general, the error appears to be directly related to the coordinates of the bounds of the control. In other words, if the control is farther to the right to begin with, then the TranslateTransform appears to offset it more than a control that is not as far to the right. The same goes for up/down. Again, resizing seems to correct the problem (in fact, the controls animate themselves into the correct spot, which is frustratingly neat), but only if the entire set of controls is visible in the new size.
This is an abbreviated version of the class:
public class MyPanel : Panel
{
private Dictionary<UIElement, Rect> lastBounds = new Dictionary<UIElement, Rect>();
protected override Size MeasureOverride(Size availableSize)
{
// do all of my measurements here and delete anything from lastBounds that
// isn't on the panel any more. This works fine
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach(UIElement child in Children)
{
Rect newBounds = // get the previously calculated new bounds;
TransformGroup group= child.RenderTransform as TransformGroup;
if (group == null)
{
group = new TransformGroup();
group.Children.Add(new TranslateTransform());
child.RenderTransform = group;
}
Rect lastBounds;
if (!this.lastBounds.TryGetValue(child, out lastBounds))
lastBounds = Rect.Empty;
if (!lastBounds.IsEmpty && EnableAnimation)
{
Rect tempBounds = new Rect(lastBounds.X, lastBounds.Y, newBounds.Width, newBounds.Height);
child.Arrange(tempBounds);
int animationDuration = 300;
DoubleAnimation xAnim = new DoubleAnimation(newBounds.X - lastBounds.X, TimeSpan.FromMilliseconds(animationDuration));
DoubleAnimation yAnim = new DoubleAnimation(newBounds.Y - lastBounds.Y, TimeSpan.FromMilliseconds(animationDuration));
xAnim.AccelerationRatio = yAnim.AccelerationRatio = 0.2;
xAnim.DecelerationRatio = yAnim.DecelerationRatio = 0.7;
TranslateTransform translate = group.Children[0] as TranslateTransform;
translate.BeginAnimation(TranslateTransform.XProperty, xAnim);
translate.BeginAnimation(TranslateTransform.YProperty, yAnim);
}
else
{
child.Arrange(newBounds);
}
}
}
}
You appear to be arranging the children to their old location, then applying a render transform to get them to appear in their new location. This may be an issue because the layout engine might have a problem with you giving it a location that is now outside the parent layout container. You may have better luck always arranging to the new bounds (within the parent element), then applying the render translate animation to move it back to where it really is:
child.Arrange(newBounds);
int animationDuration = 300;
DoubleAnimation xAnim = new DoubleAnimation(lastBounds.X - newBounds.X, 0, TimeSpan.FromMilliseconds(animationDuration));
DoubleAnimation yAnim = new DoubleAnimation(lastBounds.Y - newBounds.Y, 0, TimeSpan.FromMilliseconds(animationDuration));
I've got a Canvas with a few UIElements on. After I've moved them on the canvas by animating the top and left properties, very occasionally a subsiquent call to Canvas.GetTop results in NaN.
Am I not 'closing' the animations properly?
Here's how I'm doing the move
private void InternalMove(double durationMS, FrameworkElement fElement, Point point, EventHandler callback)
{
_moveElement = fElement;
_destination = point;
Duration duration = new Duration(TimeSpan.FromMilliseconds(durationMS));
DoubleAnimation moveLeftAnimation = new DoubleAnimation(Canvas.GetLeft(fElement), point.X, duration, FillBehavior.Stop);
Storyboard.SetTargetProperty(moveLeftAnimation, new PropertyPath("(Canvas.Left)"));
DoubleAnimation moveTopAnimation = new DoubleAnimation(Canvas.GetTop(fElement), point.Y, duration, FillBehavior.Stop);
Storyboard.SetTargetProperty(moveTopAnimation, new PropertyPath("(Canvas.Top)"));
// Create a storyboard to contain the animation.
_moveStoryboard = new Storyboard();
if (callback != null) _moveStoryboard.Completed += callback;
_moveStoryboard.Completed += new EventHandler(s1_Completed);
_moveStoryboard.Children.Add(moveLeftAnimation);
_moveStoryboard.Children.Add(moveTopAnimation);
_moveStoryboard.FillBehavior = FillBehavior.Stop;
_moveStoryboard.Begin(fElement);
}
private void s1_Completed(object sender, EventArgs e)
{
if (_moveStoryboard != null)
{
_moveStoryboard.BeginAnimation(Canvas.LeftProperty, null, HandoffBehavior.Compose);
_moveStoryboard.BeginAnimation(Canvas.TopProperty, null, HandoffBehavior.Compose);
}
Canvas.SetLeft(_moveElement, _destination.X);
Canvas.SetTop(_moveElement, _destination.Y);
}
thanks
It seems than Canvas.GetTop(x) can return 'Nan' if the value is not explictly set (even tho I do explicitly set it I still sometimes get that result).
An alternative method I'm now using is
Vector offset = VisualTreeHelper.GetOffset(fElement);
which returns the position of fElement within it's container.
I've run into a similar situation (NaN), but in a different context. As I recall, it had something to do with how the element was positioned in the container.
Sorry I couldn't provide more help, but maybe this will provide some guidance.
Check back your xaml file in Properties Layout make sure value of left or top show only number but not mix (auto).
Example: Top 123(auto), this caused Canvas.GetTop will return NaN.