動畫在 XAML 中也有,而且基本上與 WPF 中的用法一樣。不過,在 UWP 中,動畫還有一種表現方式—— 通過 UI Composition 來創建。
基於 UI Composition 的動畫,相對於 XAML 動畫,有以下優點:
1、不使用 UI 線程,XAML 動畫是共享 UI 線程的,而 Composition 中的動畫是使用輔助線程的。
2、Composition 動畫支持表達式(計算公式)來產生動畫,相對靈活。
老周的建議是:兩者都用,因為基於 XAML 和基於 Composition 的動畫各有特點,在應用程序中都可以混合來用。我們不要被一些不健康的思想所毒害,世界上沒有什么技術可以取代和不取代,只要用得上,哪怕是 1000 年前的技術也同樣適用(事實也表明有些東西我們現在科技這么發達竟然做不到,可咱們祖先在 N 千萬年前反而能做到)。所以,我們應該向庄子先生學習,思維要靈活,合理應用一切可用的資源。
對於動畫,不管是啥類型的,其實基本要素都一樣,首先,動畫是基於時間變化而產生的“眼球欺騙”技術,只是一個個幀隨着時間變化不斷改變,利用人眼的視覺延時誤差,讓我們覺得目標好像在動。其實,人看着在動,但是貓的眼睛看就不見得是這樣了。故,動畫會有時間線,可以說是動畫的時長。
其次是值,比如,你要讓綠色變成紅色,那么在特定的時間點上,你就應該給一個顏色值;再比如,一只豬從屏幕左邊滑到右邊,那么在對應的時間上,你要給出一個坐標值,表明這頭豬滑行了多長距離。
然后就是動畫的作用目標,就是你要把動畫應用到哪個對象的哪個屬性上,要是想改變不透明度,就會選擇應用到 K 對象的 Opacity 屬性上。
在 Composition API 中,Visual 類的屬性都支持動畫,如 Offset,Size 等屬性。
下面我們先介紹一種最經典的動畫類型——關鍵幀。
所謂關鍵幀動畫,就是在時間線上添加 N 個(N 肯定是有效數字)時間點,這些時間點會與一個目標值對應,當動畫播放到這個關鍵幀時,會改變目標值。而關鍵幀之間的部分,就交給某些算法去計算過度動畫。
舉個例子,用關鍵幀動畫改變某對象的 Opacity 屬性(不透明度),時間線總長為 10 秒,在第 0 秒時設定值為 0,即全透明,然后,在第 5 秒時設定值為 0.5,即半透明,最后在第 10 秒處將值設定為 1,表示完全不透明。
下面咱們玩一個例子。
XAML 代碼如下。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Canvas> <Image Name="img" Height="200" Source="Assets/1.png"/> </Canvas> <StackPanel Grid.Row="1" Margin="8" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Content="開始" Click="OnStart"/> <Button Content="停止" Margin="20,0,0,0" Click="OnStop"/> </StackPanel> </Grid>
由於老周比較窮,所以界面放的東西不多。Image 控件用來顯示多拉 B 夢的照片,然后,對,下面兩個按鈕,一個用來啟動播放動畫,另一個用來停止動畫。
Image 為啥要放到 Canvas 容器中呢,因為這個容器,你懂的,它是絕對定位。如果是一個 Grid,可能會受到對齊方式的影響,這樣后面我們要對這個對象的位置進行修改時就很不好弄。
切換到代碼視圖,在頁面類中聲明兩個變量。
Vector3KeyFrameAnimation Animation = null; Visual imageVs = null;
之所以在類級別聲明它們,因為稍后要用。這里,Vector3KeyFrameAnimation 表示關鍵幀動畫是針對 Vector3 這種值進行處理的,待會我們要讓 Image 控件中的 多拉 B 夢 移動。通過老周前面的介紹,大伙應該記得,Offset 屬性表示對象的位置,它有三個值:X、Y、Z,所以,我們要用 Vector3 而不是 Vector2,Vector2 只有兩個值,適用於 Size 屬性。
如果你要對顏色做動畫處理,那就用 ColorKeyFrameAnimation,道理一樣,它使用的值就是 Color 結構類型。如果你進行動畫處理的目標屬性只有一個值,比如 Opacity ,只是一個 float 值,那么,你就可以選用 ScalarKeyFrameAnimation。
在頁面的構造函數中,我們初始化一下各個對象。
public MainPage() { this.InitializeComponent(); // 獲取可視化對象 imageVs = ElementCompositionPreview.GetElementVisual(img); var compos = imageVs.Compositor; // 創建關鍵幀動畫 Animation = compos.CreateVector3KeyFrameAnimation(); // 時長為 4 秒 Animation.Duration = TimeSpan.FromSeconds(4d); // 插入關鍵幀 Animation.InsertKeyFrame(0f, new Vector3(0f, 0f, 0f)); Animation.InsertKeyFrame(0.5f, new Vector3(500f, 360f, 30f)); Animation.InsertKeyFrame(0.7f, new Vector3(260f, 125f, 45f)); Animation.InsertKeyFrame(1f, new Vector3(20f, 20f, 60f)); }
老周在前面的博文中說過,Composition 要用到的各種資源,都可以通過 Compositor 實例的 CreateXXX 方法來創建,動畫也是如此。關鍵幀動畫一定要記得添加關鍵幀,InsertKeyFrame 方法的第一個參數是關鍵幀在時間線上的位置,注意,它采用的是相對值(百分比),從 0.0 到 1.0,如果是 1 則表示關鍵幀在時間線 100% 處,如果是 0.5,關鍵幀正好位於時間線中央。
插入關鍵幀時要記得,它是用百分比來計算的。另外,不要忘了設置一下 Duration 屬性,就是動畫時間線的長度。
接下來,處理一下那兩個按鈕的 Click 事件,分別啟動和停止動畫。
private void OnStart(object sender, RoutedEventArgs e) { imageVs?.StartAnimation(nameof(Visual.Offset), Animation); } private void OnStop(object sender, RoutedEventArgs e) { imageVs?.StopAnimation(nameof(Visual.Offset)); }
要讓動畫對象與目標屬性關聯,可以調用可視化對象的 StartAnimation 方法,第一個參數要指定要應用到的屬性名字,本示例是應用到 Offset 屬性上。要停止正在播放的動畫,只需要把屬性名傳給 StopAnimation 方法即可。
一起來看看效果,多拉B夢在家里經常這樣鍛煉身體的。
由於 gif 動畫的幀率問題,所以你看到截圖上的動畫是不流暢的,想實際體驗就自己動手吧。
下面,老周再給大伙伴們演示一個基於顏色值的動畫。
XAML 代碼很簡單,就放一個 Canvas 就行了。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Canvas Name="cvs"/> </Grid>
然后轉到頁面代碼,初始化一下動畫。
public MainPage() { this.InitializeComponent(); Visual cvsv = ElementCompositionPreview.GetElementVisual(cvs); Compositor compos = cvsv.Compositor; // 創建顏色關鍵幀動畫 ColorKeyFrameAnimation animat = compos.CreateColorKeyFrameAnimation(); // 時間長度 animat.Duration = TimeSpan.FromSeconds(6d); // 讓它永遠循環播放 animat.IterationBehavior = AnimationIterationBehavior.Forever; // 插入關鍵幀 animat.InsertKeyFrame(0f, Colors.Red); animat.InsertKeyFrame(0.6f, Colors.Blue); animat.InsertKeyFrame(1f, Colors.Yellow); // 顏色變化模式 animat.InterpolationColorSpace = CompositionColorSpace.Rgb; // 創建顏色畫刷 CompositionColorBrush brush = compos.CreateColorBrush(Colors.Black); // 創建可視化對象 SpriteVisual sv = compos.CreateSpriteVisual(); // 設置大小和位置 sv.Size = new Vector2(360f, 250f); sv.Offset = new Vector3(150f, 140f, 0f); // 關聯畫刷 sv.Brush = brush; // 把可視化對象插入 XAML 可視化樹 ElementCompositionPreview.SetElementChildVisual(cvs, sv); // 啟動動畫 brush.StartAnimation(nameof(CompositionColorBrush.Color), animat); }
代碼比較長,但有些我前面文章中已經介紹過,我們重點看這段。
// 創建顏色關鍵幀動畫 ColorKeyFrameAnimation animat = compos.CreateColorKeyFrameAnimation(); // 時間長度 animat.Duration = TimeSpan.FromSeconds(6d); // 讓它永遠循環播放 animat.IterationBehavior = AnimationIterationBehavior.Forever; // 插入關鍵幀 animat.InsertKeyFrame(0f, Colors.Red); animat.InsertKeyFrame(0.6f, Colors.Blue); animat.InsertKeyFrame(1f, Colors.Yellow); // 顏色變化模式 animat.InterpolationColorSpace = CompositionColorSpace.Rgb;
首先,當然要創建基於顏色的關鍵幀動畫對象,然后設置一下參數,插入關鍵幀相信你都會了,跟前面那個多拉B夢移動的例子差不多,只是值的類型變成 Color 值而已。
IterationBehavior 屬性用來設置動畫的循環次數,如果你設置為 Count,那么,就要為動畫的 IterationCount 屬性指定一個數值,比如3表示播放三次。這里我設置為 Forever,表示動畫永久循環播放。
InterpolationColorSpace 屬性是個很好玩的東西,主要設置顏色在進行動畫過程如何過度。它用 CompositionColorSpace 枚舉來規范幾個值。經過測試發現,貌似使用 RGB 形式動畫比較正常, RgbLinear 會發生錯誤,但 Rgb 是正常的,所以我就選用 Rgb 模式了。
最后在啟動動畫時要注意,動畫的作用是改變顏色,所以它的應用對象應該是畫刷 CompositionColorBrush 的 Color 屬性,所以,調用 StartAnimation 方法應該在畫刷對象上,而不是 SpriteVisual 對象。
來,看看效果吧。
接下來,我們看一下跳躍式動畫。所謂跳躍式動畫,就是它可以模仿彈簧的物理特性,在動畫停止之前有一個回彈的動作。這個動畫用在控件特效很不錯。
下面我們來個彈球球的實驗。
首先,我們在 XAML 中放一個藍色的球。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Canvas> <Ellipse Name="ell" Width="100" Height="100" Fill="Blue" Canvas.Top="40" Canvas.Left="20"/> </Canvas> </Grid>
隨后,轉到代碼視圖,輸入以下代碼。
public MainPage() { this.InitializeComponent(); Visual ellVisual = ElementCompositionPreview.GetElementVisual(ell); var compositor = ellVisual.Compositor; var springAnmt = compositor.CreateSpringScalarAnimation(); springAnmt.InitialValue = 0f; springAnmt.FinalValue = 400f; springAnmt.Period = TimeSpan.FromMilliseconds(60d); springAnmt.DampingRatio = 0.2f; springAnmt.StopBehavior = AnimationStopBehavior.SetToInitialValue; Windows.System.Threading.ThreadPoolTimer.CreateTimer(timer => { ellVisual?.StartAnimation("Offset.X", springAnmt); }, TimeSpan.FromSeconds(3d)); }
InitialValue 和 FinalValue 屬性分別用於指定動畫的初始值和最終值。如果不指定初始值,那就默認使用當前的值作為初始值。這里有兩個屬性我們要重點關注的。第一個是 DampingRatio ,它是一個大於 0 的值,它表示對象在完成動畫時振動的衰減程度,就像一個球,它落到地面上會彈起來,可是,它不可能永遠都在那里彈,可能彈幾下它就落地不動了。彈性勢能會不斷地衰減。
如果你把 DampingRatio 屬性設置為 0 ,那么,物體就會不停地在彈,而且振幅很大,這是不符合現實物理現象的,因此,這個值你不能用0,一般是用大於0小於1之間,如果大於/等於1,物體幾乎不會振動,非但不振動,反而速度會逐漸變慢。所以,這個 DampingRatio 屬性值,當值小於 1 時,就像在彈簧上彈起來,而當其大於或等於 1 時,就等同於用手按彈簧,越往下按,阻力越大。
還有一個屬性,是配合 DampingRatio 使用的,它就是 Period,它表示每一輪振動的時間,時間越短,物體振動就越快。
本例的設置如下。
springAnmt.Period = TimeSpan.FromMilliseconds(60d); springAnmt.DampingRatio = 0.2f;
表示振動周期為 60 毫秒,振動衰減系數為 0.2,這個值振感明顯,但不會振個不停。
看看效果吧。
其他的跳躍式動畫的用法也一樣,本例所針對的值是可視化對象的Offset 屬性的 X 值,所以是單個 float 值,因此使用 SpringScalarNaturalMotionAnimation。如果處理動畫的目標是其他復雜的值,可以用 SpringVector2NaturalMotionAnimation 或 SpringVector3NaturalMotionAnimation,用法都是一樣的,我就不廢話了,有興趣的伙伴可以試試。
本文最后,我們看一下隱式動畫。啥叫隱式動畫?就是你不必調用 StartAnimation 方法來啟動動畫,當一些支持動畫的屬性更改時,會自動產生動畫。比如,Visual 類的屬性基本支持動畫,像 Opacity、Offset、Orientation、Size 等。
Composition 對象都從 CompositionObject 類上繼承了一個叫 ImplicitAnimations屬性,它是一個集合,我們可以將多個動畫對象加進去,然后,當指定的對象屬性更改時,會自動產生動畫。
ImplicitAnimationCollection 集合是以字典數據形式來存儲的,Key 是要進行動畫處理的屬性名,Value 是對應的動畫實例。這里你可以用關鍵幀動畫,或者上面講到過的跳躍式動畫都可以。為了最大限度保證動畫的兼容性,隱式動畫會存在一定的自動轉換功能。比如,一個針對 Vector3 的動畫可以用於 Vector2 值的屬性,它會從X,Y,Z中取兩個值來填充 Vector2 值。
下面,我們還是用示例來說明吧。我們在 XAML 中放一個物體。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Canvas> <Ellipse Name="ell" Fill="Green" Width="150" Height="150"/> </Canvas> <StackPanel Grid.Row="1" Margin="2,12" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Content="動作 1" Margin="0,0,24,0" Click="OnClick1"/> <Button Content="動作 2" Margin="0,0,24,0" Click="OnClick2"/> <Button Content="動作 3" Margin="0,0,24,0" Click="OnClick3"/> <Button Content="動作 4" Click="OnClick4" /> </StackPanel> </Grid>
下面的四個按鈕的作用是修改上面那個圓的 Offset,Opacity 屬性,說白了,就是修改它的位置和不透明度。
現在,我們轉到代碼視圖,先在類級別聲明一個 Visual 類型的變量,它表示上面的 Ellipse 對象的可視化對象引用,應用我們在四個按鈕的 Click 事件處理代碼中要訪問它,所以把其作為類級別的字段。
Visual ell_vs;
然后,在頁面類的構造函數中初始化。
public MainPage() { this.InitializeComponent(); // 設置動畫 ell_vs = ElementCompositionPreview.GetElementVisual(ell); Compositor compos = ell_vs.Compositor; ImplicitAnimationCollection implicitAnmts = compos.CreateImplicitAnimationCollection(); ScalarKeyFrameAnimation opacityAnmt = compos.CreateScalarKeyFrameAnimation(); opacityAnmt.InsertExpressionKeyFrame(0f, "this.StartingValue"); opacityAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue"); opacityAnmt.Duration = TimeSpan.FromSeconds(1d); opacityAnmt.Target = nameof(Visual.Opacity); Vector3KeyFrameAnimation offsetAnmt = compos.CreateVector3KeyFrameAnimation(); offsetAnmt.InsertExpressionKeyFrame(0f, "this.StartingValue"); offsetAnmt.InsertExpressionKeyFrame(1f, "this.FinalValue"); offsetAnmt.Duration = TimeSpan.FromSeconds(1d); offsetAnmt.Target = nameof(Visual.Offset); implicitAnmts.Add(nameof(Visual.Offset), offsetAnmt); implicitAnmts[nameof(Visual.Opacity)] = opacityAnmt; ell_vs.ImplicitAnimations = implicitAnmts; }
請注意,在為動畫插入關鍵幀時,使用的是表達式的方法,因為我們后面是對對象的不透明度和位置進行動態調整,所以,這里的代碼並不能准確知道動畫的最終值是什么,所以,使用了這兩個關鍵字:
this.StartingValue:表示動畫的初始值,它會根據實際情況自動填充值。
this.FinalValue:指的是動畫的最終值,它會自動填充。
在這個例子中,StartingValue 就是對象上一次被修改后的值,比如,第一次把 Opacity 改為 0.5,那么下一輪動畫時的初始就是這個 0.5。FinalValue就是屬性的最新值,比如Opacity 原來是 1,現在你改為 0.6,那么對本次動畫來說,StartingValue 就是 1,FinalValue 就是 0.6 了。
對了,還有一點,你得為動畫的 Target 屬性賦值,比如動畫是作用於 Opacity 屬性上的,就賦 Opacity 。這個隱式動畫比較特殊,一定要這樣賦值。
然后,我們給四個按鈕弄弄 Click 事件。
private void OnClick1(object sender, RoutedEventArgs e) { ell_vs.Opacity = 0.2f; ell_vs.Offset = new Vector3(300f, 250f, -30f); } private void OnClick2(object sender, RoutedEventArgs e) { ell_vs.Opacity = 0.8f; ell_vs.Offset = new Vector3(400f, 320f, 130f); } private void OnClick3(object sender, RoutedEventArgs e) { ell_vs.Opacity = 1f; ell_vs.Offset = new Vector3(150f, 60f, -70f); } private void OnClick4(object sender, RoutedEventArgs e) { ell_vs.Offset = new Vector3(20f, 200f, 50f); ell_vs.Opacity = 0.5f; }
好,現在可以看效果了。運行應用,分別點四個按鈕,看看它們這樣修改對象的屬性會不會更生動。
好了,本文就講到這里吧。
你一定會記得,還有一個表達式動畫,那個咱們留到下一篇文章再聊,本篇就先聊到這里。示例的代碼我都基本貼上了,所以我就不上傳示例了。