Windows Phone 7 - Image加上Zoom in/out功能
最近不小心迷上用手機看漫畫這件事,所以想說寫一個自己的漫畫程式,這樣可以選擇自己覺得
取得圖像資料比較快且穩定的網站。不過在撰寫過程裡,對於Image的處理卻是最為困難的地方。
不過還好,在網路上找到了這一篇資料<Windows Phone 7: correct pinch zoom in Silverlight>,裡面
針對圖像的處理提供了最完整的解釋,往下針對該篇文章提到的部分,加以說明。
〉定義XAML中Image的呈現樣式:
1: <Grid x:Name="LayoutRoot" Background="Transparent">
2: <Image x:Name="ImgZoom"
3: Source="sample.jpg"
4: Stretch="UniformToFill"
5: RenderTransformOrigin="0,0">
6: <toolkit:GestureService.GestureListener>
7: <!-- 定義Pinch系列的事件,DoubleTap回到原圖,Drag圖像移動-->
8: <toolkit:GestureListener
9: PinchStarted="OnPinchStarted"
10: PinchDelta="OnPinchDelta"
11: DragDelta="OnDragDelta"
12: DoubleTap="OnDoubleTap"/>
13: </toolkit:GestureService.GestureListener>
14: <Image.RenderTransform>
15: <!-- 定義CompositeTransform支援Scale與Translate的特效 -->
16: <CompositeTransform
17: ScaleX="1" ScaleY="1"
18: TranslateX="0" TranslateY="0"/>
19: </Image.RenderTransform>
20: </Image>
21: </Grid>
如果針對GestureService不是非常熟悉的地方,可以參考<Windows Phone 7 - 淺談手勢(Gestures)運作>的說明。
上述程式主要針對Pinch、Drag與DoubleTap三種手勢進行事件的定義,並且實作了二種RenderTransform支援
Image圖像內容的Scale、Translate二種特效。
‧Image:
在Image控件裡幾個屬性,在使用RenderTransform時需要注意的,如下:
屬性 | 說明 |
ScaleX |
屬於ScaleTransform的重要屬性。 控制物件的Scale X比例(横向擴大),一種二維的X-Y坐標系統。 |
SacleY |
屬於ScaleTransform的重要屬性。 控制物件的Scale Y比例(直向擴大),一種二維的X-Y坐標系統。 |
TranslateX |
屬於TranslateTransform的重要屬性。 控制物件的TranslatesX比例(横向移動),一種二維的X-Y坐標系統。 |
TranslateY |
屬於TranslateTransform的重要屬性。 控制物件的TranslatesX比例(直向移動),一種二維的X-Y坐標系統。 |
在處理RenderTransform裡還有包括:RotateTransform、SkewTransform、TranslateTransform、MatrixTransform等,
這些Transform可透過TransformGroup組合起來一同管理,讓套用的物件可同時具有這些效果。
定義好了圖像的基本效果與XAML後,接下來便來看看,實際如何處理:Pinch、Drag與DoubleTap的手勢事件;
〉處理Pinch事件(PinchStarted與PinchDelta):
Pinch代表運用二個手指按在螢幕上,進行二指間的拉近與拉遠移動所觸發的事件。
在<Windows Phone 7 - 淺談手勢(Gestures)運作>提到二個事件的定義,如下:
PinchStarted:代表Pinched事件的開始,即是二個手指剛觸控於螢幕時;
PinchDelta:代表Pinched事件執行的過程,即時二個手指間距移動時觸發的事件;
(1) 在PinchStarted記下目前圖像控件的二個手指點擊的位置與設定Scale比例為1;
1: /// <summary>
2: /// Initializes the zooming operation
3: /// </summary>
4: private void OnPinchStarted(object sender, PinchStartedGestureEventArgs e)
5: {
6: //取得目前二個手指在圖像控件的座置,並且設定目前Scale比率為1
7: _oldFinger1 = e.GetPosition(ImgZoom, 0);
8: _oldFinger2 = e.GetPosition(ImgZoom, 1);
9:
10: //設定為1,代表以目前圖像的狀態進行Scale的調整
11: _oldScaleFactor = 1;
12: }
(2) 在PinchDelta進行一系列移動時的圖像Scale與座標移動的計算;
1: /// <summary>
2: /// Computes the scaling and translation to correctly zoom around your fingers.
3: /// </summary>
4: private void OnPinchDelta(object sender, PinchGestureEventArgs e)
5: {
6: //取得移動的距離比率 / 目前的Scale比率,取得對應的Scale比率
7: var scaleFactor = e.DistanceRatio / _oldScaleFactor;
8: //檢查新的Scale比率是否合法
9: if (!IsScaleValid(scaleFactor))
10: return;
11:
12: //取得新的二個手指座標
13: var currentFinger1 = e.GetPosition(ImgZoom, 0);
14: var currentFinger2 = e.GetPosition(ImgZoom, 1);
15:
16: //根據目前的二手指座標、上一個二手指座標、圖像的座標、新Scale比率,重新建立一個Point
17: var translationDelta = GetTranslationDelta(
18: currentFinger1,
19: currentFinger2,
20: _oldFinger1,
21: _oldFinger2,
22: ImagePosition,
23: scaleFactor);
24:
25: //更新儲存現在的手指座標與Scale比率
26: _oldFinger1 = currentFinger1;
27: _oldFinger2 = currentFinger2;
28: _oldScaleFactor = e.DistanceRatio;
29:
30: //更新圖像Scale比率與圖像座標
31: UpdateImageScale(scaleFactor);
32: UpdateImagePosition(translationDelta);
33: }
透過在該PinchDelta擷取到的(1) 移動距離搭配目前的Scale比率,計算出的Scale比率;並且因為比率的改變,也要讓
(2) 圖像放大的位置是二指間的圖像區域,因此再透過二指間的座標與Scale比率等參數,計算新的座標來配合Scale
的變動;
在上述的程式裡有二個事件參數,它們均繼承GestureEventArgs加上了Pinch的部分:
‧PinchStartedGestureEventArgs:定義了二個觸控點在啟動時需要擷取到的基本資料;
屬性/方法 | 說明 |
GetPosition | 回傳在相對應UIElement中二個觸控點(0或1)任一個的座標位置。 |
Distance | 二個觸控點之間的距離。 |
‧PinchGestureEventArgs:定義在Pinch過程中相對於距離、角度的處理值;
屬性/方法 | 說明 |
DistanceRatio | 回傳接觸點之間的當前距離/原來的接觸點之間的距離的比值。 |
TotalAngleDelta | 回傳當前的觸摸位置和原始觸摸位置之間的角度差。 |
〉處理Drag事件:
Drag負責一個手指按在螢幕上,移動時連著帶動有監聽Drag事件的物件跟著移動。此程式增加Drag的機制讓圖像
可以跟著Drag的新座標進行移動的效果。如下程式:
1: /// <summary>
2: /// Moves the image around following your finger.
3: /// </summary>
4: private void OnDragDelta(object sender, DragDeltaGestureEventArgs e)
5: {
6: //取得移動的新座標
7: var translationDelta = new Point(e.HorizontalChange, e.VerticalChange);
8:
9: //驗證是否超過Scale後可移動範圍、目前圖像移動與實體大小的差距
10: if (IsDragValid(1, translationDelta))
11: UpdateImagePosition(translationDelta);
12: }
〉處理DoubleTab事件:
DoubleTab事件發生連續快速二次Tap(輕敲)動作於UIElement。此程式增加了在圖像上進行DoubleTab之後,將圖像復原
成原來的樣子。如下程式:
1: /// <summary>
2: /// Resets the image scaling and position
3: /// </summary>
4: private void OnDoubleTap(object sender, Microsoft.Phone.Controls.GestureEventArgs e)
5: {
6: TotalImageScale = 1;
7: ImagePosition = new Point(0, 0);
8: //呼叫ApplyScale()與ApplyPosition()根據TotalImageScale與ImagePosition
9: //復原圖像的原始比例
10: ApplyScale();
11: ApplyPosition();
12: }
〉相關的副程式:
(1) 取得隨著Scale與座標改變圖像的新座標:GetTranslationDelta();
1: /// <summary>
2: /// Computes the translation needed to keep the image centered between your fingers.
3: /// </summary>
4: private Point GetTranslationDelta(
5: Point currentFinger1, Point currentFinger2,
6: Point oldFinger1, Point oldFinger2,
7: Point currentPosition, double scaleFactor)
8: {
9: var newPos1 = new Point(
10: currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
11: currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);
12:
13: var newPos2 = new Point(
14: currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
15: currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);
16:
17: var newPos = new Point(
18: (newPos1.X + newPos2.X) / 2,
19: (newPos1.Y + newPos2.Y) / 2);
20:
21: return new Point(
22: newPos.X - currentPosition.X,
23: newPos.Y - currentPosition.Y);
24: }
(2) 執行圖像Scale與Position更換的方法:ApplyScale()與ApplyPosition();
依賴編輯好的公用變數:TotalImageScale與ImagePosition,進行圖像控件的調整;
1: /// <summary>
2: /// Applies the computed position to the image control.
3: /// </summary>
4: private void ApplyPosition()
5: {
6: ((CompositeTransform)ImgZoom.RenderTransform).TranslateX = ImagePosition.X;
7: ((CompositeTransform)ImgZoom.RenderTransform).TranslateY = ImagePosition.Y;
8: }
9:
10: /// <summary>
11: /// Applies the computed scale to the image control.
12: /// </summary>
13: private void ApplyScale()
14: {
15: ((CompositeTransform)ImgZoom.RenderTransform).ScaleX = TotalImageScale;
16: ((CompositeTransform)ImgZoom.RenderTransform).ScaleY = TotalImageScale;
17: }
上述中CompositeTransform.ScaleX/ScaleY、CompositeTransform.TranslateX/TranslateY,該CompositeTransform是什麼呢?
‧CompositeTransform:提供一個整合的對象,讓開發人員可以直接設定物件對應的Transform對象;
(3) 識別新的Scale與Translate是否符合圖像的限制:IsScaleValid()與IsDragValid();
1: /// <summary>
2: /// Checks that dragging by the given amount won't result in empty space around the image
3: /// </summary>
4: private bool IsDragValid(double scaleDelta, Point translateDelta)
5: {
6: if (ImagePosition.X + translateDelta.X > 0 || ImagePosition.Y + translateDelta.Y > 0)
7: return false;
8:
9: if ((ImgZoom.ActualWidth * TotalImageScale * scaleDelta) + (ImagePosition.X + translateDelta.X) < ImgZoom.ActualWidth)
10: return false;
11:
12: if ((ImgZoom.ActualHeight * TotalImageScale * scaleDelta) + (ImagePosition.Y + translateDelta.Y) < ImgZoom.ActualHeight)
13: return false;
14:
15: return true;
16: }
17:
18: /// <summary>
19: /// Tells if the scaling is inside the desired range
20: /// </summary>
21: private bool IsScaleValid(double scaleDelta)
22: {
23: //比對 "總Image Scale * 移動新的Scale 是否有大於1 ,而且
24: //比對 "總Image Scale * 移動新的Scale 是否有大於1 ,小於等於最大Scale比率
25: return (TotalImageScale * scaleDelta >= 1) && (TotalImageScale * scaleDelta <= MAX_IMAGE_ZOOM);
26: }
(4) 搭配PinchDelta()與DragDelta()的方式,進行計算並且變動Scale與Position:UpdateImageScale()與UpateImagePosition();
例如:PinchDelta()改變Scale也配合調整了Position;DragDelta()改變Position;
1: /// <summary>
2: /// Updates the scaling factor by multiplying the delta.
3: /// </summary>
4: private void UpdateImageScale(double scaleFactor)
5: {
6: TotalImageScale *= scaleFactor;
7: ApplyScale();
8: }
9:
10: /// <summary>
11: /// Updates the image position by applying the delta.
12: /// Checks that the image does not leave empty space around its edges.
13: /// </summary>
14: private void UpdateImagePosition(Point delta)
15: {
16: var newPosition = new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);
17:
18: if (newPosition.X > 0) newPosition.X = 0;
19: if (newPosition.Y > 0) newPosition.Y = 0;
20:
21: if ((ImgZoom.ActualWidth * TotalImageScale) + newPosition.X < ImgZoom.ActualWidth)
22: newPosition.X = ImgZoom.ActualWidth - (ImgZoom.ActualWidth * TotalImageScale);
23:
24: if ((ImgZoom.ActualHeight * TotalImageScale) + newPosition.Y < ImgZoom.ActualHeight)
25: newPosition.Y = ImgZoom.ActualHeight - (ImgZoom.ActualHeight * TotalImageScale);
26:
27: ImagePosition = newPosition;
28:
29: ApplyPosition();
30: }
[範例程式]
[執行結果] (注意,圖像來源非本App與作者所有)
======
此篇文章主要針對<Windows Phone 7: correct pinch zoom in Silverlight>中我覺得必需要懂得的地方,加以說明。
需要實際的Sample Code可至該出處進行下載。由於對於圖像的處理、座標的控制不是非常熟悉,因此,希望
撰寫該篇做為筆記之外,也能幫助跟我一樣對座標與Scale處理不是非常熟悉的。如果有撰寫錯誤,也請大家
給予指導。謝謝。
References:
‧How to zoom in and zoom out Images in WP7?
‧MultiTouch Behavior for Windows Phone 7
‧MultiTouch Behavior: Update for Windows Phone 7 tools beta
‧Windows Phone 7: correct pinch zoom in Silverlight
‧Touch Gestures on Windows Phone (原理,必讀)
‧WP7 and the Phone Controls Toolkit
‧WP7 Pinch and Pan Zoom an Image
‧Windows Phone, Silverlight and WPF Multi-Touch Manipulations
‧风云的银光志Silverlight4.0教程之使用CompositeTransform复合变形特效实现倒影