UIView.transform的高級玩法

最近在重構(gòu)之前上架的一款畫板應(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維向量來表示烟阐。對于一個(gè)向量v以及基oabc忍燥,可以找到一組坐標(biāo)(v1,v2,v3)使得v=v1a+v2b+v3c(1-1)拧晕。而對于一個(gè)點(diǎn)p,則可以找到一組坐標(biāo)(p1,p2,p3)使得p - o = p1a + p2b + p3c(1-2)
從上面對向量和點(diǎn)的表達(dá)梅垄,我們可以看出為了在坐標(biāo)系中表示一個(gè)點(diǎn)我們可以把點(diǎn)的位置看作是對于這個(gè)基的原點(diǎn)o所進(jìn)行的一個(gè)位移厂捞,即一個(gè)向量p - o,我們在表達(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è)向量需要額外的信息貌虾。如果我寫一個(gè)代數(shù)分量表達(dá)(1,4,7)吞加,誰知道它是個(gè)向量還是一個(gè)點(diǎn)。我們現(xiàn)在把(1-1),(1-3)寫成矩陣的形式:

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)如果寫成(1,4,7,0)码党,它就是個(gè)向量德崭;如果是(1,4,7,1)它就是個(gè)點(diǎn)。
由于齊次坐標(biāo)使用了4個(gè)分量來表達(dá)3D概念或者說用了3個(gè)分量來表達(dá)2D概念揖盘,從而使得放射變換可以使用矩陣進(jìn)行眉厨。

平面幾何變換的定義

如果有一種法則T,對平面點(diǎn)集中的每個(gè)點(diǎn)A兽狭,都對應(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è)意義上說,可以稱T為幾何變換伐庭。例如對圖形作平移變換座享、旋轉(zhuǎn)變換、縮放變換似忧、對稱變換等都是幾何變換渣叛。
在平面直角坐標(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-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)變換的公式為:

由于:

所以可化簡為:

旋轉(zhuǎn)變換

3功咒、比例變換

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

比例變換

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

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

是對圖形的縮放绞蹦、旋轉(zhuǎn)、對稱榜旦、錯(cuò)切等變換坦辟;

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

是對圖形作投影變換章办,g的作用是在x軸的1/g處產(chǎn)生一個(gè)滅點(diǎn)锉走,而h的作用是在y軸的1/h處產(chǎn)生一個(gè)滅點(diǎn);i是對整個(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軸對稱的圖形
  • 當(dāng)b=d=0,a=1,e=-1時(shí)有x'=x,y'=-y產(chǎn)生與x軸對稱的圖形
  • 當(dāng)b=d=0,a=e=-1時(shí)有x'=-x,y'=-y產(chǎn)生與原點(diǎn)對稱的圖形
  • 當(dāng)b=d=1,a=e=0時(shí)有x'=y,y'=x產(chǎn)生與直線y=x對稱的圖形
  • 當(dāng)b=d=-1,a=e=0時(shí)有x'=-y,y'=-x產(chǎn)生與直線y=-x對稱的圖形

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),需要通過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ù)合平移:
對同一圖形做兩次平移相當(dāng)于將兩次平移相加起來,即

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

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

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

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

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

UIView外部坐標(biāo)系

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

UIView外部坐標(biāo)系

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

  • 設(shè)置View的frame和center會(huì)改變其位置和大小层宫,同時(shí)會(huì)改變View的bounds,bounds是View相對于自身的尺寸bounds=(0,0,view.width,view.height)
  • 設(shè)置完成frame或者center之后可以通過調(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除了剛剛我們說的外部坐標(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)向下遞增锌历,通過改變UIView的transform可以對其進(jìn)行仿射變換贮庞,如上面我們提到的縮放、旋轉(zhuǎn)究西、平移贸伐、切變等。有了這個(gè)特性UIView能做的事情就更多了怔揩,當(dāng)然也可以借此做更有意思的動(dòng)畫捉邢。
在內(nèi)部坐標(biāo)系中原點(diǎn)的位置可以通過anchorPoint調(diào)整,UIView沒有開放出來商膊,可以訪問CALayer獲取伏伐。

anchorPoint

參考上圖通過調(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修改

見上圖以旋轉(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é)合起來看一下

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

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

  • frame
  • center
  • transform
  • bounds
  • anchorPoint

遵循如下規(guī)則:

  • 在設(shè)置transform之前可以通過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修改之后仍然可以通過調(diào)整bounds來修改frame和center也可以直接修改center妆距,transform會(huì)根據(jù)新的bounds和center來計(jì)算新的frame,參考之前
  • anchorPoint的修改會(huì)影響transform的原點(diǎn)位置從而產(chǎn)生不同的變換效果蛹含,也會(huì)引起frame的重新計(jì)算

UIView.transform的高級玩法

上面的理論知識(shí)已經(jīng)寫了很多了毅厚,接下來我們實(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ù)合變換的過程中一直跟隨View在移動(dòng)因此平移和旋轉(zhuǎn)的順序會(huì)決定不同的結(jié)果酷窥。

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

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

AnchorPoint

如果不修改AnchorPoint則所有的變化都是基于View的中心進(jìn)行蓬推,但是可以通過修改anchorPoint改變原點(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è)案例來對transform做一個(gè)綜合的應(yīng)用妆棒,這個(gè)案例也是從實(shí)際項(xiàng)目中產(chǎn)生的。先看最終效果:

綜合應(yīng)用

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

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

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

手勢控制

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

/// 畫布
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;
        }
    }
}

添加需要的手勢

// 雙指點(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í)躏鱼,三指手勢要優(yōu)先觸發(fā)氮采。因此需要對手勢的delegate做一點(diǎn)處理

// MARK: - UIGestureRecognizerDelegate
extension CanvasContentView: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // 各手勢之間要并發(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;
    }
}

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

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

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

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

  • 1畔师、調(diào)整anchorPoint把View坐標(biāo)原點(diǎn)移動(dòng)到旋轉(zhuǎn)點(diǎn)位置娶靡,然后通過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)是比較容易通過旋轉(zhuǎn)中心點(diǎn)的坐標(biāo)推算出anchorPoint的新位置,但是一旦View發(fā)生了旋轉(zhuǎn)就很難再計(jì)算出新的anchorPoint的位置腔寡。而方案2只需要計(jì)算出旋轉(zhuǎn)過程中View中心點(diǎn)的位置變化即可焚鲜。
根據(jù)之前的理論知識(shí)坐標(biāo)系中的一個(gè)點(diǎn)繞另一個(gè)點(diǎn)的旋轉(zhuǎ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)行縮放
變換矩陣表示為:

化簡為:


看一下部分代碼

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);
}

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

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

// 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 {
                    // 記錄開始位置
                    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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锤灿,一起剝皮案震驚了整個(gè)濱河市挽拔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌但校,老刑警劉巖螃诅,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異状囱,居然都是意外死亡术裸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進(jìn)店門亭枷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袭艺,“玉大人,你說我怎么就攤上這事叨粘』啵” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵升敲,是天一觀的道長答倡。 經(jīng)常有香客問我,道長驴党,這世上最難降的妖魔是什么瘪撇? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮港庄,結(jié)果婚禮上倔既,老公的妹妹穿的比我還像新娘。我一直安慰自己攘轩,他們只是感情好叉存,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著度帮,像睡著了一般歼捏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笨篷,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天瞳秽,我揣著相機(jī)與錄音,去河邊找鬼率翅。 笑死练俐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冕臭。 我是一名探鬼主播腺晾,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼燕锥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悯蝉?” 一聲冷哼從身側(cè)響起归形,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鼻由,沒想到半個(gè)月后暇榴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蕉世,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年蔼紧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狠轻。...
    茶點(diǎn)故事閱讀 39,739評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奸例,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哈误,到底是詐尸還是另有隱情哩至,我是刑警寧澤躏嚎,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布蜜自,位于F島的核電站,受9級特大地震影響卢佣,放射性物質(zhì)發(fā)生泄漏重荠。R本人自食惡果不足惜般妙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一吟孙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛤铜,春花似錦嘹叫、人聲如沸婆殿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婆芦。三九已至,卻和暖如春喂饥,著一層夾襖步出監(jiān)牢的瞬間消约,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工员帮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留或粮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓捞高,卻偏偏與公主長得像氯材,于是被迫代替她去往敵國和親渣锦。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容