开发者

How to achieve smooth animated effect when dragging an object in WPF

I am trying to learn some WPF and I was hoping to be able to implement a simple game. In this game, there are a few items on a Canvas. For the purpose of this question, let’s say there’s just one, and it’s an Ellipse:

<Canvas Name="canvas">
    <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"/>
</Canvas>

The user needs to be able to drag these items around arbitrarily.

So I implemented the following code and it seems to work:

public MainWindow()
{
    InitializeComponent();

    Canvas.SetLeft(ellipse, 0);
    Canvas.SetTop(ellipse, 0);

    ellipse.MouseDown += new MouseButtonEventHandler(ellipse_MouseDown);
    ellipse.MouseMove += new MouseEventHandler(ellipse_MouseMove);
    ellipse.MouseUp += new MouseButtonEventHandler(ellipse_MouseUp);
}

void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed)
        return;
    ellipse.CaptureMouse();
    ellipse.RenderTransform = new ScaleTransform(1.25, 1.25, ellipse.Width / 2, ellipse.Height / 2);
    ellipse.Opacity = 0.75;
}

void ellipse_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
        return;
    var pos = e.GetPosition(canvas);
    Canvas.SetLeft(ellipse, pos.X - ellipse.Width * 0.5);
    Canvas.SetTop(ellipse, pos.Y - ellipse.Height * 0.5);
}

void ellipse_MouseUp(object sender, MouseButtonEventArgs e)
{
    if (!ellipse.IsMouseCaptured)
        return;
    ellipse.ReleaseMouseCapture();
    ellipse.RenderTransform = null;
    ellipse.Opacity = 1;
}

Now, if you try this, you’ll see that the movements are very jagged. When you mouse down, the ellipse grows instantly and changes i开发者_Go百科ts transparency instantly. I want to smoothen this out so that there are no sudden jumps.

I’ve tried the obvious things using a DoubleAnimation on Ellipse.OpacityProperty, ScaleTransform.ScaleXProperty and (notably) Canvas.LeftProperty/TopProperty. However, I run into the following problems:

  • As soon as I begin an animation on Canvas.LeftProperty/TopProperty, I can never use Canvas.SetLeft/Top again, so the ellipse doesn’t move when it is dragged. I couldn’t find a way to remove the animation from the object.

  • If the user releases the mouse while the animation is still happening, the “shrinking” animation on the ScaleTransform starts from the full size before the “growing” animation has reached it, which causes a sudden jump. If you click the mouse frantically, the object’s size jumps frantically, which it shouldn’t.

If you need to, you can look at my failed code, which doesn’t work.

How do you implement these smooth motions properly in WPF?

Please do not post an answer without trying it out first. If there are any sudden jumps, the result is unsatisfactory. Thanks!


Instead of creating a new ScaleTransform for each change, use the same one and keep applying new animations. If you don't specify a From property for the animation, it will start with the current value and do a smooth animation.

To avoid the location skip, remember the position of the mouse within the ellipse instead of always centering it. That way you won't need to worry about recentering it. (You can call BeginAnimation with a null timeline to stop the current animation, but then you'll just get a jump on the first MouseMove.)

In XAML:

<Ellipse Name="ellipse" Width="100" Height="100"
         Stroke="Black" StrokeThickness="3" Fill="GreenYellow">
    <Ellipse.RenderTransform>
        <ScaleTransform x:Name="scale" CenterX="50" CenterY="50"/>
    </Ellipse.RenderTransform>
</Ellipse>

In code:

private Point offsetInEllipse;

void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed)
        return;

    ellipse.CaptureMouse();
    offsetInEllipse = e.GetPosition(ellipse);

    var scaleAnimate = new DoubleAnimation(1.25,
        new Duration(TimeSpan.FromSeconds(1)));
    scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
    scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);
}

void ellipse_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
        return;

    var pos = e.GetPosition(canvas);
    Canvas.SetLeft(ellipse, pos.X - offsetInEllipse.X);
    Canvas.SetTop(ellipse, pos.Y - offsetInEllipse.Y);
}

void ellipse_MouseUp(object sender, MouseButtonEventArgs e)
{
    if (!ellipse.IsMouseCaptured)
        return;
    ellipse.ReleaseMouseCapture();

    var scaleAnimate = new DoubleAnimation(1,
        new Duration(TimeSpan.FromSeconds(1)));
    scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
    scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);
}

how do I return Canvas.SetLeft to normal operation after having an animation on the Canvas.LeftProperty?

One way is to set the FillBehavior to Stop:

ellipse.BeginAnimation(Canvas.LeftProperty, new DoubleAnimation(
    pos.X - ellipse.Width * 0.5, 
    new Duration(TimeSpan.FromSeconds(1)), 
    FillBehavior.Stop));
Canvas.SetLeft(ellipse, pos.X - ellipse.Width * 0.5);

That will cause the property to go back to its un-animated value after the animation ends. If you set the value after you start the animation then the un-animated value will just be the final value.

Another way is to clear the animation when you're done:

ellipse.BeginAnimation(Canvas.LeftProperty, null);

Either of those will still cause it to jump when you drag, though. You could have the drag start a new animation every time, but that will make the dragging feel very laggy. Maybe you want to handle the dragging using Canvas.Left, but handle the smooth centering using an animated TranslateTransform?

XAML:

<Ellipse.RenderTransform>
    <TransformGroup>
        <ScaleTransform x:Name="scale" CenterX="50" CenterY="50"/>
        <TranslateTransform x:Name="translate"/>
    </TransformGroup>
</Ellipse.RenderTransform>

Code:

void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed)
        return;

    ellipse.CaptureMouse();

    var scaleAnimate = new DoubleAnimation(1.25,
        new Duration(TimeSpan.FromSeconds(1)));
    scale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
    scale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);

    // We are going to move the center of the ellipse to the mouse
    // location immediately, so start the animation with a shift to
    // get it back to the current center and end the animation at 0.  
    var offsetInEllipse = e.GetPosition(ellipse);
    translate.BeginAnimation(TranslateTransform.XProperty, 
        new DoubleAnimation(ellipse.Width / 2 - offsetInEllipse.X, 0, 
            new Duration(TimeSpan.FromSeconds(1))));
    translate.BeginAnimation(TranslateTransform.YProperty, 
        new DoubleAnimation(ellipse.Height / 2 - offsetInEllipse.Y, 0, 
            new Duration(TimeSpan.FromSeconds(1))));

    MoveEllipse(e);
}

void ellipse_MouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
        return;

    MoveEllipse(e);
}

private void MoveEllipse(MouseEventArgs e)
{
    var pos = e.GetPosition(canvas);
    Canvas.SetLeft(ellipse, pos.X - ellipse.Width / 2);
    Canvas.SetTop(ellipse, pos.Y - ellipse.Height / 2);
}


You probably should look into the Thumb control.

Here is a nice CodeProject using it.


You could get some animations with effects and it would of soften the initial drag effect.... I am afraid the drag animation may not be smoothed out as WPF wasnt just made for this kinds of things, i think you should go for XNA instead :p

This video may suit your needs: http://windowsclient.net/learn/video.aspx?v=280279


As Quartermeister already mentioned you should not specify from value for the animations. This way animation will start with current value and will combine with currently executing animations. Also you should not re-create transformations every time.

Besides that I suggest that you use TranslateTransform instead of setting Top/Left properties of Canvas. It gives you move flexibility and you are not tied to Canvas panel.

So, here is what I got:

XAML:

<Canvas Name="canvas">
    <Ellipse Name="ellipse" Width="100" Height="100" Stroke="Black" StrokeThickness="3" Fill="GreenYellow"
             RenderTransformOrigin="0.5,0.5">
        <Ellipse.RenderTransform>
            <TransformGroup>
                <ScaleTransform />
                <TranslateTransform />
            </TransformGroup>                
        </Ellipse.RenderTransform>
    </Ellipse>
</Canvas>

Code-behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Canvas.SetLeft(ellipse, 0);
        Canvas.SetTop(ellipse, 0);

        ellipse.MouseDown += new MouseButtonEventHandler(ellipse_MouseDown);
        ellipse.MouseMove += new MouseEventHandler(ellipse_MouseMove);
        ellipse.MouseUp += new MouseButtonEventHandler(ellipse_MouseUp);
    }

    private ScaleTransform EllipseScaleTransform
    {
        get { return (ScaleTransform)((TransformGroup)ellipse.RenderTransform).Children[0]; }
    }

    private TranslateTransform EllipseTranslateTransform
    {
        get { return (TranslateTransform)((TransformGroup)ellipse.RenderTransform).Children[1]; }
    }

    void ellipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed)
            return;

        ellipse.CaptureMouse();
        var pos = e.GetPosition(canvas);

        AnimateScaleTo(1.25);
    }

    void ellipse_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton != MouseButtonState.Pressed || !ellipse.IsMouseCaptured)
            return;

        var pos = e.GetPosition(canvas);

        AnimatePositionTo(pos);
    }

    void ellipse_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (!ellipse.IsMouseCaptured)
            return;
        ellipse.ReleaseMouseCapture();

        AnimateScaleTo(1);
    }

    private void AnimateScaleTo(double scale)
    {
        var animationDuration = TimeSpan.FromSeconds(1);
        var scaleAnimate = new DoubleAnimation(scale, new Duration(animationDuration));
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimate);
        EllipseScaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimate);
    }

    private void AnimatePositionTo(Point pos)
    {
        var xOffset = pos.X - ellipse.Width * 0.5;
        var yOffset = pos.Y - ellipse.Height * 0.5;

        var animationDuration = TimeSpan.FromSeconds(1);

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.XProperty,
            new DoubleAnimation(xOffset, new Duration(animationDuration)));

        EllipseTranslateTransform.BeginAnimation(TranslateTransform.YProperty,
            new DoubleAnimation(yOffset, new Duration(animationDuration)));
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜