如何在WPF中拖动对象时实现平滑的动画效果

时间:2011-02-06 15:05:31

标签: c# wpf

我正在尝试学习一些WPF,我希望能够实现一个简单的游戏。在这个游戏中,Canvas上有几个项目。出于这个问题的目的,让我们说只有一个,它是Ellipse

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

用户需要能够随意拖动这些项目。

所以我实现了以下代码,它似乎有效:

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;
}

现在,如果你试试这个,你会发现这些动作非常不稳定。当您向下鼠标时,椭圆会立即增长并立即改变其透明度。我想让它变得平滑,以便没有突然跳跃。

我在DoubleAnimationEllipse.OpacityProperty和(特别是)ScaleTransform.ScaleXProperty上使用Canvas.LeftProperty/TopProperty尝试了显而易见的事情。但是,我遇到了以下问题:

  • 一旦我在Canvas.LeftProperty/TopProperty开始播放动画,我就再也不能使用Canvas.SetLeft/Top了,因此拖动时椭圆不会移动。我找不到从对象中删除动画的方法。

  • 如果用户在动画仍然发生时释放鼠标,ScaleTransform上的“缩小”动画将从“增长”动画到达之前的全尺寸开始,这会突然导致跳。如果你疯狂地点击鼠标,对象的大小会疯狂跳跃,不应该跳过。

如果需要,您可以look at my failed code, which doesn’t work

如何在WPF中正确实现这些平滑动作?

请不要先试用,否则不要发布答案。如果有任何突然跳跃,结果是不能令人满意的。谢谢!

4 个答案:

答案 0 :(得分:3)

不是为每个更改创建一个新的ScaleTransform,而是使用相同的一个并继续应用新的动画。如果没有为动画指定From属性,它将以当前值开始并执行平滑动画。

要避免位置跳过,请记住鼠标在椭圆内的位置,而不是始终居中。这样你就不用担心重新定位了。 (您可以使用空时间轴调用BeginAnimation来停止当前动画,但之后您将在第一个MouseMove上跳转。)

在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>

在代码中:

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);
}

  

如何返回Canvas.SetLeft   有一个正常的操作   动画在Canvas.LeftProperty

一种方法是将FillBehavior设置为停止:

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);

这会导致属性在动画结束后返回其未设置动画的值。如果在开始动画后设置值,则未动画值将只是最终值。

另一种方法是在完成后清除动画:

ellipse.BeginAnimation(Canvas.LeftProperty, null);

但是,当你拖动时,其中任何一个都会导致它跳跃。您可以让拖动每次都开始一个新的动画,但这会使拖动感觉非常滞后。也许您想使用Canvas.Left处理拖动,但使用动画TranslateTransform处理平滑居中?

XAML:

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

代码:

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);
}

答案 1 :(得分:2)

您可能应该查看Thumb控件。

这是使用它的nice CodeProject

答案 2 :(得分:1)

你可以获得一些带效果的动画,它会软化初始拖动效果....我担心拖动动画可能不会被平滑掉,因为WPF不是为这种事情做的,我想你应该去对于XNA而言:p

此视频可能符合您的需求: http://windowsclient.net/learn/video.aspx?v=280279

答案 3 :(得分:1)

正如Quartermeister已经提到的那样,你不应该为动画指定from值。这样,动画将以当前值开始,并将与当前正在执行的动画结合使用。此外,您不应每次都重新创建转换。

除此之外,我建议您使用TranslateTransform而不是设置Top的{​​{1}} / Left属性。它为您提供了移动灵活性,而且您不依赖于Canvas面板。

所以,这就是我得到的:

<强> XAML:

Canvas

<强>代码隐藏:

<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>