首先說兩件事:
1、大爆炸我還記着呢,先欠着吧。。。
2、部落格搬家啦,新位址:
https://blog.ultrabluefire.cn/==========下面是正文==========
前些日子看到Xaml Controls Gallery的ToggleTheme過渡非常心水,大概是這樣的:

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