本篇的最終目的,是模擬系統的照片APP可以左右滑動,縮放圖片的操作。在實現的過程中,我們會逐步分析UWP編寫UI的一些思路和技巧。
首先我們先實現一個橫向的可以瀏覽圖片的功能,也是大部分APP中的實現。最簡單的方式是使用FlipView,再將FlipView的ItemTemplate設置成Image。大體代碼如下:
<FlipView ItemsSource="{Binding Photos,Mode=OneTime}"> <FlipView.ItemTemplate> <DataTemplate> <Image Source="{Binding ImageUri,Mode=OneTime}"></Image> </DataTemplate> </FlipView.ItemTemplate> </FlipView>
上述代碼很簡單,同時效果也非常好。問題圖片如果縱橫比例較大,比如長微博那種豎長的圖片在手機上就沒法方便地閱讀了。這時候我們需要能夠縮放和拖動圖片,對圖片的局部進行觀察。請注意這是一個強需求!特別是打開一張柳岩照片卻尷尬地發現無法縮放時的強需求!
分析一下我們遇到的問題,需要支持手勢對圖片的縮放和移動。UWP里一般通過UIElement類型的Manipulation相關事件來處理。接下來我們來創建一個支持手勢的控件。
一開始的想法是繼承Image來實現一個支持縮放的ScalableImage,但不幸的是Image類是不允許繼承的sealed類型。那我們索性搞大一點,實現一個ScalableGrid,該Grid允許將內部的元素通過Manipulation進行操作。
public class ScalableGrid : Grid { private TransformGroup transformGroup; private ScaleTransform scaleTransform; private TranslateTransform translateTransform; public ScalableGrid() { this.scaleTransform = new ScaleTransform(); this.translateTransform = new TranslateTransform(); this.transformGroup = new TransformGroup(); this.transformGroup.Children.Add(scaleTransform); this.transformGroup.Children.Add(translateTransform); this.RenderTransform = transformGroup; this.ManipulationMode = ManipulationModes.System | ManipulationModes.Scale; this.ManipulationDelta += ScalableGrid_ManipulationDelta; this.Loaded += ScalableGrid_Loaded; this.SizeChanged += (a, b) => { this.scaleTransform.CenterX = this.ActualWidth / 2; this.scaleTransform.CenterY = this.ActualHeight / 2; }; this.DoubleTapped += ScalableGrid_DoubleTapped; } private void ScalableGrid_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { scaleTransform.ScaleX = scaleTransform.ScaleY = 1; this.translateTransform.X = 0; this.translateTransform.Y = 0; this.ManipulationMode = ManipulationModes.System | ManipulationModes.Scale; } private void ScalableGrid_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { this.Loaded -= ScalableGrid_Loaded; scaleTransform.CenterX = this.ActualWidth / 2; scaleTransform.CenterY = this.ActualHeight / 2; } private void ScalableGrid_ManipulationDelta(object sender, Windows.UI.Xaml.Input.ManipulationDeltaRoutedEventArgs e) { if (scaleTransform.ScaleX == 1 && scaleTransform.ScaleY == 1) { this.ManipulationMode = ManipulationModes.System | ManipulationModes.Scale; } else { this.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY | ManipulationModes.Scale | ManipulationModes.TranslateInertia; } scaleTransform.ScaleX *= e.Delta.Scale; scaleTransform.ScaleY *= e.Delta.Scale; if (scaleTransform.ScaleY < 1) { scaleTransform.ScaleX = scaleTransform.ScaleY = 1; } translateTransform.X += e.Delta.Translation.X; translateTransform.Y += e.Delta.Translation.Y; StopWhenTranslateToEdge(); }
TranslateTransform和ScaleTransform分別對應平移操作和縮放操作。
this.ManipulationMode = ManipulationModes.System | ManipulationModes.Scale;
ManipulationMode在構造函數中,初始設置支持System和Scale,沒有TranslateX和TranslateY是因為初始打開的時候不希望可以有平移操作,只有縮放后,才根據放大的具體情況放開對平移的支持。
this.SizeChanged += (a, b) => { this.scaleTransform.CenterX = this.ActualWidth / 2; this.scaleTransform.CenterY = this.ActualHeight / 2; };
SizeChanged事件是為了在窗口大小變化,比如桌面縮放窗口或手機橫豎屏切換時,重新定位縮放的中心點。
private void ScalableGrid_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { scaleTransform.ScaleX = scaleTransform.ScaleY = 1; this.translateTransform.X = 0; this.translateTransform.Y = 0; this.ManipulationMode = ManipulationModes.System | ManipulationModes.Scale; }
DoubleTapped事件是為了雙擊還原到初始狀態。
對手勢的支持代碼是在private void ScalableGrid_ManipulationDelta(object sender, Windows.UI.Xaml.Input.ManipulationDeltaRoutedEventArgs e)方法中。其中判斷Scale大於1,也就是放大后才支持平移操作。同時去除System枚舉,這是因為不希望對圖片的平移被判斷為滑動FlipView控件,導致切換Image。
StopWhenTranslateToEdge()方法是希望避免將圖片滑出屏幕邊緣導致無法繼續操作。
將完成的ScalableGrid放置到FlipView的ItemTemplate中:
<FlipView ItemsSource="{Binding Photos,Mode=OneTime}"> <FlipView.ItemTemplate> <DataTemplate> <local:ScalableGrid> <Image Source="{Binding ImageUri,Mode=OneTime}"></Image> </local:ScalableGrid> </DataTemplate> </FlipView.ItemTemplate> </FlipView>
至此,一個滑動查看圖片的功能算是完成了。我們可以左右切換圖片,對FilpView的某一張圖片進行縮放和平移的操作,閱讀長微博也不是問題。
那是不是完美無缺了呢?變態的用戶們會發現,我們在放大圖片后,如果當前的圖片沒有撐滿整個FilpViewItem,通過在空白處滑動屏幕,可以切換到另一張圖片。雖然也不是什么大問題,但是用戶老爺會不爽,那如何解決呢?我們祭出神器Live Visual Tree,來檢查一下到底是誰無視當前的ManipulationMode,硬是將手勢事件傳遞給了FilpView。
從截圖中的Visual Tree可以看出,選中ScalableGrid時,Gird實際是撐滿整個FilpViewItem的,也就是說ScalableGrid在非圖片區域不作為,不僅沒有截獲處理內部的Manipulation事件,反而直接冒泡傳遞給了上層FilpViewItem。
原先我的猜測是ScalableGird無法撐滿FlipViewItem,Manipulation事件不經過ScalableGrid。這種情況我需要在ScalableGrid外層再套一個Panel或Border遮蓋整個FlipViewItem的面積,然后綁定二者的ManipulationMode。
實際情況比想象的還要簡單,我只需要設置ScalableGird的Background屬性為Transparent即可。最終的XAML如下:
<FlipView ItemsSource="{Binding Photos,Mode=OneTime}"> <FlipView.ItemTemplate> <DataTemplate> <local:ScalableGrid Background="Transparent"> <Image Source="{Binding ImageUri,Mode=OneTime}" Stretch="None"></Image> </local:ScalableGrid> </DataTemplate> </FlipView.ItemTemplate> </FlipView>
好了,可以用你的Lumia 950XL或者Surface Pro 4來試一試了,沒有的話趕緊去買,最近大降價了,你值得擁有。另外StopWhenTranslateToEdge的算法實現得不是很好,期待評論中有好的思路,最好能不依賴外部UIElement。
GitHub:
https://github.com/manupstairs/UWPSamples/tree/master/UWPSamples/PhotosBrowser