首先說兩件事: 1、大爆炸我還記著呢,先欠著吧。。。 2、博客搬家啦,新地址:https://blog.ultrabluefire.cn/ 下麵是正文 前些日子看到Xaml Controls Gallery的ToggleTheme過渡非常心水,大概是這樣的: 在17134 SDK里寫法如下: 這和我 ...
首先說兩件事:
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 2 { 3 Compositor Compositor => Window.Current.Compositor; 4 ColorKeyFrameAnimation ColorAnimation; 5 bool IsConnected; 6 7 //被設置到控制項屬性時觸發,例RootGrid.Background=new FluentSolidColorBrush(); 8 protected override void OnConnected() 9 { 10 if (CompositionBrush == null) 11 { 12 IsConnected = true; 13 14 ColorAnimation = Compositor.CreateColorKeyFrameAnimation(); 15 16 //進度為0的關鍵幀,表達式為起始顏色。 17 ColorAnimation.InsertExpressionKeyFrame(0f, "this.StartingValue"); 18 19 //進度為0的關鍵幀,表達式為參數名為Color的參數。 20 ColorAnimation.InsertExpressionKeyFrame(1f, "Color"); 21 22 //創建顏色筆刷 23 CompositionBrush = Compositor.CreateColorBrush(Color); 24 } 25 } 26 27 //從屬性中移除時觸發,例RootGrid.Background=null; 28 protected override void OnDisconnected() 29 { 30 if (CompositionBrush != null) 31 { 32 IsConnected = false; 33 34 ColorAnimation.Dispose(); 35 ColorAnimation = null; 36 CompositionBrush.Dispose(); 37 CompositionBrush = null; 38 39 //清除已註冊的事件。 40 ColorChanged = null; 41 } 42 } 43 44 public TimeSpan Duration 45 { 46 get { return (TimeSpan)GetValue(DurationProperty); } 47 set { SetValue(DurationProperty, value); } 48 } 49 50 public static readonly DependencyProperty DurationProperty = 51 DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FluentSolidColorBrush), new PropertyMetadata(TimeSpan.FromSeconds(0.4d), (s, a) => 52 { 53 if (a.NewValue != a.OldValue) 54 { 55 if (s is FluentSolidColorBrush sender) 56 { 57 if (sender.ColorAnimation != null) 58 { 59 sender.ColorAnimation.Duration = (TimeSpan)a.NewValue; 60 } 61 } 62 } 63 })); 64 65 public Color Color 66 { 67 get { return (Color)GetValue(ColorProperty); } 68 set { SetValue(ColorProperty, value); } 69 } 70 71 public static readonly DependencyProperty ColorProperty = 72 DependencyProperty.Register("Color", typeof(Color), typeof(FluentSolidColorBrush), new PropertyMetadata(default(Color), (s, a) => 73 { 74 if (a.NewValue != a.OldValue) 75 { 76 if (s is FluentSolidColorBrush sender) 77 { 78 if (sender.IsConnected) 79 { 80 //給ColorAnimation,進度為1的幀的參數Color賦值 81 sender.ColorAnimation.SetColorParameter("Color", (Color)a.NewValue); 82 83 //創建一個動畫批,CompositionAnimation使用批控制動畫完成。 84 var batch = sender.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation); 85 86 //批內所有動畫完成事件,完成時如果畫刷沒有Disconnected,則觸發ColorChanged 87 batch.Completed += (s1, a1) => 88 { 89 if (sender.IsConnected) 90 { 91 sender.OnColorChanged((Color)a.OldValue, (Color)a.NewValue); 92 } 93 }; 94 sender.CompositionBrush.StartAnimation("Color", sender.ColorAnimation); 95 batch.End(); 96 } 97 } 98 } 99 })); 100 101 public event ColorChangedEventHandler ColorChanged; 102 private void OnColorChanged(Color oldColor, Color newColor) 103 { 104 ColorChanged?.Invoke(this, new ColorChangedEventArgs() 105 { 106 OldColor = oldColor, 107 NewColor = newColor 108 }); 109 } 110 } 111 112 public delegate void ColorChangedEventHandler(object sender, ColorChangedEventArgs args); 113 public class ColorChangedEventArgs : EventArgs 114 { 115 public Color OldColor { get; internal set; } 116 public Color NewColor { get; internal set; } 117 }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,就將當前筆刷設置成舊筆刷,觸發FluentSolidColorBrush的OnDisconnected, 57 //OnDisconnected中會清理掉ColorChanged上註冊的事件,防止筆刷在卸載之後,動畫完成時觸發事件,導致運行不正常。 58 if (sender.GetValue(BackgroundProperty) is FluentSolidColorBrush tmp_fluent) 59 { 60 sender.SetValue(BackgroundProperty, OldBrush); 61 } 62 63 //如果OldBrush或者NewBrush中有一個為空,就不播放動畫,直接賦值 64 if (OldBrush == null || NewBrush == null) 65 { 66 sender.SetValue(BackgroundProperty, NewBrush); 67 return; 68 } 69 70 var FluentBrush = new FluentSolidColorBrush() 71 { 72 Duration = GetDuration(sender), 73 Color = OldBrush.Color, 74 }; 75 FluentBrush.ColorChanged += (s, a) => 76 { 77 sender.SetValue(BackgroundProperty, NewBrush); 78 }; 79 sender.SetValue(BackgroundProperty, FluentBrush); 80 FluentBrush.Color = NewBrush.Color; 81 } 82 } 83 } 84 }View Code
調用的時候就不能直接設置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