首先说两件事:
1、大爆炸我还记着呢,先欠着吧。。。
2、博客搬家啦,新地址:
https://blog.ultrabluefire.cn/==========下面是正文==========
前些日子看到Xaml Controls Gallery的ToggleTheme过渡非常心水,大概是这样的:
在17134 SDK里写法如下:1 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
2 <Grid.BackgroundTransition>
3 <BrushTransition Duration="0:0:0.4" />
4 </Grid.BackgroundTransition>
5 </Grid>
这和我原本的思路完全不同。
我原本的思路是定义一个静态的笔刷资源,然后动画修改他的Color,但是这样就不能和系统的笔刷资源很好的融合了。怎么办呢?
前天半梦半醒间,突然灵光一现,感觉可以用一个附加属性作为中间层,给Background赋临时的笔刷实现过渡。
闲话不多说,开干。
首先我们需要一个画刷,这个画刷要实现以下功能:
- 拥有一个Color属性。
- 对Color属性赋值时会播放动画。
- 动画播放结束触发事件。
- 可以从外部清理事件。
这个可以使用Storyboard,CompositionAnimation手动Start或者ImplicitAnimation实现,在这里我选择了我最顺手的Composition实现。
下面贴代码:
1 public class FluentSolidColorBrush : XamlCompositionBrushBase, IDisposable
2 {
3 public FluentSolidColorBrush()
4 {
5 ColorAnimation = Compositor.CreateColorKeyFrameAnimation();
6
7 //进度为0的关键帧,表达式为起始颜色。
8 ColorAnimation.InsertExpressionKeyFrame(0f, "this.StartingValue");
9
10 //进度为1的关键帧,表达式为参数名为Color的参数。
11 ColorAnimation.InsertExpressionKeyFrame(1f, "Color");
12
13 //创建颜色笔刷
14 CompositionBrush = Compositor.CreateColorBrush();
15 }
16
17 ~FluentSolidColorBrush()
18 {
19 Dispose(false);
20 }
21
22 Compositor Compositor => Window.Current.Compositor;
23 ColorKeyFrameAnimation ColorAnimation;
24 bool IsConnected;
25
26 //被设置到控件属性时触发,例RootGrid.Background=new FluentSolidColorBrush();
27 protected override void OnConnected()
28 {
29 IsConnected = true;
30 }
31
32 //从属性中移除时触发,例RootGrid.Background=null;
33 protected override void OnDisconnected()
34 {
35 IsConnected = false;
36 }
37
38 protected virtual void Dispose(bool disposing)
39 {
40 ColorAnimation.Dispose();
41 ColorAnimation = null;
42 CompositionBrush.Dispose();
43 CompositionBrush = null;
44
45 //清除已注册的事件。
46 ColorChanged = null;
47
48 if (disposing)
49 {
50 GC.SuppressFinalize(this);
51 }
52 }
53
54 public void Dispose()
55 {
56 Dispose(true);
57 }
58
59 public TimeSpan Duration
60 {
61 get { return (TimeSpan)GetValue(DurationProperty); }
62 set { SetValue(DurationProperty, value); }
63 }
64
65 public static readonly DependencyProperty DurationProperty =
66 DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FluentSolidColorBrush), new PropertyMetadata(TimeSpan.FromSeconds(0.4d), (s, a) =>
67 {
68 if (a.NewValue != a.OldValue)
69 {
70 if (s is FluentSolidColorBrush sender)
71 {
72 if (sender.ColorAnimation != null)
73 {
74 sender.ColorAnimation.Duration = (TimeSpan)a.NewValue;
75 }
76 }
77 }
78 }));
79
80
81
82 public Color Color
83 {
84 get { return (Color)GetValue(ColorProperty); }
85 set { SetValue(ColorProperty, value); }
86 }
87
88 public static readonly DependencyProperty ColorProperty =
89 DependencyProperty.Register("Color", typeof(Color), typeof(FluentSolidColorBrush), new PropertyMetadata(default(Color), (s, a) =>
90 {
91 if (a.NewValue != a.OldValue)
92 {
93 if (s is FluentSolidColorBrush sender)
94 {
95 if (sender.IsConnected)
96 {
97 //给ColorAnimation,进度为1的帧的参数Color赋值
98 sender.ColorAnimation.SetColorParameter("Color", (Color)a.NewValue);
99
100 //创建一个动画批,CompositionAnimation使用批控制动画完成。
101 var batch = sender.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
102
103 //批内所有动画完成事件,完成时如果画刷没有Disconnected,则触发ColorChanged
104 batch.Completed += (s1, a1) =>
105 {
106 if (sender.IsConnected)
107 {
108 sender.OnColorChanged((Color)a.OldValue, (Color)a.NewValue);
109 }
110 };
111 sender.CompositionBrush.StartAnimation("Color", sender.ColorAnimation);
112 batch.End();
113 }
114 else
115 {
116 ((CompositionColorBrush)sender.CompositionBrush).Color = (Color)a.NewValue;
117 }
118 }
119 }
120 }));
121
122 public event ColorChangedEventHandler ColorChanged;
123 private void OnColorChanged(Color oldColor, Color newColor)
124 {
125 ColorChanged?.Invoke(this, new ColorChangedEventArgs()
126 {
127 OldColor = oldColor,
128 NewColor = newColor
129 });
130 }
131 }
132
133 public delegate void ColorChangedEventHandler(object sender, ColorChangedEventArgs args);
134 public class ColorChangedEventArgs : EventArgs
135 {
136 public Color OldColor { get; internal set; }
137 public Color NewColor { get; internal set; }
138 }
View Code
这样这个笔刷在每次修改Color的时候就能自动触发动画了,这完成了我思路的第一步,接下来我们需要一个Background属性设置时的中间层,用来给两个颜色之间添加过渡,这个使用附加属性和Behavior都可以实现。
我开始选择了Behavior,优点是可以在VisualState的Storyboard节点中赋值,而且由于每个Behavior都是独立的属性,可以存储更多的非公共属性、状态等;但是缺点也非常明显,使用Behavior要引入"Microsoft.Xaml.Behaviors.Uwp.Managed"这个包,使用的时候也要使用至少三行代码。
而附加属性呢,优点是原生和短,缺点是不能存储过多状态,也不能在Storyboard里使用,只能用Setter控制。
不过对于我们的需求呢,只需要Background和Duration两个属性,综上所述,最终我选择了附加属性实现。
闲话不多说,继续贴代码:
1 public class TransitionsHelper : DependencyObject
2 {
3 public static Brush GetBackground(FrameworkElement obj)
4 {
5 return (Brush)obj.GetValue(BackgroundProperty);
6 }
7
8 public static void SetBackground(FrameworkElement obj, Brush value)
9 {
10 obj.SetValue(BackgroundProperty, value);
11 }
12
13 public static TimeSpan GetDuration(FrameworkElement obj)
14 {
15 return (TimeSpan)obj.GetValue(DurationProperty);
16 }
17
18 public static void SetDuration(FrameworkElement obj, TimeSpan value)
19 {
20 obj.SetValue(DurationProperty, value);
21 }
22
23 public static readonly DependencyProperty BackgroundProperty =
24 DependencyProperty.RegisterAttached("Background", typeof(Brush), typeof(TransitionsHelper), new PropertyMetadata(null, BackgroundPropertyChanged));
25
26 public static readonly DependencyProperty DurationProperty =
27 DependencyProperty.RegisterAttached("Duration", typeof(TimeSpan), typeof(TransitionsHelper), new PropertyMetadata(TimeSpan.FromSeconds(0.6d)));
28
29 private static void BackgroundPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
30 {
31 if (e.NewValue != e.OldValue)
32 {
33 if (d is FrameworkElement sender)
34 {
35 //拿到New和Old的Brush,因为Brush可能不是SolidColorBrush,这里不能使用强制类型转换。
36 var NewBrush = e.NewValue as SolidColorBrush;
37 var OldBrush = e.OldValue as SolidColorBrush;
38
39 //下面分别获取不同控件的Background依赖属性。
40 DependencyProperty BackgroundProperty = null;
41 if (sender is Panel)
42 {
43 BackgroundProperty = Panel.BackgroundProperty;
44 }
45 else if (sender is Control)
46 {
47 BackgroundProperty = Control.BackgroundProperty;
48 }
49 else if (sender is Shape)
50 {
51 BackgroundProperty = Shape.FillProperty;
52 }
53
54 if (BackgroundProperty == null) return;
55
56 //如果当前笔刷是FluentSolidColorBrush,就清理掉附加的事件,防止笔刷在卸载之后,动画完成时触发事件,导致运行不正常。
57 //如果使用Behavior,可以单独存储当前的FluentSolidColorBrush和NewBrush,不用lambda表达式注册事件,就不用这么Hack的清理事件列表了。
58 //而在附加属性中,由于存储一个对象的对应的值太复杂了,所以不单独存储NewBrush,利用lambda的变量作用域去访问他。
59 if (sender.GetValue(BackgroundProperty) is FluentSolidColorBrush tmp_fluent)
60 {
61 tmp_fluent.Dispose();
62 }
63
64 //如果OldBrush或者NewBrush中有一个为空,就不播放动画,直接赋值
65 if (OldBrush == null || NewBrush == null)
66 {
67 sender.SetValue(BackgroundProperty, NewBrush);
68 return;
69 }
70
71 var FluentBrush = new FluentSolidColorBrush()
72 {
73 Duration = GetDuration(sender),
74 Color = OldBrush.Color,
75 };
76 FluentBrush.ColorChanged += (s, a) =>
77 {
78 sender.SetValue(BackgroundProperty, NewBrush);
79 if (s is FluentSolidColorBrush tmp_fluent2)
80 {
81 tmp_fluent2.Dispose();
82 }
83 };
84 sender.SetValue(BackgroundProperty, FluentBrush);
85 FluentBrush.Color = NewBrush.Color;
86 }
87 }
88 }
89 }
调用的时候就不能直接设置Background了:
1 <Grid helper:TransitionsHelper.Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
2 <Button x:Name="ToggleTheme" Click="ToggleTheme_Click">ToggleTheme</Button>
3 </Grid>
在Style里调用方法也类似:
1 <!-- Element中 -->
2 <Grid x:Name="RootGrid" helper:TransitionsHelper.Background="{TemplateBinding Background}">
3 ...
4 </Grid>
5
6 <!-- VisualState中 -->
7 <VisualState x:Name="TestState">
8 <VisualState.Setter>
9 <Setter Target="RootGrid.(helper:TransitionsHelper.Background)" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=SecondBackground}" />
10 </VisualState.Setter>
11 </VisualState>
这里还有个点要注意,在VisualState中,不管是Storyboard还是Setter,如果要修改模板绑定,直接写Value="{TemplateBinding XXX}"会报错,正确的写法是Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=SecondBackground}"。
最后附一张效果图:
原文地址:
https://blog.ultrabluefire.cn/archives/13.html