課程內容
Ø Animation
Ø Event Triggers
Ø Named Resources
Ø Settings Page
Ø Color Picker
Ø Clipping
Silly Eye是一個非常吸引眼球的應用,特別是在一群小孩子中間。該應用程式會以一種有趣、近乎瘋狂的方式來展示一個巨大的卡通眼球。展示該應用程式,只需要將手機放在右眼的前面,並假裝它就是你的右眼球。本應用程式介紹了一些有用的新技術,並且與建立新的頁面和選擇使用者自訂的顏色相關。但最重要的是,這是與本書第二部分的主題“Transforms & Animations”中“Animations”相關的第一個應用程式。
Introducing Animation
大多數人看到動畫,就會認為是一種類似於卡通的東西,它利用連續快速地播放圖片來類比運動效果。在Silverlight中,動畫有一個更加詳細的定義:在時間軸上改變一個屬性的值。這與運動相關,例如,通過增加元素的寬度來營造一種生長的效果,或者,改變一個元素的不透明度來營造另一種完全不同的效果。
在時間軸上改變一個屬性的值有很多種方法。最經典的方法是使用定時器,比如,很多前面章節中使用的DispatcherTimer,以及基於定時器觸發時間而設定的周期性回調方法(Tick事件處理)。在這種方法中,我們可以手動更新目標屬性(即基於逝去的時間,通過數學計算來決定當前的屬性值),直到最終的設定值。這個時候,我們可以停止計時器,並且/或者刪除事件處理常式。
但是,Silverlight提供了一種更容易使用的動畫機制,它更加強大,效能要比定時器方法更加出色。這是一種以Storyboard的對象為中心的機制。Storyboard包含了一個或多個特定的動畫對象,它們被應用到特定元素的指定屬性中。
Silly Eye使用了三個Storyboard來實現動畫。為了理解Storyboard的概念和工作原理,我們來學習以下三個內容:
➔ 與瞳孔相關的 Storyboard
➔ 與虹膜相關的 Storyboard
➔ 與眼瞼相關的 Storyboard
The Pupil Storyboard
下面是Silly Eye應用程式中,將Storyboard應用到瞳孔中,使其進行重複地擴張和縮小。其注意點如下:
➔ Storyboard.TargetName這個可附加的屬性工作表明,該動畫被應用到本頁面中一個名為Pupil的元素中,而Pupil是一個橢圓。
➔ Storyboard.TargetProperty這個可附加的屬性工作表明,Pupil的StrokeThickness屬性被設定了動畫效果。
➔ Storyboard中的DoubleAnimation表明,StrokeThickness這個值會在半秒鐘之內,從100變為70。DoubleAnimation中的“Double”代表了目標屬性的類型(因為StrokeThickness類型是double)。
➔ 因為AutoReverse被設定為true,所以,在StrokeThickness達到70時,會自動從70變為100。由於RepeatBehavior被設定為Forever,所以只要應用程式啟動以後,這種重複的動作就會一直持續。
➔ EasingFunction屬性(設定為ElasticEase的執行個體)控制著StrokeThickness值是如何在時間軸上進行改寫的。這部分的內容會在下面的“Interpolation”節介紹。
開始動畫中,storyboard的Begin方法調用如下:
this.PupilStoryboard.Begin();
這個動畫在整個應用程式中表現出來的效果12.2所示。Pupil這個橢圓的元素在背後代碼中被賦為亮藍色的畫刷。
圖12.2 PupilStoryboard使得Pupil的stroke(藍色部分)厚度從100縮小到70,因而使得黑色填充變大。
在XAML中,有一種方法來觸發storyboard的所有行為。因此,我們沒有必要在背後代碼中調用它的Begin方法。我們可以為一個元素的Triggers屬性添加一個事件觸發程式。幸虧這個特殊的BeginStoryboard元素,在grid的Loaded事件中,它會內部調用storyboard 的Begin方法。而Loaded事件是Silverlight事件觸發中支援的唯一一個事件。
Types of Animations
Silverlight 提供了animation類來實現四種不同的資料類型:double、Color、Point和Object。在Silly Eye應用程式中,只使用了double屬性。剩餘的類型在第15章“Mood Ring”中介紹。如果我們想要在時間軸上改變元素中double類型的屬性值(比如Width、Height、Opacity、Canvas.Left等等),我們可以使用DoubleAnimation的執行個體。如果我們想要在時間軸上改變元素中Point類型的屬性值(比如一個線性漸層brush的StartPoint和EndPoint屬性),我們可以使用PointAnimation的執行個體。目前為止,由於大量的double類型的屬性需要動畫效果,所以DoubleAnimation是最常用的動畫類。
並不是所有的屬性都可以做動畫效果,即使它的資料類型與動畫類使用的資料類型匹配!
本章內容所討論的動畫機制,只有那些被稱為“依賴項屬性”(dependency property)的類型才能夠使用。幸運的是,大部分視覺元素中的屬性就是該類型的。依賴項屬性的有關內容會在第18章“Cocktails”中討論。與判斷一個事件是否是“路由事件”(routed event)類似,我們可以通過檢查類中所包含的一個名為PropertyNameProperty的DependencyProperty類型的靜態欄位來決定該屬性是否是依賴項屬性。如果該類中包含了這種欄位,如ellipse類中的StrokeThicknessProperty欄位,那麼它就是一個依賴項屬性。
為了達到理想的效果,思考一個元素的哪些屬性來做動畫,這需要通過一些實驗來決定。例如,如果我們想要一個元素以漸層的方式出現,那麼,對它的Visibility屬性做動畫就沒有任何意義,因為在Collapsed 和 Visible兩者之間沒有中間值。相反,我們應該對它的Opacity屬性做動畫,它的值是double類型的,範圍從0到1。
Interpolation
認識下面這點很重要:在預設情況下,DoubleAnimation通過時間軸上的線性插值來達到平緩地改變double類型的屬性值。換句話說,在一秒鐘之內,如果該值從50增加到100,那麼,在0.1秒的時刻,它的值就是55(時間增加10%,其值也線性增加10%),在0.5秒的時刻,它的值就是75(時間增加50%,其值也線性增加50%),以此類推。這也就是圖12.2中,StrokeThickness屬性的中間值是85的原因。
但是,Windows Phone應用程式中使用的大部分動畫是非線性。而且,他們更傾向以突然加速或者突然減速的方式從一個值改變到另一個值。這種方式使得動畫變得妙趣橫生。我們可以通過應用一個緩和的函數來實現這種非線性動畫效果。
這種過渡函數負責屬性值從起始到最終值之間的自訂插值。Pupil Storyboard使用了名為ElasticEase的函數來實現這種行為。圖12.3展示了屬性值從100減小到70時,使用預設的線性變換和彈性變換之間的差異。在這種情況下,85這個中間值並不是在中間時間點達到,它其實更接近於終點時才達到。
圖12.3 ElasticEase過渡函數極大地改變了double類型值從100減小到70的方式。
Silverlight提供了11個不同的過渡函數,每個函數有三種不同的模式,有些函數提供了更深層次的屬性行為自訂。例如,ElasticEase具有Oscillations 和 Springiness 屬性,預設設定為3。在實際應用中,如果我們想要在動畫中加入自訂的函數,那麼這種自訂行為的可能性是無窮無盡的。與預設的線性過渡相比,本應用中使用的過渡函數為使用者提供了一種完全不同的體驗。
附錄D“Animation Easing Reference”中展示了每個內建過渡函數的行為。我覺得這些函數非常有用,因為每次當我想要設計一個新的動畫時,我都會回去參考這些函數。
另一種產生非線性動畫的方法
過渡函數並非是產生非線性過渡動畫效果的唯一方法。第14章“Love Meter”中討論的Keyframe animations,使得我們可以利用不同的差值方法,將一個動畫分解為多個片段。
The Iris Storyboard
Silly Eye應用程式將以下Storyboard應用到了一個名為Iris的canvas控制項中,使得眼球看上去在左右移動。其注意點如下:
➔ TargetProperty的文法與其名稱相比,稍顯複雜。當它設定為一個可附加的屬性(如Canvas.Left)時,它必須被包含在括弧內。
➔ 該動畫使用了一個不同的過渡函數,使得其運動的邊界更加明顯。關於BounceEase的行為,請參考附錄D。
➔ 該動畫缺少了From值!這是可行的,而且我們推薦這樣處理。當From值沒有指定時,動畫就從目標屬性的當前值開始,而不管該值大小為多少。同樣,一個動畫可以指定From值,但並不指定To值!這樣的話,動畫就從屬性的指定值開始,到當前值為止。
為了達到平緩的效果,將From設定為預設是很重要的,特別是動畫開始於一個可重複的使用者輸入。比如,在使用者點擊一個介面元素時,開始它增長的動畫,那麼,每次快速的點擊會使得其大小迅速變回From設定的值。但是,通過預設的From值設定,隨後的點擊也會使動畫從當前值開始,使得動畫更加的平滑和自然。
在目標屬性值無法插值的情況下,我們必須指定From 和 To 的值!
假如我們嘗試著為一個auto-sized元素的寬度或者高度做動畫效果,而它的From和To沒有指定,那麼,動畫效果就不會出現。當元素的寬度或者高度被設定為Double.NaN(非數值)時,它的大小是自適應的。因為當兩個值中存在一個非數值的數時,DoubleAnimation也就無法完成插值的操作。而且,將動畫應用到ActualWidth或者ActualHeight中去(它們被設定為真實的寬度或高度值,而非NaN),這並不是一個好的選擇。因為這些屬性是唯讀,而且並不是依賴項屬性。相反,為了有動畫效果,我們必須顯式地設定目標元素的寬度/高度。
對於Pupil Storyboard來說,我們必須調用Storyboard的Begin方法來使得它開始工作。
動畫的效果12.4所示。Iris Canvas 除了包含 Pupil ellipse(事實上,它的外圍就是Iris)以外,還包括另外兩個Ellipse,它們為Iris增添了“光澤”。因為Canvas控制項的位置加入了動畫效果,所以其包含的所有視覺元素都會一起移動。
圖12.4 IrisStoryboard水平移動Iris Canvas,其值從287(它初始的Canvas.Left值)增加到543。
動畫效果類中,也有一個名為 By 的欄位,它可以用來代替 To 欄位。下面的動畫意味著“在當前屬性值的基礎上,增加256”:
<DoubleAnimation By=”256” Duration=”0:0:2”/>
對於縮減當前值時,出現負值的情況也是支援的。
The Eyelid Animation
Silly Eye應用程式中使用的最後一個storyboard,用來對具有皮膚顏色的Eyelid Ellipse模仿眨眼的效果。對於Pupil Ellipse來說,皮膚顏色的畫刷在背後的代碼中進行設定。其注意點如下:
➔ Storyboard.TargetName 和 Storyboard.TargetProperty 這兩個屬性被設定為attachable的原因是:它們可以在單獨的動畫中使用,而不用去理會任何Storyboard中的設定。該Storyboard以 Eyelid Ellipse 中的 Height 和 Canvas.Top 這兩個屬性為目標。因此,單個目標的名字被標記在了Storyboard上,但是多個不同的目標屬性被用來標記多個不同的動畫效果。
➔ Canvas.Top 與 Height 具有同步的動畫效果,所以橢圓在垂直縮小的同時,保持在中間的位置。下一章介紹的“Transforms”,提供了一種更加快捷的方法來實現這個效果。
➔ 這兩種動畫都使用了預設的線性插值方法。它們的移動速度如此之快,以至於沒有必要再去嘗試別的更具生命力的方法。
➔ Storyboard不僅僅是一個簡單的用來給相關的物體實現動畫效果的容器。該Storyboard具有自身的持續行為和重複行為!這兩種動畫只持續了0.2秒鐘的時間(0.1秒之內將屬性的當前值從380減小到50,另外的0.1秒鐘之內,由於其auto-reverse設定,將屬性值變回到原來的值)。但是,因為給Storyboard的期間是3秒鐘,而且它自身具有auto-reverse設定(並非是它的子項目),動畫在後面的時間裡面保持不動,知道最後3秒時間消耗完。然後,持續0.2秒鐘的動作再次開始,之後便是2.8秒鐘的靜止時間。因此,該Storyboard使得眼睛眨得非常快,且在3秒鐘發生一次。
這種動畫的效果12.5所示(在C#中調用 Begin 方法開始動畫)。因為Eyelid Ellipse與背景的顏色一致(有意在其左邊的相鄰地區用黑色做填充),我們就無法看到Eyelid Ellipse本身。相反,一旦Ellipse的高度(380)小於其填充厚度的兩倍(400),我們可以看到其內部的空間急劇縮小到0。
圖12.5 Eyelid Storyboard 壓縮 Eyelid Ellipse的高度,並向下移動,使其保持置中。
Storyboard and Animation Properties
我們已經瞭解了 Duration、AutoReverse 和 RepeatBehavior 屬性,它們可以應用到單獨的動畫或者一個完整的Storyboard中去。總的來說,有6種屬性可以應用到 Storyboard 和Animation 中去:
➔ Duration:Animation 或者 Storyboard 的長度,預設值為1秒。
要謹慎指定duration的值!
一個 duration 的字串格式與 TimeSpan.Parse 這個介面函數的格式相同:
days.hours:minutes:seconds.fraction
這裡允許使用捷徑,所以我們不用指定每個字串。但是,它的行為可能不是如你所願。字元“2”表示2天,而非2秒。字串“2.5”表示2天零5個小時!字串“0:2”表示2分鐘。如果大多數動畫都不能超過幾秒鐘的時限,那麼,典型的使用格式就是 hours:minutes:seconds 或者 hours:minutes:seconds.fraction。所以,2秒可以表示為“0:0:2”,半秒可以表示為“0:0:0.5”或者“0:0:.5”。
➔ BeginTime:動畫或者Storyboard延時開始的時間長度,由一個具體的時間值表示,預設為0。對於子項目的動畫,Storyboard可以使用自訂的BeginTime值,使得它們可以相繼開始動畫,而非同時開始。
➔ SpeedRatio:期間(Duration)的乘子,預設為1。我們可以將它設定為任意的大於0的double類型的值。一個小於1的值能夠減緩動畫的速度,一個大於1的值能夠加速動畫的速度。SpeedRatio不會影響BeginTime。
➔ AutoReverse:該屬性設定為True時,使得動畫或者Storyboard達到終點以後,實現自動回播。回播花費同樣長度的時間,所以SpeedRatio也會影響回播。需要注意的是,通過BeginTime指定的延時並不會影響回播。回播一般會在正常的動畫結束以後,立即啟動。
➔ RepeatBehavior:可以設定為一個時間段,或者設定為一個字串,例如“2x”、“3x”或者“Forever”。因此,我們可以使用RepeatBehavior,使得動畫的期間減短(或者減少他們的期間),或者使得動畫自動重複多次(甚至可以是一個帶小數的倍數,如2.5倍),或者是永遠重複動畫(本章就是使用這個方法)。如果AutoReverse設定為True,那麼,回播操作也會重複。
➔ FillBehavior:可以設定為 Stop,而它的預設值為 HoldEnd,使得相關的動畫完成以後,其屬性值恢複到動畫之前的值。
The Total Length of an Animation
通過使用諸如BeginTime、SpeedRatio、AutoReverse和RepeatBehavior屬性,可以對動畫做多方面的調整,在動畫開始以後,測試其持續總時間的長度是有難度的。它的Duration值當然不足以描述真正的時間長度!相反,以下的公式描述了一個動畫真正的期間:
總時間= BeginTime +(Duration *(AutoReverse ? 2:1)* RepeatBehavior)/ SpeedRatio
這可以在RepeatBehavior屬性指定為一個double類型的值時使用(或者使用它的預設值1)。如果RepeatBehavior設定為一個時間段,那麼總的時間長度就是RepeatBehavior的值加上BeginTime的值。
The Main Page
Silly Eye首頁面的XAML包含了一些向量圖片,一個應用程式欄,以及三個Storyboard。它同樣包含了一個“使用說明頁面”,暗示使用者點擊螢幕開始應用,12.6所示。因此,我們一開始可以展示應用程式欄,但是應用程式開始運行時,它就隱藏了,因為螢幕上顯示的按鈕會妨礙應用程式的效果。介紹頁面暗示使用者他們可以通過點擊螢幕,在任何時候達到重新調出應用程式欄的目的。
圖12.6 應用程式欄只有在“介紹頁面”出現使可見
➔ 應用程式欄包含了導向設定頁面、說明頁面和關於頁面的連結。前兩個頁面會在下面兩節中介紹。我們已經在第6章“Baby Sign Language”中學習了關於頁面。我們認為,設定頁面的連結作為按鈕放置在應用程式欄,要好於一個功能表項目,因為在本應用程式中,使用者對設定進行自訂也是一件很正常的事情(在應用程式的正常操作過程中,應用程式欄不會引入視覺上的混亂,因為它是隱藏的!)。
➔ 注意,三個Storyboard資源的名稱被命名為“x:Name”,而不是“x:Key”!這是一種方便的手段,使得我們可以更加方便地使用背後的代碼。在我們給資源命名以後,它就可以作為字典中的一個鍵來使用,或者作為C#產生的一個欄位。
➔ 顯式的From值已經從Pupil Storyboard的動畫中移除了,因為它並不是必須的。這部分內容已經在本章進行了介紹,它有助於理解動畫是如何工作的。
➔ IntroTextBlock元素用來監聽使用者的點擊,並且隱藏IntroPanel。它的寬度值為700,比整個頁面的寬度還要大,因為如果它距離應用程式欄太近的話,使用者在想點擊應用程式欄時,可能不小心點到它(然後就會隱藏應用程式欄)。
The Code-Behind
➔ 由於XAML中的x:Name標記,通過各自的名稱,三個Storyboard在建構函式中初始化。
➔ 頁面的Clip屬性被設定為一個螢幕大小的矩形地區。這樣做是為了在動畫頁面切換期間,防止對螢幕以外的向量圖形進行渲染。這不僅避免了出現非常奇怪的視覺元素,而且也有助於提高應用程式的效能。所有具有這個Clip屬性的UI元素可以被設定為一個任意的幾何圖形。
Geometries Used for Clipping
Clip屬性可以被設定為一些幾何形狀,這些形狀與第5章“Ruler”中介紹的形狀對象類似,但又有不同。我們可以使用 RectangleGeometry、 EllipseGeometry 、LineGeometry、PathGeometry 或者是 GeometryGroup 來組合多個幾何形狀。附錄E“Geometry Reference”中將會討論這些幾何形狀。
➔ 我們使用兩個儲存設定來儲存皮膚和眼睛顏色的值,它們在OnNavigatedTo事件處理中使用。在OnNavigatedFrom事件處理中,它們沒有必要進行儲存,因為設定頁面會進行處理。這些設定的內容在單獨的Settings.cs檔案中定義。事實上,眼睛預設的顏色就是手機的主題強調色,就和本章圖片中展示的藍色一樣。
➔ IntroPanel的可視性(以及應用程式欄)放置於頁面的狀態中,所以如果頁面在休眠和啟用以後,看上去是一致的。無論頁面經曆了多大的改變,也不要忘記使用版面設定,有了它,在應用程式經曆被打斷、又重新啟用時,我們可以快速並且自動地恢複頁面狀態。
➔ IntroTextBlock的對齊在OnOrientationChanged事件中進行調整,這樣做是為了保持它與應用程式欄保持對立的位置關係。前面已經敘述過,在手機的方向為landscape right時,應用程式欄出現在螢幕的左邊;當手機的方向為landscape left時,應用程式欄出現在螢幕的右邊。
The Settings Page
設定頁面12.7所示,它使得使用者可以為眼睛和皮膚選擇不同的顏色。
圖12.7 設定頁面使得使用者可以選擇Silly Eye應用程式的顏色。
在系統內建的設定程式中,如何為我們的應用程式添加一個設定頁面?
在目前Windows Phone 7.0的版本中,我們還無法做到這點。雖然設定應用程式套件含了一個系統設定的列表和一個應用設定的列表,但是後者只是針對系統內建的應用來說的。相反,我們需要為我們的應用程式添加設定頁面,使其使用者體驗和系統內建的設定頁面一致。換句話說,對於我們的設定頁面,應該使用“SETTINGS”作為應用程式的名稱出現在標準的header中,使用應用程式的名稱作為頁面標題,12.7所示。
對於設定頁面的設計指導,請參考第20章“Alarm Clock”中的內容。
頁面設計的注意點如下:
➔ 該頁面的自訂header樣式從App.xaml檔案獲得。對於本書中剩餘的應用程式來說,App.xaml.cs這個檔案同樣提供了自訂的頁面過渡效果,如第19章“Animation Lab”所述。
➔ 那兩個可點擊的地區顯示了當前的顏色,看上去和按鈕很像,但實際上它們只不過是矩形填充。它們的MouseLeftButtonUp事件處理包含了使用者對於每種介面顏色改變的處理。
➔ 雖然在不同的方向模式下,內容完全符合螢幕,但是主要的stack panel控制項被放置在scroll viewer內。這對於使用者來說,很適合觸摸操作,因為使用者可以用手指拖動螢幕查看內容,並使他們確信瀏覽了螢幕中所有的內容。
在很多頁面中,例如設定頁面、說明頁面或者是關於頁面,將內容放置於scroll viewer中是一個很好的選擇,即使所有的內容可以用一個螢幕來容納。那樣的話,使用者可以用手指做一個快速的拖動,從視覺上就可以判定沒有更多的內容可以看了(因為scroll viewer控制項的scroll-and-squish特性)。這種反饋是非常另使用者滿意的。當螢幕對於使用者的拖動沒有反應時,使用者會認為他不夠用力,可能會再嘗試一次。
The Code-Behind
為了使使用者能夠改變每個顏色,該頁面會將使用者導航到一個顏色選擇頁面,12.8所示。這個特性頁面被本書中的很多應用程式共用,包含在本書的原始碼中。它提供了一個標準顏色的調色盤,它也允許使用者自訂色彩的色相、飽和度和亮度,不管是通過互動介面或者是輸入一個十六進位的數值(或者是任何能夠被XAML解析的字串,如“red”、“tan”或者是“lemonchiffon”)。調整顏色的不透明度是可選的。
圖12.8 顏色選取器頁面為使用者提供了一個漂亮的頁面來選擇顏色。
顏色選取器頁面通過查詢字串可以接受以下四種參數:
➔ showOpacity-預設值為True,可以將其設定為False來隱藏opacity slider。它也會將調色盤頂層的透明顏色移除,並且阻止使用者輸入透光的顏色。因此,當我們將它設定為False時,我們可以確定一個不透明的顏色將會被選中。
➔ currentColor-當頁面呈現時,開始被選擇的顏色。它必須作為一個對XAML有效字串參數傳入。如果指定為一個十六進位的數,“#”必須被移除,這樣做是為了與URI混淆。
➔ defaultColor-在顏色選取器頁面中,使用者點擊reset按鈕時,可以得到的顏色。它指定的字串格式要求與currentColor一樣。
➔ settingName-隔離儲存空間中使用的名稱,在從頁面返回時,選定的顏色可以從中被找到。在構造一個Setting的執行個體時,用到了同樣的名字。在列表12.4的OnNavigatedTo方法中,當從顏色選取器頁面返回時,它自動選擇新的顏色數值,那隻是因為導航到顏色選擇頁面之前,需要調用ForceRefresh方法。第20章詳細介紹了這種方法。
使用本書的顏色選取器頁面(或者類似的頁面)為使用者提供一種簡便高效的顏色選取方法。在當前的版本中,該頁面的主要缺點是只支援portrait的螢幕方向。因此,在landscape螢幕方向下,使用硬體鍵盤輸入一個顏色的十六進位數的使用者體驗並不好。
The Instructions Page
本應用程式的說明頁面12.9所示,其注意點如下:
圖12.9 Silly Eye應用程式中的說明頁面
➔ 和設定頁面採用的方法一樣,主要的內容被放置在scroll viewer中,從而提示使用者沒有更多的內容可以瀏覽。
➔ 和首頁面中的intro pane一樣,單個text block利用LineBreak元素來格式化其常值內容。
➔ 對於背後的代碼檔案-InstructionsPage.xaml.cs,在其建構函式中,只包含了對InitializeComponent方法的調用。