Storyboard animation is not in sync with the previous animation - c#

I am trying to implement MiddleClickScrolling in ScrollViewer and it works well.
The problem is when moving the pointer the Storyboard will restart to update the speed but when we move the pointer a jitter is occurring. I have attached a gif but you may not notice this jitter in this gif.
Since this a big class, I can't put all the code here. You can see my full code on GitHub (Note: Please select SmoothScroll branch if you are cloning it). An easy way to reproduce this issue is to move the pointer Up and Down for a small distance rapidly.
This is my code for storyboard animation
_verticalDoubleAnimation = new DoubleAnimation()
{
EnableDependentAnimation = true,
Duration = new TimeSpan(0, 0, 1)
};
//Different function
var offsetX = _currentPosition.X - _startPosition.X;
var offsetY = _currentPosition.Y - _startPosition.Y;
SetCursorType(offsetX, offsetY);
if (CanScrollVertical())
{
if (Math.Abs(offsetY) > _threshold)
{
RunInUIThread(() =>
{
_verticalDoubleAnimation.From = _scrollViewer.VerticalOffset;
_verticalDoubleAnimation.To = _scrollViewer.VerticalOffset + (offsetY > 0 ? _scrollViewer.ScrollableHeight : -_scrollViewer.ScrollableHeight);
if ((_scrollViewer.ScrollableHeight / (Math.Abs(offsetY) * _factor)) == double.NaN)
{
return;
}
_verticalDoubleAnimation.Duration = TimeSpan.FromSeconds(_scrollViewer.ScrollableHeight / (Math.Abs(offsetY) * _factor));
_verticalStoryboard.Begin();
});
}
else
{
RunInUIThread(() =>
{
_verticalStoryboard.Stop();
_sliderVertical.Value = _scrollViewer.VerticalOffset;
});
}
}

Instead of animating it, use a timer or a while loop to dynamically update the position using the ScrollViewer.ChangeView() method. Make sure the interval is less than 16.6ms for smooth 60fps motion. Also make sure to set the final parameter of ChangeView to false so that its built-in animation is disabled.
So in your first commit, change this:
timer = new Timer(ScrollAsync, null, 50, 50);
To this:
timer = new Timer(ScrollAsync, null, 0, 5);
The Timer does not give precision within 1ms or even 10ms to my undestanding. Overcompensating by updating it every 1-5ms should make up for that so it's probably fine and it's what I'm using in my custom scrolling control. A Stopwatch is very precise but is a massive CPU hog. Another option: It's a lot of extra work, but if you want precision timing and maximum performance with minimal battery usage, you can use Win2D's CanvasAnimatedControl which can be used to run code exactly once every 60th of a second.

Related

Can you get the final value from an animation before that animation has completed?

Say I start a four-second DoubleAnimation on the Canvas.Left property of a control that animates the value from its current value to 100 and that animation was started by calling BeginAnimation on that control in code-behind.
void Animate(Control someControlOnCanvas, int newX){
var canvasLeftAnimation = new DoubleAnimation();
canvasLeftAnimation.To = newX;
canvasLeftAnimation.FillBehavior = FillBehavior.Stop;
canvasLeftAnimation.Duration = new Duration(TimeSpan.FromSeconds(4));
canvasLeftAnimation.Completed += (s, e) => {
Canvas.SetLeft(targetButton, x);
};
someControlOnCanvas.BeginAnimation(Canvas.LeftProperty, canvasLeftAnimation);
}
Animate(somethingToMove, 100);
Say half-way through that animation--at the two-second mark--I need to cancel the current animation and start a new one with a new To position (the From position would be wherever it is right now) that's 100 past where the first animation would end (i.e. not where it currently is at 50, but where it's supposed to be, which is 100, so adding another 100 would yield a new target of 200.)
If I try getting the current Left value, it's the animated value of 50. If I try getting the base value, it's where the animation started, which is zero since it's not officially updated until the completion handler fires.
So outside of manually adding a variable to track where things should be, how can I start a new animation that says 'Go to the original end, plus 100?'
My thoughts are:
Get the existing/current animation
Get it's To value
Start a new animation with the previous animation's To value, plus 100
What I'm stuck on is getting #1.
...or am I going about this all wrong?

How to create a smooth animated text marquee?

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

Animate Layer Opacity with UIView.Animate

I have 2 CALayers, each with an image. Both have an initial Opacity of 0.
I want to animate Layer1's Opacity to 1 over 1 second, starting straight away. Then after a delay of 0.5 seconds, I want to animate Layer2's Opacity to 1 over 1 second. These 2 layers sit on top of one another.
What I am trying to achieve is having the first image fade in, then while it is fading in, fade the second image over it.
But I cannot use UIView.Animate for some reason as it does not animate at all, just sets the values straight away.
UIView.Animate(1, 0, UIViewAnimationOptions.CurveEaseIn, () =>
{
backgroundLayer.Opacity = 1;
}, () => UIView.Animate(5, () =>
{
backgroundLayer2.Opacity = 1;
}));
Here is simply tried to run the animation straight after one another and it still just sets the values right away and there is no animation.
UIView animations are intended for animating UIView properties and don't seem to play well with CALayers. Using UIImageViews instead of CALayers would solve your problem (then you should use the imageView.alpha property instead of the layer.opacity property).
However, if you insist on using CALayers you can animate them with CABasicAnimation. The code below accomplishes the animation you described (note that this is Swift code, as I don't use Xamarin).
var animation1 = CABasicAnimation(keyPath: "opacity")
// Don't go back to the initial state after the animation.
animation1.fillMode = kCAFillModeForwards
animation1.removedOnCompletion = false
// Set the initial and final states of the animation.
animation1.fromValue = 0
animation1.toValue = 1
// Duration of the animation.
animation1.duration = 1.0
var animation2 = CABasicAnimation(keyPath: "opacity")
animation2.fillMode = kCAFillModeForwards
animation2.removedOnCompletion = false
animation2.fromValue = 0
animation2.toValue = 1
animation2.duration = 1.0
// Add a 0.5 second delay.
animation2.beginTime = CACurrentMediaTime() + 0.5
// Animate!
backgroundLayer.addAnimation(animation1, forKey: "opacity")
backgroundLayer2.addAnimation(animation2, forKey: "opacity")
You can only animate following properties with UIView.Animate:
UIView.Frame
UIView.Bounds
UIView.Center
UIView.Transform
UIView.Alpha
UIView.BackgroundColor
UIView.ContentStretch
For more sophisticated animations you have to use CABasicAnimation like in #bjornorri answer.
You guys are correct in that I was trying to animate layers using the incorrect methods. What I ended up doing was replacing the layers with UIImageView and using UIView.Animate to change the Alpha properties.
Not only was this easier to code, it seems that UIImageViews are actually more performant when it comes to images.

How to animate a simple shape moving vertically?

So, I have a rectangle "rectangle1", at 160,160.
I want it to move smoothly to cordinates 160,30, with a duration of about 1 second. (time delay)
I've figured out that some basic code to move the shape is
rectangle1.Location = new Point(160,30);
However, when I tried doing a for loop with
rectangle1.Location = new Point(160, rectangle1.Location.Y - 100);
it just moved there instantly. Which I should have expected really. Same occurred with
int count = 0;
while(count != 300)
{
rectangle1.Location = new Point(160, rectangle1.Location.Y -1);
count += 2;
}
So, I assume I need some sort of clock / timer loop, that moves it by x pixels every x milliseconds. Not sure how to do this, so help would be appreciated.
Also, I'm going to be animating two other rectangles horizontally, which will then move up at the same time/speed as rectangle1. I think I'll have to "delay" rectangle1's movement until they are in position, correct?
Thanks.
PS: I've googled a fair bit, but since I'm not entirely sure what I'm looking for, it wasn't very fruitful.
If you need smooth movements, it's great to use timers, threads, backgroundworkers.
Here is what you need to do. Assuming you have the code that increment/decrement x,y points for the shape.
Steps:
set timer interval to for e.g. 100
set an integer int count=0; *
in timer_tick event do the moving work
private void timer1_Tick(object sender, EventArgs e)
// no need to use your while loop anymore :))
{
If(count< 300) //set to your own criteria
{
//e.g. myrect.location=new point(x,y);
// rectangle1.Location = new Point(160, rectangle1.Location.Y -1);
}
count += 2;
}

Best way to move an image across part of the screen?

I have a Silverlight WP7 app and an image on my page that I want to appear to slide across the screen. What is the best way of doing this? I wrote this real quick but the UI doesn't update until the entire method is done.
private void SpinImg(Image img, double left) {
for(int i = 1; i <= 10000; i++) {
img.Margin = new Thickness(left, img.Margin.Top + 1, 0, 0);
if(img.Margin.Top > 314) {
//move it to the top
img.Margin = new Thickness(left, -105, 0, 0);
}
int wait = 1000 / i;
Thread.Sleep(wait);
}
}
Use a Storyboard - this is hardware-acceleratable, and all occurs on the Render thread, so you'll see much better performance than trying to update position directly over and over.
Storyboard has the advantage of being time-based instead of frame-based, so it's easy to declare "I want the image to move from to in 0.5 seconds" and it will just happen.
Thread.Sleep will freeze ALL UI processing, use Dispatcher class.

Categories