UIView.transform的高級(jí)玩法

最近在重構(gòu)之前上架的一款畫(huà)板應(yīng)用,期間用到了一些UIView的transform相關(guān)的特性。借此機(jī)會(huì)也系統(tǒng)整理了一下transform相關(guān)的知識(shí)。
在進(jìn)入正題之前需要補(bǔ)充一點(diǎn)線性代數(shù)(數(shù)學(xué)專業(yè)應(yīng)該叫高等代數(shù))相關(guān)的知識(shí)。

齊次坐標(biāo)系

所謂齊次坐標(biāo)系就是將一個(gè)原本是n維的向量用一個(gè)n+1維向量來(lái)表示。對(duì)于一個(gè)向量v以及基oabc,可以找到一組坐標(biāo)(v1,v2,v3)使得v=v1a+v2b+v3c(1-1)。而對(duì)于一個(gè)點(diǎn)p,則可以找到一組坐標(biāo)(p1,p2,p3)使得p - o = p1a + p2b + p3c(1-2)
從上面對(duì)向量和點(diǎn)的表達(dá),我們可以看出為了在坐標(biāo)系中表示一個(gè)點(diǎn)我們可以把點(diǎn)的位置看作是對(duì)于這個(gè)基的原點(diǎn)o所進(jìn)行的一個(gè)位移,即一個(gè)向量p - o,我們?cè)诒磉_(dá)這個(gè)向量的同時(shí)用等價(jià)的方式表達(dá)出了點(diǎn)p: p = o + p1a + p2b + p3c(1-3)。(1-1),(1-3)是坐標(biāo)系下表達(dá)一個(gè)向量和點(diǎn)的不同表達(dá)方式。這里可以看出,雖然都是用代數(shù)分量的形式表達(dá)向量和點(diǎn),但表達(dá)一個(gè)點(diǎn)比一個(gè)向量需要額外的信息。如果我寫(xiě)一個(gè)代數(shù)分量表達(dá)(1,4,7),誰(shuí)知道它是個(gè)向量還是一個(gè)點(diǎn)。我們現(xiàn)在把(1-1),(1-3)寫(xiě)成矩陣的形式:

1-4
1-5

這里(a b c o)是坐標(biāo)基矩陣,左邊的行向量分別是向量v和點(diǎn)p在基下的坐標(biāo)。這樣,向量和點(diǎn)再同一個(gè)基下就有了不同的表達(dá):三維向量的第四個(gè)代數(shù)分量是0,而三維點(diǎn)的第四個(gè)代數(shù)分量是1。像這種用四個(gè)代數(shù)分量表示三維幾何概念的方式是一種齊次坐標(biāo)表示。這樣,上面的(1,4,7)如果寫(xiě)成(1,4,7,0),它就是個(gè)向量;如果是(1,4,7,1)它就是個(gè)點(diǎn)。
由于齊次坐標(biāo)使用了4個(gè)分量來(lái)表達(dá)3D概念或者說(shuō)用了3個(gè)分量來(lái)表達(dá)2D概念,從而使得放射變換可以使用矩陣進(jìn)行。

平面幾何變換的定義

如果有一種法則T,對(duì)平面點(diǎn)集中的每個(gè)點(diǎn)A,都對(duì)應(yīng)平面上唯一的一個(gè)點(diǎn)T(A),則T稱為平面上的一個(gè)變換,T(A)稱為A的像。變換是函數(shù)概念的自然推廣。
平面上的圖形由點(diǎn)組成,因而平面上的變換T會(huì)將一個(gè)圖形C變到另一個(gè)圖形T(C),T(C)稱為C的像。從這個(gè)意義上說(shuō),可以稱T為幾何變換。例如對(duì)圖形作平移變換、旋轉(zhuǎn)變換、縮放變換、對(duì)稱變換等都是幾何變換。
在平面直角坐標(biāo)系中,點(diǎn)A由坐標(biāo)(x,y)表示。在變換T下,點(diǎn)A(x,y)的像為A'(x',y'),其中x'和y'都是x,y的函數(shù):
x' = f1(x,y), y' = f2(x,y)
因此,函數(shù)f1,f2能夠確定一個(gè)平面上的變換T。如果能夠從方程組中反解出x和y:
x = g1(x', y'), y = g2(x', y')
則由函數(shù)g1,g2確定了T的逆變換,記為T(mén)-1。設(shè)平面曲線C的參數(shù)方程為:
x = x(t), y = y(t), t∈D
其中D是函數(shù)x(t),y(t)的定義域,則曲線C在變換T下的像T(C)的參數(shù)方程為
x = f1(x(t),y(t)), y = f2(x(t), y(t)), t∈D

平面幾何變換及其矩陣表示

平面圖形幾何變換

1、平移變換

平移變換是將圖形中的每一個(gè)點(diǎn)從一個(gè)位置(x,y)移動(dòng)到另一個(gè)位置(x',y')的變換,tx,ty稱為平移距離,則平移變換公式為:

平移變換

2、旋轉(zhuǎn)變換

旋轉(zhuǎn)變換是以某個(gè)參考點(diǎn)為圓心,將圖像上的各點(diǎn)(x,y)圍繞圓心轉(zhuǎn)動(dòng)一個(gè)逆時(shí)針角度θ,變?yōu)樾碌淖鴺?biāo)(x',y')的變換。當(dāng)參考點(diǎn)為(0,0)時(shí),旋轉(zhuǎn)變換的公式為:

由于:

所以可化簡(jiǎn)為:

旋轉(zhuǎn)變換

3、比例變換

比例變換是使對(duì)象按比例因子(sx,sy)放大或縮小的變換

比例變換

平面圖形幾何變換的矩陣表示

從變換功能上可以把T2D分為四個(gè)子矩陣。其中

是對(duì)圖形的縮放、旋轉(zhuǎn)、對(duì)稱、錯(cuò)切等變換;

是對(duì)圖形進(jìn)行平移變換;

是對(duì)圖形作投影變換,g的作用是在x軸的1/g處產(chǎn)生一個(gè)滅點(diǎn),而h的作用是在y軸的1/h處產(chǎn)生一個(gè)滅點(diǎn);i是對(duì)整個(gè)圖形做伸縮變換。平移變換、旋轉(zhuǎn)變換、比例變換、錯(cuò)切變換這4中基本變換都可以表示為3x3的變換矩陣和齊次坐標(biāo)相乘的形式

1、平移變換的矩陣表示

平移變換的矩陣表示為

tx,ty分別表示x軸方向和y軸方向的平移距離。

2、旋轉(zhuǎn)變換矩陣表示

旋轉(zhuǎn)變換的矩陣表示為

逆時(shí)針旋轉(zhuǎn)時(shí)θ取正值,順時(shí)針旋轉(zhuǎn)時(shí)θ取負(fù)值

3、比例變換的矩陣表示

比例變換的矩陣表示為

  • 當(dāng)b=d=0時(shí),a和e的取值決定了縮放效果,a和e>1放大,<1縮小
  • 當(dāng)b=d=0,a=-1,e=1時(shí)有x'=-x,y'=y產(chǎn)生與y軸對(duì)稱的圖形
  • 當(dāng)b=d=0,a=1,e=-1時(shí)有x'=x,y'=-y產(chǎn)生與x軸對(duì)稱的圖形
  • 當(dāng)b=d=0,a=e=-1時(shí)有x'=-x,y'=-y產(chǎn)生與原點(diǎn)對(duì)稱的圖形
  • 當(dāng)b=d=1,a=e=0時(shí)有x'=y,y'=x產(chǎn)生與直線y=x對(duì)稱的圖形
  • 當(dāng)b=d=-1,a=e=0時(shí)有x'=-y,y'=-x產(chǎn)生與直線y=-x對(duì)稱的圖形

4、錯(cuò)切變換的矩陣表示

錯(cuò)切變換的矩陣表示為

其中當(dāng)d = 0時(shí),x' = x + by, y' = y,此時(shí),圖形的y坐標(biāo)不變,x坐標(biāo)隨初值(x, y)及變換系數(shù)b作線性變化;當(dāng)b = 0時(shí),x' = x,y' = dx + y,此時(shí),圖形的x坐標(biāo)不變,y坐標(biāo)隨初值(x, y)及變換系數(shù)d作線性變化。

5、復(fù)合變換

一個(gè)比較復(fù)雜的變換要連續(xù)進(jìn)行若干個(gè)基本變換才能完成。例如圍繞任意點(diǎn)(xf, yf)的旋轉(zhuǎn),需要通過(guò)3個(gè)基本變換T(xf, yf),R(θ),T(xf, yf)才能完成。這些由基本變換構(gòu)成的連續(xù)變換序列稱為復(fù)合變換。
變換的矩陣形式使得復(fù)合變換的計(jì)算工作量大為減少。以繞任意點(diǎn)旋轉(zhuǎn)為例,本應(yīng)進(jìn)行如下3次變換,分別是

  • p' = pT(-xf, -yf) 將原點(diǎn)移動(dòng)到任意點(diǎn)位置
  • p'' = p'R(θ) 旋轉(zhuǎn)
  • p = p''T(xf, yf) 將原點(diǎn)歸位

合并之后為p = pT(-xf, -yf)R(θ)T(xf, yf)
令Tc = T(-xf, -yf)R(θ)T(xf, yf)則有p = pTc,Tc稱為復(fù)合變換矩陣。由上面推到可知在計(jì)算復(fù)合變換時(shí),首先可將各基本變換矩陣按次序想乘,形成總的復(fù)合變換矩陣Tc然后,坐標(biāo)只需與Tc想乘一次,便可同時(shí)完成一連串基本變換。因此采用復(fù)合變換矩陣可以大大節(jié)省坐標(biāo)乘法所耗費(fèi)的運(yùn)算時(shí)間。下面我們看幾個(gè)基本的復(fù)合變換:
復(fù)合平移:
對(duì)同一圖形做兩次平移相當(dāng)于將兩次平移相加起來(lái),即

復(fù)合縮放:
以原點(diǎn)為參考點(diǎn)對(duì)同一圖形做兩次連續(xù)的縮放相當(dāng)于將縮放操作相乘,即:

復(fù)合旋轉(zhuǎn):
以原點(diǎn)為參考點(diǎn)對(duì)同一圖形做兩次連續(xù)的旋轉(zhuǎn)相當(dāng)于將兩次的旋轉(zhuǎn)角度相加, 即:

縮放、旋轉(zhuǎn)變換都與參考點(diǎn)有關(guān),上面進(jìn)行的各種縮放、旋轉(zhuǎn)變換都是以原點(diǎn)為參考點(diǎn)的。如果相對(duì)某個(gè)一般的參考點(diǎn)(xf,yf)作縮放、旋轉(zhuǎn)變換,相當(dāng)于將該點(diǎn)移到坐標(biāo)原點(diǎn)處,然后進(jìn)行縮放、旋轉(zhuǎn)變換,最后將(xf,yf)點(diǎn)移回原來(lái)的位置。如關(guān)于(xf,yf)的縮放變換為:

各種復(fù)雜的變換無(wú)非是一些基本變換的組合,利用數(shù)學(xué)方法也就是矩陣的 乘法來(lái)解決復(fù)合變換問(wèn)題,關(guān)鍵是將其分解為一定順序的基本變換,然后逐一 進(jìn)行這些基本變換;或者求出這些基本變換矩陣連乘積,即求出復(fù)合變換矩陣, 從而使復(fù)合變化問(wèn)題得到解決。

寫(xiě)了這么多只是想把平面仿射變換的基本原理描述清楚,以便能對(duì)UIView.transform有更深入的理解。
接下來(lái)我們進(jìn)入正題

UIView外部坐標(biāo)系

這里說(shuō)的坐標(biāo)系是UIView相對(duì)于其父視圖的相對(duì)位置和大小

UIView外部坐標(biāo)系

如上圖以父視圖左上角為坐標(biāo)原點(diǎn),x軸從原點(diǎn)向右遞增,y軸從原點(diǎn)向下遞增,通過(guò)改變UIView的frame和center可以調(diào)整UIView的位置和大小,當(dāng)然UIView是對(duì)CALayer的封裝也可以直接調(diào)整layer的frame和position達(dá)到相同的效果。
基于此我們可以調(diào)整UIView的位置和大小,或者通過(guò)UIView的位置和大小進(jìn)行適當(dāng)?shù)膭?dòng)畫(huà)展示,當(dāng)然也僅限于此,對(duì)于旋轉(zhuǎn)、切變是無(wú)能為力的。

  • 設(shè)置View的frame和center會(huì)改變其位置和大小,同時(shí)會(huì)改變View的bounds,bounds是View相對(duì)于自身的尺寸bounds=(0,0,view.width,view.height)
  • 設(shè)置完成frame或者center之后可以通過(guò)調(diào)整bounds重新設(shè)置frame,如果frame = (x,y,w,h) 重新設(shè)置bounds = (0,0,w',h')則新的frame=(x',y',w',h')
  • 當(dāng)然如果在設(shè)置完bounds之后再設(shè)置frame則bounds會(huì)被重置為(0,0,view.width,view.height)

UIView內(nèi)部坐標(biāo)系

UIView除了剛剛我們說(shuō)的外部坐標(biāo)系,還有一個(gè)內(nèi)部坐標(biāo)系。

UIView內(nèi)部坐標(biāo)系

跟笛卡爾坐標(biāo)系(直角坐標(biāo)系)稍微有點(diǎn)區(qū)別,以UIView視圖中心為坐標(biāo)原點(diǎn),x軸從原點(diǎn)向右遞增,y軸從原點(diǎn)向下遞增,通過(guò)改變UIView的transform可以對(duì)其進(jìn)行仿射變換,如上面我們提到的縮放、旋轉(zhuǎn)、平移、切變等。有了這個(gè)特性UIView能做的事情就更多了,當(dāng)然也可以借此做更有意思的動(dòng)畫(huà)。
在內(nèi)部坐標(biāo)系中原點(diǎn)的位置可以通過(guò)anchorPoint調(diào)整,UIView沒(méi)有開(kāi)放出來(lái),可以訪問(wèn)CALayer獲取。

anchorPoint

參考上圖通過(guò)調(diào)整anchorPoint的值可以修改內(nèi)部坐標(biāo)系的原點(diǎn)位置,設(shè)置(0,0)可以把原點(diǎn)移動(dòng)到View的左上角,設(shè)置(1,1)可以把原點(diǎn)移動(dòng)到右下角,設(shè)置(0.5, 0.5)可以把原點(diǎn)移動(dòng)到View中心。當(dāng)然anchorPoint的值也不限制在[0,1],可以推廣到任意浮點(diǎn)值,相應(yīng)的調(diào)整規(guī)則類似,比如設(shè)置為(-1,-1)則可以把原點(diǎn)移動(dòng)到左上角再向左上偏移一個(gè)View的位置。
anchorPoint值的修改不只會(huì)調(diào)整原點(diǎn)位置,同時(shí)也會(huì)修改View的frame,修改規(guī)則如下:

基于View的transform可以進(jìn)行仿射變換,所有的變化都是基于原點(diǎn)位置進(jìn)行的,因此anchorPoint的設(shè)置可以產(chǎn)生更多有意思的效果,
后續(xù)我們一個(gè)個(gè)看

跟anchorPoint的設(shè)置一樣,transform的設(shè)置也會(huì)引起frame的調(diào)整

Transform修改

見(jiàn)上圖以旋轉(zhuǎn)變換為例,旋轉(zhuǎn)變換會(huì)讓原有圖形的frame從白色框變?yōu)樘摼€框,我們假設(shè)原有View的四個(gè)點(diǎn)為p0 p1 p2 p3 則旋轉(zhuǎn)變換之后的點(diǎn)為:
p0' = p0T(θ)
p1' = p1T(θ)
p2' = p2T(θ)
p3' = p3T(θ)
則frame = (x',y',w',h')

UIView內(nèi)部坐標(biāo)系和外部坐標(biāo)系的聯(lián)系

我們把上面提到的兩個(gè)坐標(biāo)系結(jié)合起來(lái)看一下

內(nèi)外坐標(biāo)系

影響View位置和形狀的幾個(gè)參數(shù)有:

  • frame
  • center
  • transform
  • bounds
  • anchorPoint

遵循如下規(guī)則:

  • 在設(shè)置transform之前可以通過(guò)frame和center調(diào)整View的大小和尺寸,frame的改變會(huì)影響bounds,設(shè)置bounds會(huì)重新修改frame和center,規(guī)則參考之前
  • View的transform參考內(nèi)部坐標(biāo)系,transform的改變會(huì)影響frame和center,但是不會(huì)修改bounds
  • 在設(shè)置了transform修改之后仍然可以通過(guò)調(diào)整bounds來(lái)修改frame和center也可以直接修改center,transform會(huì)根據(jù)新的bounds和center來(lái)計(jì)算新的frame,參考之前
  • anchorPoint的修改會(huì)影響transform的原點(diǎn)位置從而產(chǎn)生不同的變換效果,也會(huì)引起frame的重新計(jì)算

UIView.transform的高級(jí)玩法

上面的理論知識(shí)已經(jīng)寫(xiě)了很多了,接下來(lái)我們實(shí)際體驗(yàn)一下,看一下View的transform結(jié)構(gòu)

struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};

結(jié)合上面關(guān)于線性代數(shù)相關(guān)的知識(shí),可以發(fā)現(xiàn)View的transform最終都轉(zhuǎn)換成了矩陣運(yùn)算

UIView的復(fù)合變換

UIView *view = [UIView new];
view.backgroundColor = [UIColor redColor];
view.frame = CGRectMake(200, 200, 100, 100);
[self.view addSubview:view];
[UIView animateWithDuration:5 animations:^{
    // 先平移
    CGAffineTransform move = CGAffineTransformMakeTranslation(100, 100);
    // 后旋轉(zhuǎn)
    CGAffineTransform rotation = CGAffineTransformMakeRotation(M_PI);
    view.transform = CGAffineTransformConcat(rotation, move);
}];
先平移后旋轉(zhuǎn)

先不解釋,我們接著再看一個(gè)變換

UIView *view = [UIView new];
view.backgroundColor = [UIColor redColor];
view.frame = CGRectMake(200, 200, 100, 100);
[self.view addSubview:view];
[UIView animateWithDuration:5 animations:^{
    // 先旋轉(zhuǎn)
    CGAffineTransform rotation = CGAffineTransformMakeRotation(M_PI);
    // 后平移
    CGAffineTransform move = CGAffineTransformMakeTranslation(100, 100);
    view.transform = CGAffineTransformConcat(move,rotation);
}];
先旋轉(zhuǎn)后平移

綜合上面兩個(gè)不同順序的變換,由于View內(nèi)部坐標(biāo)系的原點(diǎn)在復(fù)合變換的過(guò)程中一直跟隨View在移動(dòng)因此平移和旋轉(zhuǎn)的順序會(huì)決定不同的結(jié)果。

  • 如果原點(diǎn)在整個(gè)變換過(guò)程中一直不變,則需要先旋轉(zhuǎn)后平移
  • 如果原點(diǎn)在整個(gè)變換過(guò)程中一直跟隨View,則需要先平移后旋轉(zhuǎn)

目的就是保證旋轉(zhuǎn)始終是圍繞原點(diǎn)進(jìn)行

AnchorPoint

如果不修改AnchorPoint則所有的變化都是基于View的中心進(jìn)行,但是可以通過(guò)修改anchorPoint改變?cè)c(diǎn)的位置從而改變變換的效果

UIView *view = [UIView new];
view.backgroundColor = [UIColor redColor];
view.frame = CGRectMake(200, 200, 100, 100);
[self.view addSubview:view];
view.layer.anchorPoint = CGPointMake(0, 0);
[UIView animateWithDuration:5 animations:^{
    view.transform = CGAffineTransformMakeRotation(M_PI);
}];
繞點(diǎn)旋轉(zhuǎn)

如上圖可以實(shí)現(xiàn)繞點(diǎn)旋轉(zhuǎn)的效果

綜合應(yīng)用

借用一個(gè)案例來(lái)對(duì)transform做一個(gè)綜合的應(yīng)用,這個(gè)案例也是從實(shí)際項(xiàng)目中產(chǎn)生的。先看最終效果:

綜合應(yīng)用

最近在用一些零散的時(shí)間重構(gòu)之前上架的一款畫(huà)板應(yīng)用,希望為畫(huà)布增加更加靈活的操作方式,在雙指拖拽畫(huà)布的同時(shí)可以實(shí)現(xiàn)定點(diǎn)的縮放和旋轉(zhuǎn),可以通過(guò)雙指點(diǎn)擊完成筆跡的撤銷,通過(guò)三指點(diǎn)擊完成筆跡的重做。

把問(wèn)題拆解一下,為了達(dá)到上面展示的效果,需要解決以下問(wèn)題:

  • 手勢(shì)的控制,雙指拖拽,雙指捏合,雙指旋轉(zhuǎn)
  • 處理各手勢(shì)之間的沖突和配合
  • 處理View的平移、旋轉(zhuǎn)、縮放復(fù)合變換
  • 其中旋轉(zhuǎn)和縮放變換要以雙指連線的中點(diǎn)為旋轉(zhuǎn)或縮放中心

手勢(shì)控制

綜合分析以上問(wèn)題首先需要為畫(huà)布增加一個(gè)容器,然后才能在容器上添加手勢(shì),通過(guò)手勢(shì)控制畫(huà)布的frame和transform

/// 畫(huà)布
var canvasView: UIView? = nil {
    didSet {
        if self.canvasView != nil {
            self.addSubview(self.canvasView!);
            self.canvasView?.backgroundColor = UIColor.white;
            // 移動(dòng)到容器中心
            self.canvasView!.center = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2);
            // transform歸零,設(shè)置為單位矩陣
            self.canvasView!.transform = CGAffineTransform.identity;
        }
    }
}

添加需要的手勢(shì)

// 雙指點(diǎn)擊
let doubleTouchesGesture = UITapGestureRecognizer(target: self, action: #selector(gestureRecognizer(gesture:)));
doubleTouchesGesture.numberOfTapsRequired = 1;
doubleTouchesGesture.numberOfTouchesRequired = 2;
doubleTouchesGesture.delegate = self;
self.addGestureRecognizer(doubleTouchesGesture);

// 三指點(diǎn)擊
let tripleTouchesGesture = UITapGestureRecognizer(target: self, action: #selector(gestureRecognizer(gesture:)));
tripleTouchesGesture.numberOfTapsRequired = 1;
tripleTouchesGesture.numberOfTouchesRequired = 3;
tripleTouchesGesture.delegate = self;
self.addGestureRecognizer(tripleTouchesGesture);

// 縮放
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(gestureRecognizer(gesture:)));
pinchGesture.delegate = self;
self.addGestureRecognizer(pinchGesture);

// 移動(dòng)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognizer(gesture:)));
panGesture.minimumNumberOfTouches = 2;
panGesture.delegate = self;
self.addGestureRecognizer(panGesture);

// 旋轉(zhuǎn)
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(gestureRecognizer(gesture:)));
rotationGesture.delegate = self;
self.addGestureRecognizer(rotationGesture)

我們需要旋轉(zhuǎn)、移動(dòng)和縮放同時(shí)觸發(fā)并且在觸發(fā)旋轉(zhuǎn)、移動(dòng)或者縮放的時(shí)候雙指點(diǎn)擊不能被觸發(fā),但是如果用戶使用三指點(diǎn)擊時(shí),三指手勢(shì)要優(yōu)先觸發(fā)。因此需要對(duì)手勢(shì)的delegate做一點(diǎn)處理

// MARK: - UIGestureRecognizerDelegate
extension CanvasContentView: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // 各手勢(shì)之間要并發(fā)進(jìn)行
        return true;
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer || gestureRecognizer is UIPinchGestureRecognizer) && otherGestureRecognizer is UITapGestureRecognizer {
            // 移動(dòng)、旋轉(zhuǎn)、縮放時(shí)要避免雙指點(diǎn)擊觸發(fā)
            if otherGestureRecognizer.numberOfTouches == 3 {
                // 三指點(diǎn)擊時(shí)用戶意圖明顯,因此要優(yōu)先觸發(fā)
                return false;
            }
            return true;
        }
        return false;
    }
}

這樣各種手勢(shì)就可以相互配達(dá)到我們的需求

繞固定點(diǎn)的旋轉(zhuǎn)

繞固定點(diǎn)旋轉(zhuǎn)

如上圖,如果是畫(huà)布繞其中心旋轉(zhuǎn)是很容易實(shí)現(xiàn)的,不需要調(diào)整View原點(diǎn)位置直接旋轉(zhuǎn)θ角度即可。如果旋轉(zhuǎn)點(diǎn)不在畫(huà)布中心處理起來(lái)就要麻煩一點(diǎn)。有兩種方案可以實(shí)現(xiàn)

  • 1、調(diào)整anchorPoint把View坐標(biāo)原點(diǎn)移動(dòng)到旋轉(zhuǎn)點(diǎn)位置,然后通過(guò)transform設(shè)置讓View旋轉(zhuǎn)θ
  • 2、拆解繞點(diǎn)旋轉(zhuǎn)變換為:先把View中心移動(dòng)到目標(biāo)位置,然后旋轉(zhuǎn)θ角度

分析一下看一下哪種方案更合適,如果調(diào)整anchorPoint必然會(huì)引起frame的改變,也就是center位置的變化,需要在anchorPoint調(diào)整之后恢復(fù)center的位置,另外如果View在初始狀態(tài)是比較容易通過(guò)旋轉(zhuǎn)中心點(diǎn)的坐標(biāo)推算出anchorPoint的新位置,但是一旦View發(fā)生了旋轉(zhuǎn)就很難再計(jì)算出新的anchorPoint的位置。而方案2只需要計(jì)算出旋轉(zhuǎn)過(guò)程中View中心點(diǎn)的位置變化即可。
根據(jù)之前的理論知識(shí)坐標(biāo)系中的一個(gè)點(diǎn)繞另一個(gè)點(diǎn)的旋轉(zhuǎn)變換可以表示為:

化簡(jiǎn)之后為:

看一下部分代碼實(shí)現(xiàn):

private func rotateAt(center: CGPoint, rotation: CGFloat) {
    self.gestureParams.rotation = self.gestureParams.rotation + rotation;
    // x = (x1 - x0)cosθ - (y1 - y0)sinθ + x0
    // y = (y1 - y0)cosθ + (x1 - x0)sinθ + y0
    let x1 = self.canvasView!.center.x;
    let y1 = self.canvasView!.center.y;
    let x0 = center.x;
    let y0 = self.bounds.size.height - center.y;
    let x = (x1 - x0) * cos(rotation) - (y1 - y0) * sin(rotation) + x0
    let y = (y1 - y0) * cos(rotation) + (x1 - x0) * sin(rotation) + y0;
    
    self.canvasView!.center = CGPoint(x: x, y: y);
    self.canvasView!.transform =  CGAffineTransform.identity.rotated(by: self.gestureParams.rotation).scaledBy(x: self.gestureParams.scale, y: self.gestureParams.scale);
}

以固定點(diǎn)為中心縮放

以固定點(diǎn)為中心縮放

跟旋轉(zhuǎn)類似以固定點(diǎn)為中心的縮放依然可以選擇兩種方案,我們依然以選擇第二中方案,先把中心點(diǎn)移動(dòng)到目標(biāo)位置然后進(jìn)行縮放
變換矩陣表示為:

化簡(jiǎn)為:


看一下部分代碼

private func scaleAt(center: CGPoint, scale: CGFloat) {
    // x' = Sx(x - x0) + x0
    // y' = Sy(y - y0) + y0
    let formerScale = self.gestureParams.scale;
    self.gestureParams.scale = scale * self.gestureParams.scale;
    self.gestureParams.scale = min(max(self.minScale, self.gestureParams.scale), self.maxScale);
    let currentScale = self.gestureParams.scale/formerScale;
    
    let x = self.canvasView!.center.x;
    let y = self.canvasView!.center.y;
    let x1 = currentScale * (x - center.x) + center.x;
    let y1 = currentScale * (y - center.y) + center.y;
    self.canvasView!.center = CGPoint(x: x1, y: y1);
    self.canvasView!.transform =  CGAffineTransform.identity.rotated(by: self.gestureParams.rotation).scaledBy(x: self.gestureParams.scale, y: self.gestureParams.scale);
}

手勢(shì)信息收集和轉(zhuǎn)換

最主要的問(wèn)題其實(shí)都已經(jīng)解決掉了,接下來(lái)就是把手勢(shì)信息轉(zhuǎn)換為我們需要的數(shù)據(jù)即可,這里不做過(guò)多的解釋了,直接貼代碼:

// MARK: - Gestures
extension CanvasContentView {
    @objc func gestureRecognizer(gesture: UIGestureRecognizer) {
        if self.canvasView != nil {
            switch gesture {
            case is UIPinchGestureRecognizer:
                let pinchGesture = gesture as! UIPinchGestureRecognizer;
                if pinchGesture.state == .began || pinchGesture.state == .changed {
                    // 計(jì)算縮放的中心點(diǎn)和縮放比例,每次縮放的比例需要累計(jì)
                    var center = pinchGesture.location(in: self);
                    if pinchGesture.numberOfTouches == 2 {
                        let center0 = pinchGesture.location(ofTouch: 0, in: self);
                        let center1 = pinchGesture.location(ofTouch: 1, in: self);
                        center = CGPoint(x: (center0.x + center1.x)/2, y: (center0.y + center1.y)/2);
                    }
                    self.scaleAt(center: center, scale: pinchGesture.scale);
                    pinchGesture.scale = 1;
                    self.delegate?.canvasContentView(self, scale: self.gestureParams.scale);
                }
                break;
            case is UIPanGestureRecognizer:
                let panGesture = gesture as! UIPanGestureRecognizer;
                let location = panGesture.location(in: self);
                if  panGesture.state == .began {
                    // 記錄開(kāi)始位置
                    self.gestureParams.from = location;
                    self.gestureParams.lastTouchs = gesture.numberOfTouches;
                }else if panGesture.state == .changed {
                    if self.gestureParams.lastTouchs != panGesture.numberOfTouches {
                        self.gestureParams.from = location;
                    }
                    // 計(jì)算偏移量
                    self.gestureParams.lastTouchs = panGesture.numberOfTouches;
                    let x = location.x - self.gestureParams.from.x;
                    let y = location.y - self.gestureParams.from.y;
                    self.gestureParams.from = location;
                    self.translate(x: x, y: y);
                    self.delegate?.canvasContentView(self, x: x, y: y);
                }
                break;
            case is UIRotationGestureRecognizer:
                let rotatioGesture = gesture as! UIRotationGestureRecognizer;
                if rotatioGesture.state == .began || rotatioGesture.state == .changed {
                    // 計(jì)算旋轉(zhuǎn)的中心點(diǎn)和旋轉(zhuǎn)角度,每次旋轉(zhuǎn)的角度需要累計(jì)
                    var center = rotatioGesture.location(in: self);
                    if rotatioGesture.numberOfTouches == 2 {
                        let center0 = rotatioGesture.location(ofTouch: 0, in: self);
                        let center1 = rotatioGesture.location(ofTouch: 1, in: self);
                        center = CGPoint(x: (center0.x + center1.x)/2, y: (center0.y + center1.y)/2);
                    }
                    self.rotateAt(center: center, rotation: rotatioGesture.rotation);
                    rotatioGesture.rotation = 0;
                    self.delegate?.canvasContentView(self, rotation: self.gestureParams.rotation);
                }
                break;
            case is UITapGestureRecognizer:
                let tapGesture = gesture as! UITapGestureRecognizer;
                if tapGesture.numberOfTouches == 2 {
                    self.delegate?.canvasContentView(self, tapTouches: 2);
                }else if tapGesture.numberOfTouches == 3 {
                    self.delegate?.canvasContentView(self, tapTouches: 3);
                }
                break;
            default:
                break;
            }
        }
    }
}

完整代碼

https://github.com/fuxiaoghost/CanvasContentView

寫(xiě)了很多,總結(jié)一句,UIView在二維狀態(tài)下的形變多數(shù)情況都可以轉(zhuǎn)換為仿射變換或者多個(gè)仿射變換的復(fù)合變換,從而用矩陣運(yùn)算的知識(shí)解決。以后再遇到比較有意思的問(wèn)題我會(huì)繼續(xù)補(bǔ)充……

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容