UWP Background过渡动画

日期 2018-09-09 C#,UWP 作者 叫我蓝火火 共0评论

前些日子看到Xaml Controls Gallery的ToggleTheme过渡非常心水,大概是这样的:

在17134 SDK里写法如下:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.BackgroundTransition>
        <BrushTransition Duration="0:0:0.4" />
    </Grid.BackgroundTransition>
</Grid>

这和我原本的思路完全不同。
我原本的思路是定义一个静态的笔刷资源,然后动画修改他的Color,但是这样就不能和系统的笔刷资源很好的融合了。怎么办呢?
前天半梦半醒间,突然灵光一现,感觉可以用一个附加属性作为中间层,给Background赋临时的笔刷实现过渡。
闲话不多说,开干。
首先我们需要一个画刷,这个画刷要实现以下功能:

  • 拥有一个Color属性。
  • 对Color属性赋值时会播放动画。
  • 动画播放结束触发事件。
  • 可以从外部清理事件。

这个可以使用Storyboard,CompositionAnimation手动Start或者ImplicitAnimation实现,在这里我选择了我最顺手的Composition实现。
下面贴代码:

public class FluentSolidColorBrush : XamlCompositionBrushBase
{
    Compositor Compositor => Window.Current.Compositor;
    ColorKeyFrameAnimation ColorAnimation;
    bool IsConnected;

    //被设置到控件属性时触发,例RootGrid.Background=new FluentSolidColorBrush();
    protected override void OnConnected()
    {
        if (CompositionBrush == null)
        {
            IsConnected = true;

            ColorAnimation = Compositor.CreateColorKeyFrameAnimation();

            //进度为0的关键帧,表达式为起始颜色。
            ColorAnimation.InsertExpressionKeyFrame(0f, "this.StartingValue");

            //进度为0的关键帧,表达式为参数名为Color的参数。
            ColorAnimation.InsertExpressionKeyFrame(1f, "Color");

            //创建颜色笔刷
            CompositionBrush = Compositor.CreateColorBrush(Color);
        }
    }

    //从属性中移除时触发,例RootGrid.Background=null;
    protected override void OnDisconnected()
    {
        if (CompositionBrush != null)
        {
            IsConnected = false;

            ColorAnimation.Dispose();
            ColorAnimation = null;
            CompositionBrush.Dispose();
            CompositionBrush = null;

            //清除已注册的事件。
            ColorChanged = null;
        }
    }

    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue(DurationProperty); }
        set { SetValue(DurationProperty, value); }
    }

    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FluentSolidColorBrush), new PropertyMetadata(TimeSpan.FromSeconds(0.4d), (s, a) =>
        {
            if (a.NewValue != a.OldValue)
            {
                if (s is FluentSolidColorBrush sender)
                {
                    if (sender.ColorAnimation != null)
                    {
                        sender.ColorAnimation.Duration = (TimeSpan)a.NewValue;
                    }
                }
            }
        }));

    public Color Color
    {
        get { return (Color)GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }

    public static readonly DependencyProperty ColorProperty =
        DependencyProperty.Register("Color", typeof(Color), typeof(FluentSolidColorBrush), new PropertyMetadata(default(Color), (s, a) =>
        {
            if (a.NewValue != a.OldValue)
            {
                if (s is FluentSolidColorBrush sender)
                {
                    if (sender.IsConnected)
                    {
                        //给ColorAnimation,进度为1的帧的参数Color赋值
                        sender.ColorAnimation.SetColorParameter("Color", (Color)a.NewValue);

                        //创建一个动画批,CompositionAnimation使用批控制动画完成。
                        var batch = sender.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation);

                        //批内所有动画完成事件,完成时如果画刷没有Disconnected,则触发ColorChanged
                        batch.Completed += (s1, a1) =>
                        {
                            if (sender.IsConnected)
                            {
                                sender.OnColorChanged((Color)a.OldValue, (Color)a.NewValue);
                            }
                        };
                        sender.CompositionBrush.StartAnimation("Color", sender.ColorAnimation);
                        batch.End();
                    }
                }
            }
        }));

    public event ColorChangedEventHandler ColorChanged;
    private void OnColorChanged(Color oldColor, Color newColor)
    {
        ColorChanged?.Invoke(this, new ColorChangedEventArgs()
        {
            OldColor = oldColor,
            NewColor = newColor
        });
    }
}

public delegate void ColorChangedEventHandler(object sender, ColorChangedEventArgs args);
public class ColorChangedEventArgs : EventArgs
{
    public Color OldColor { get; internal set; }
    public Color NewColor { get; internal set; }
}

这样这个笔刷在每次修改Color的时候就能自动触发动画了,这完成了我思路的第一步,接下来我们需要一个Background属性设置时的中间层,用来给两个颜色之间添加过渡,这个使用附加属性和Behavior都可以实现。
我开始选择了Behavior,优点是可以在VisualState的Storyboard节点中赋值,而且由于每个Behavior都是独立的属性,可以存储更多的非公共属性、状态等;但是缺点也非常明显,使用Behavior要引入"Microsoft.Xaml.Behaviors.Uwp.Managed"这个包,使用的时候也要使用至少三行代码。
而附加属性呢,优点是原生和短,缺点是不能存储过多状态,也不能在Storyboard里使用,只能用Setter控制。
不过对于我们的需求呢,只需要Background和Duration两个属性,综上所述,最终我选择了附加属性实现。
闲话不多说,继续贴代码:

public class TransitionsHelper : DependencyObject
{
    public static Brush GetBackground(FrameworkElement obj)
    {
        return (Brush)obj.GetValue(BackgroundProperty);
    }

    public static void SetBackground(FrameworkElement obj, Brush value)
    {
        obj.SetValue(BackgroundProperty, value);
    }

    public static TimeSpan GetDuration(FrameworkElement obj)
    {
        return (TimeSpan)obj.GetValue(DurationProperty);
    }

    public static void SetDuration(FrameworkElement obj, TimeSpan value)
    {
        obj.SetValue(DurationProperty, value);
    }

    public static readonly DependencyProperty BackgroundProperty =
        DependencyProperty.RegisterAttached("Background", typeof(Brush), typeof(TransitionsHelper), new PropertyMetadata(null, BackgroundPropertyChanged));

    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.RegisterAttached("Duration", typeof(TimeSpan), typeof(TransitionsHelper), new PropertyMetadata(TimeSpan.FromSeconds(0.6d)));

    private static void BackgroundPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != e.OldValue)
        {
            if (d is FrameworkElement sender)
            {
                //拿到New和Old的Brush,因为Brush可能不是SolidColorBrush,这里不能使用强制类型转换。
                var NewBrush = e.NewValue as SolidColorBrush;
                var OldBrush = e.OldValue as SolidColorBrush;

                //下面分别获取不同控件的Background依赖属性。
                DependencyProperty BackgroundProperty = null;
                if (sender is Panel)
                {
                    BackgroundProperty = Panel.BackgroundProperty;
                }
                else if (sender is Control)
                {
                    BackgroundProperty = Control.BackgroundProperty;
                }
                else if (sender is Shape)
                {
                    BackgroundProperty = Shape.FillProperty;
                }

                if (BackgroundProperty == null) return;

                //如果当前笔刷是FluentSolidColorBrush,就将当前笔刷设置成旧笔刷,触发FluentSolidColorBrush的OnDisconnected,
                //OnDisconnected中会清理掉ColorChanged上注册的事件,防止笔刷在卸载之后,动画完成时触发事件,导致运行不正常。
                if (sender.GetValue(BackgroundProperty) is FluentSolidColorBrush tmp_fluent)
                {
                    sender.SetValue(BackgroundProperty, OldBrush);
                }

                //如果OldBrush或者NewBrush中有一个为空,就不播放动画,直接赋值
                if (OldBrush == null || NewBrush == null)
                {
                    sender.SetValue(BackgroundProperty, NewBrush);
                    return;
                }

                var FluentBrush = new FluentSolidColorBrush()
                {
                    Duration = GetDuration(sender),
                    Color = OldBrush.Color,
                };
                FluentBrush.ColorChanged += (s, a) =>
                {
                    sender.SetValue(BackgroundProperty, NewBrush);
                };
                sender.SetValue(BackgroundProperty, FluentBrush);
                FluentBrush.Color = NewBrush.Color;
            }
        }
    }
}

调用的时候就不能直接设置Background了:

<Grid helper:TransitionsHelper.Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button x:Name="ToggleTheme" Click="ToggleTheme_Click">ToggleTheme</Button>
</Grid>

在Style里调用方法也类似

<!-- Element中 -->
<Grid x:Name="RootGrid" helper:TransitionsHelper.Background="{TemplateBinding Background}">
    ...
</Grid>

<!-- VisualState中 -->
<VisualState x:Name="TestState">
    <VisualState.Setter>
        <Setter Target="RootGrid.(helper:TransitionsHelper.Background)" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=SecondBackground}" />
    </VisualState.Setter>
</VisualState>

这里还有个点要注意,在VisualState中,不管是Storyboard还是Setter,如果要修改模板绑定,直接写Value="{TemplateBinding XXX}"会报错,正确的写法是Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=SecondBackground}"。
最后附一张效果图:

如果有错误请告诉我,谢谢各位大佬