UIView中frame屬性的內部實現

frame、center、bounds、transform

UIView中用於表徵視圖在父視圖中顯示出來的位置和尺寸的屬性是frame。 同時系統還提供另外兩個屬性center和bounds。其中center屬性值描述視圖的中心點在父視圖中的位置,而bounds屬性的size部分則描述視圖自己固有的尺寸。須要注意的是bounds屬性中的origin部分描述的是視圖內部座標系中原點的位置,它影響着裏面子視圖的位置。除此以外,系統還提供一個transform屬性來實現視圖的仿射變換: 好比平移、縮放、旋轉、傾斜的效果。git

在這四個屬性中,除了frame屬性是計算屬性外,其餘三個屬性都是實體屬性。frame的值是依賴這三個屬性計算出來的。在介紹frame的計算公式前先要了解三個概念:圖層(CALayer)、錨點(Anchor Point)、仿射變換。程序員

iOS和macOS兩個系統的參考座標系不一致,對於iOS來講原點默認在視圖的左上角位置,而對於macOS來講原點默認是在視圖的左下角位置。github

UIView和CALayer的定位映射關係

UIView是對視圖的抽象類,它主要用來負責數據的存儲和操做邏輯的實現。而CALayer則是對視圖在屏幕上的渲染和顯示信息的抽象類。對於一個視圖的位置和尺寸來講也是屬於渲染顯示信息的一部分。所以上述視圖中的幾個屬性的內部實現實際上是委託給CALayer中的對應屬性來實現的,其對應關係表以下:編程

UIView CALayer
frame frame
center position
bounds bounds
transform affineTransform

視圖的位置和尺寸-圖片來源於核心動畫編程指南

錨點(Anchor Point)

所謂錨點就是用來肯定視圖在父視圖中的位置而在視圖內某個點的相對座標值。視圖是一個矩形區域,裏面有無數個點,只要明確了視圖內某個點的座標值在父視圖中的位置,那麼這個視圖的位置就能夠被確認,而這個被指定的視圖內的位置座標點就是錨點。系統爲了表徵錨點而爲層提供了一個anchorPoint屬性。錨點是一個相對座標值,其左上角的位置是(0,0)而右下角的位置是(1,1)中心點的錨點值就是(0.5,0.5)了*(對於macOS系統來講,由於座標系的不一樣,(0,0)位置位於左下角,而(1,1)位置則位於右上角)*。默認狀況下系統將層內的中心點做爲錨點,這也就是視圖的center屬性描述的是視圖的中心點在父視圖的位置的緣由。錨點是CALayer中的概念,而不是視圖的概念。就如上面的視圖屬性和層屬性的對應關係能夠看出來視圖的center屬性對應的是層的position屬性。其實後者更能表現錨點位置這個概念,由於position代表的是層的錨點在父層中的絕對位置。雖然默認狀況下錨點是(0.5,0.5)而這個設定恰好和center屬性所代表的意思是一致的,可是咱們是能夠改變錨點的值的。就好比下面的代碼:bash

//創建時A的frame是(0,0,100,100), 而center值是(50,50)代表center恰好是中心點位置。
UIView *A = [[UIView alloc] initWithFrame:CGRectMake(0,0,100,100)];
A.layer.anchorPoint = CGPointMake(0,0);

//這時候frame的值將變爲(50,50,100,100), 可是center的值仍是(50,50)卻不是代表視圖的中心點位置了。
CGRect frame = A.frame;  

複製代碼

錨點-圖片來源於核心動畫編程指南

仿射變換

所謂仿射變換就是對一個座標空間的全部點進行一次線性變換並接上一個平移處理。iOS系統中的視圖的屬性transform就是用來實現對視圖進行仿射變換處理的。經過仿射變換咱們能夠很輕易的實現對視圖的移動、縮放、旋轉、傾斜等處理。transform屬性是一個結構體類型的數據:markdown

struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};
複製代碼

下面的公式就是利用這個結構體來實現座標點由(x0,y0)到(x1,y1)的仿射變換處理:函數

x1 = a*x0 + b*y0 + tx
y1 = c*x0 + d*y0 + ty
複製代碼

系統提供了衆多以CGAffine開頭的函數API來構造和處理各類常見的仿射變換操做。默認狀況下視圖的transform屬性值是一個CGAffineTransformIdentity代表不會對視圖進行任何仿射變換處理。oop

一個視圖最終渲染到屏幕上的位置和尺寸是由視圖的原始位置和尺寸外加仿射變換來決定的。視圖渲染到屏幕上的最終位置和尺寸能夠經過frame屬性來獲取。佈局

frame的計算規則

從上面的介紹中能夠看出,一個視圖最終渲染出來的位置和尺寸須要經過設置視圖或者層的center、bounds、transform、anchorPoint四個屬性來完成。可是這樣太過於麻煩,所以爲了簡化操做能夠經過frame屬性來完成這些設置。 frame屬性是一個計算屬性。下面就是這個屬性的獲取和設置的實現的僞代碼:動畫

-(CGRect)frame
{
      CGRect retValue = CGRectZero;
      if (CGAffineTransformIsIdentity(self.transform))
      { //沒有設置仿射變換的狀況下
           
            //位置等於中心點的位置減去視圖尺寸乘以錨點的值。
            retValue.origin.x = self.center.x - self.bounds.size.width * self.layer.anchorPoint.x;
            retValue.origin.y = self.center.y - self.bounds.size.height * self.layer.anchorPoint.y;
           //尺寸等於視圖的尺寸
            retValue.size.width = self.bounds.size.width;
            retValue.size.height = self.bounds.size.height;
      }
      else
      {
            CGAffineTransform left =  CGAffineTransformMakeTranslation(-1 * self.bounds.size.width * self.layer.anchorPoint.x, -1 * self.bounds.size.height * self.layer.anchorPoint.y);
            //由於下面的座標變換應用是從(0,0)開始的,所以這裏的right指定中心點的位置,也就是下面的複合變換右乘right來實現位置的變換處理。
            CGAffineTransform right =  CGAffineTransformMakeTranslation(self.center.x, self.center.y);
            //整個複合變換是left 左乘 視圖的tansform屬性而後再右乘right變換。
            CGAffineTransform concat  = CGAffineTransformConcat(CGAffineTransformConcat(left, self.transform), right); 
            retValue  = CGRectApplyAffineTransform(CGRectMake(0,0,self.bounds.size.width, self.bounds.size.height), concat);
      }

    return retValue
}

-(void)setFrame:(CGRect)frame
{
      //設置frame時並無考慮到仿射座標變換屬性transform。
      self.center.x = frame.origin.x + self.bounds.size.width * self.layer.anchorPoint.x;
      self.center.y = frame.origin.y  + self.bounds.size.height * self.layer.anchorPoint.y;
      self.bounds.size.width = frame.size.width;
      self.bounds.size.height = frame.size.height;
}
複製代碼

從上面的代碼能夠看出:當一個視圖設置了非CGAffineTransformIdentity的仿射變化值後,咱們不能再經過設置frame屬性的值來修改視圖的位置和尺寸了,不然最終展現的效果未可知。 所以當對視圖設置了仿射變換屬性後,若是須要調整視圖的位置和尺寸時咱們須要操做的是center屬性和bounds屬性而不能在操做frame屬性了。好比下面的例子:

//假設視圖尺寸由原來的(width0, height0)改變爲(width1, height1)那麼正確代碼應該以下設置:
//尺寸直接設置。
view.bounds.size = CGSizeMake(width1, height1);
//位置須要調整
view.center.x += (width1 - width0) * view.layer.anchorPoint.x;
view.center.y += (height1 - height0) * view.layer.anchorPoint.y; 

//假設視圖位置由原來的(x0,y0)改成(x1,y1)那麼正確的代碼應該以下設置:
view.center.x = x1 + view.bounds.size.width * view.layer.anchorPoint.x;
view.center.y = y1 + view.bounds.size.height * view.layer.anchorPoint.y;


複製代碼

AutoLayout在完成佈局後,所計算出來的位置和尺寸內部修改的值是center和bounds兩個屬性,所以最終的展現效果不會由於仿射變換而產生異常。同時這也解釋了爲何經過AutoLayout設置約束後修改frame屬性來改變位置和尺寸不會起做用的緣由。

MyLayout佈局計算早期是經過修改視圖的frame屬性來完成佈局的,可是後來發現有程序員在設置了仿射變換屬性後發現視圖展現出現異常,後來的版本內部也統一改成了修改視圖的center和bounds屬性來解決這類問題。

相關文章
相關標籤/搜索