首先说两件事:
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 }
这样这个笔刷在每次修改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