天天看點

UWP Background過渡動畫

原文: UWP Background過渡動畫

首先說兩件事:

1、大爆炸我還記着呢,先欠着吧。。。

2、部落格搬家啦,新位址:

https://blog.ultrabluefire.cn/

==========下面是正文==========

前些日子看到Xaml Controls Gallery的ToggleTheme過渡非常心水,大概是這樣的:

UWP Background過渡動畫
在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實作。

下面貼代碼:

UWP Background過渡動畫
UWP Background過渡動畫
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兩個屬性,綜上所述,最終我選擇了附加屬性實作。

閑話不多說,繼續貼代碼:

UWP Background過渡動畫
UWP Background過渡動畫
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}"。

最後附一張效果圖:

UWP Background過渡動畫

原文位址:

https://blog.ultrabluefire.cn/archives/13.html