路徑佈局-基於數學函式的檢視佈局方法

lvxfcjf發表於2021-09-09

路徑佈局MyPathLayout佈局體系中的第7種佈局體系,在這種佈局體系中您只需要提供一個座標軸、一個曲線函式、以及檢視之間的距離這三個要素就可以構造出來一個非常酷炫的介面佈局效果。在瞭解路徑佈局之前您可以看看下面幾個用路徑佈局實現的效果例項:

圖片描述

路徑佈局效果演示圖

曲線

在解析幾何的課程中可以知道一個一元函式可以在二維平面座標空間中繪製出一條對應的幾何曲線來。下面是幾種常見的函式的幾何曲線圖。

圖片描述

常見的函式曲線

一些應用中我們能看到一些UI介面的元素總是按照某些曲線路徑來排列展示,這些特殊效果能夠大大的增強使用者體驗以及增強介面的美觀性。這些佈局中檢視按照某些規則排列在某些函式曲線之上,或者說我們提供一條路徑曲線,然後子檢視按照這條路徑曲線等距離或者按照某種規則進行排列。所以基於這種規律性,我們提出了路徑佈局的概念。

路徑佈局MyPathLayout是MyLayout佈局體系裡面的其中一種檢視佈局的方法,在路徑佈局裡面的子檢視總是按照提供的一條函式曲線和一種定位的規則進行排列布局。 那麼如何來構造這個曲線函式,以及如何來指定這些規則呢?

座標軸

我們知道檢視是一個矩形區域的抽象,而我們在用平面座標進行曲線繪製時也是要求將自變數和因變數限制在某個區間當中,區間也是一個矩形區域。因此一個檢視的區域是完全可以當做一個平面座標的區域的。對於構建一個平面座標來說,我們需要指定座標的原點在哪裡,同時我們還要指定座標中橫軸代表的是自變數還是因變數,同時我們還要指定縱軸中的值在原點以上是正數還是負數,同時我們還要指定函式曲線的自變數的開始和結束的取值區間來構建有限的平面區域。為了對座標的表徵我們抽象出了一個座標類:

   /**
 * 座標軸設定類,用來描述座標軸的資訊。一個座標軸具有原點、座標系型別、開始和結束點、座標軸對應的值這四個方面的內容。
 */@interface MyCoordinateSetting : NSObject/**
 *座標原點的位置,位置是相對位置,預設是(0,0), 假如設定為(0.5,0.5)則在檢視的中間。
 */@property(nonatomic, assign) CGPoint origin;/**
 * 指定是否是數學座標系,預設為NO,表示繪圖座標系。 數學座標系y軸向上為正,向下為負;繪圖座標系則反之。
 */@property(nonatomic, assign) BOOL isMath;/**
 *指定是否是y軸和x軸互換,預設為NO,如果設定為YES則方程提供的變數是y軸的值,方程返回的是x軸的值。
 */@property(nonatomic, assign) BOOL isReverse;//開始位置和結束位置。如果設定為-CGFLOAT_MAX, CGFLOAT_MAX表示取值是正無窮和負無窮@property(nonatomic, assign) CGFloat start;@property(nonatomic, assign) CGFloat end;

-(void)reset;  //恢復預設設定。@end

MyCoordinateSetting就是一個對座標進行抽象的類,從類的定義中我們可以看出一個座標設定的所有元素:

  • 其中的origin用來指定座標的原點在平面區域的位置,這裡的值是一個相對值,預設的(0,0)表示座標原點位於檢視平面區域的左上角,而如果您設定的值是(0.5,0.5)則表示位於檢視區域的中心點的位置。

  • 其中的isMath用來指定縱座標軸也就是y軸的值的方向。在我們學習幾何課程時一般都把縱軸原點以上的值設定為正值,而把原點以下的值設定為負值。而在iOS開發中則恰恰相反。因為這個屬性值預設是設定為NO的,表示縱軸的值原點往上是負數而原點往下則是正數。

  • 其中的isReverse則用來指定橫軸上的值代表的是自變數還是因變數

  • 其中的start,end則用來表明座標軸上自變數的取值區間。如果不設定則根據座標原點設定以及檢視的尺寸自動確定,因為座標軸是一個無窮大的區域,因此我們必須要限制這個區域的大小才能對映到真實的檢視矩形區域中去。

在MyPathLayout中存在一個屬性:

/**
 * 座標系設定,您可以調整座標系的各種引數來完成下列兩個方法中的座標到繪製的對映轉換。
 */@property(nonatomic, strong, readonly) MyCoordinateSetting *coordinateSetting;

就是用來描述路徑佈局中所使用的座標軸資訊的,因此座標軸是路徑佈局中的第一要素。

函式

當座標軸設定完成後,我們就需要指定在座標軸上的曲線了。我們知道在二維座標系中的一條曲線由無數個點組成,一個點組(x,y)分別表示x軸上的數字和y軸上的數字,這些點是服從某些規則來進行排列的,而這個規則我們是可以用數學函式來描述,也就是一條曲線將對應一個數學函式。為了表示這種(x,y)點規則的數學函式,我們可以用如下三種方式來表徵:

  • 直角座標系方式: y = ?(x)

  • 引數方程方式: y = ?(t),x = ?(t)

  • 極座標系方式: r = ?(?),   y = r * sin(?),x = r * cos(?)

具體用那種方式來描述平面座標點,則可以根據具體的需要以及情況。在路徑佈局MyPathLayout中我們可以提供上面三種方程的表示:

/**
 * 直角座標普通方程,x是座標系裡面x軸的位置,返回y = f(x)。要求函式在定義域內是連續的,否則結果不確定。如果返回的y無效則函式要返回NAN
 */@property(nonatomic, copy) CGFloat (^rectangularEquation)(CGFloat x);/**
 *直角座標引數方程,t是引數, 返回CGPoint是x軸和y軸的值。要求函式在定義域內是連續的,否則結果不確定。如果返回的點無效,則請返回CGPointMake(NAN,NAN)
 */@property(nonatomic, copy) CGPoint (^parametricEquation)(CGFloat t);/**
 *極座標方程,angle是極座標的弧度,返回r半徑。要求函式在定義域內是連續的,否則結果不確定。如果返回的點無效,則請返回NAN
 */@property(nonatomic, copy) CGFloat (^polarEquation)(CGFloat angle);

上面的rectangularEquation, parametricEquation, polarEquation分別用來表示直角座標方程函式,引數方程函式,以及極座標方程函式。可以看出三者都是以block方式存在。因此我們只需要在block中實現不同的函式體即可。不同的函式體意味著不同的方程,在路徑佈局中一個時刻只能有一種函式生效。從上面提供的三個屬性中我們可以得出如下規約:

  1. 每種函式中如果返回NAN則表示在這個定義域內或者值域內是無值的,也就是函式透過返回NAN來描述不連續性。

  2. 對於直角座標方程函式來說x的值的區間由MyCoordinateSetting中的start和end來指定,預設步長是1,如果不指定開始和結束區間預設就是佈局檢視的尺寸作為區間。

  3. 對於引數方程函式來說t的值的區間由MyCoordinateSetting中的start和end來指定,預設步長是1,如果不指定開始和結束區間預設就是佈局檢視的尺寸作為區間。函式返回的一定是一個CGPoint型分別表示x和y。

  4. 對於極座標方程函式來說angle的值是弧度值,其區間由MyCoordinateSetting中的start和end來指定,預設步長是1度。如果不指定則預設是0到2?。

下面是一些常見函式的例子:

 //直線函式 y = a *x + b;     
 pathLayout.rectangularEquation = ^(CGFloat x)
 {      return 2 * x + 3;
 };        
 //正玄函式 y = a* sin(x);
 pathLayout.rectangularEquation = ^(CGFloat x)
 {     return (CGFloat)(100 * sin(x / 180.0 * M_PI));
 };       
 //擺線函式, 用引數方程: x = a * (t - sin(t); y = a *(1 - cos(t));
 pathLayout.parametricEquation = ^(CGFloat t)
 {      CGFloat t2 = t / 180 * M_PI;  //角度轉化為弧度。
      CGFloat a = 50;      return CGPointMake(a * (t2 - sin(t2)), a * (1 - cos(t2)));            
};       
//阿基米德螺旋線函式: r = a * θ   用的是極座標。 pathLayout.polarEquation = ^(CGFloat angle){   return 20 * angle;
};//心形線 r = a *(1 + cos(θ)pathLayout.polarEquation = ^(CGFloat angle)
{    return (CGFloat)(120 * (1 + cos(angle)));
}; //星型線 x = a * cos^3(θ); y =a * sin^3(θ);pathLayout.parametricEquation = ^(CGFloat t)
{            
    return CGPointMake(150 * pow(cos(t / 180 * M_PI),3), 150 * pow(sin(t / 180 * M_PI),3));
 };

距離

當一個路徑佈局中的座標和曲線函式都確定好了以後,接下來就需要確定佈局中的子檢視按照什麼規則來進行排列布局了。我們知道函式曲線是一個連續的曲線,我們的子檢視將根據新增的順序沿著這條曲線依次排列。一般的情況下是希望裡面的子檢視的中心點在曲線上等距離排列。而且目前路徑佈局也只是支援了這種等距離排列的機制。需要注意的是這個等距離並不是兩個子檢視中心點之間的直線距離而是曲線距離。為此我們提供了一個路徑距離的類MyPathSpace。這個類用來描述子檢視之間的路徑距離的型別。他的定義如下:

/**
 *子檢視之間的路徑距離類,描述子檢視在路徑上的間隔距離的型別。
 */@interface MyPathSpace : NSObject/**浮動距離,根據佈局檢視的尺寸和子檢視的數量動態決定*/+(id)flexed;/**固定距離,len為長度,每個子檢視之間的距離都是len*/+(id)fixed:(CGFloat)len;/**數量距離,根據佈局檢視的尺寸和指定的數量count動態決定。*/+(id)count:(NSInteger)count;@end

可以看出MyPathSpace路徑距離可以支援三種型別的距離:

  • flexed  浮動距離,這個距離將會根據佈局檢視的尺寸和新增的子檢視的數量來動態計算。也就是說子檢視之間的距離會隨著數量的增加和被壓縮減少。

  • fixed 固定距離,這個表示無論新增多少子檢視,子檢視之間的距離總是一個固定的數字。

  • count 數量距離,這個值表示的是子檢視之間的距離總是按照在一定佈局尺寸並且某個具體的數量下決定的。flexed和count的區別是前者根據所有的子檢視數量來動態計算間距,而後者則是根據指定的子檢視數量來靜態計算間距。

在路徑佈局中提供了一個如下的屬性來指定佈局中的子檢視距離型別:

/**
 *設定子檢視在路徑曲線上的距離的型別,一共有Flexed, Fixed, MaxCount,預設是Flexed,
 */@property(nonatomic, strong) MyPathSpace *spaceType;

透過上面的三要素:座標、函式、距離我們就可以很簡單的完成路徑佈局的工作了,你後續需要做的只是指定要新增到路徑佈局的子檢視的尺寸就可以了,至於位置則會根據你所指定的三要素自動按照新增的順序進行排列了。

路徑佈局MyPathLayout中的各種方法和屬性

1. 原點檢視

在實踐中我們還存在一種場景就是希望某個檢視排列在座標區域的中心原點,而不是排列在曲線上,這也是可以實現的,我們可以透過如下屬性:

/**
 *設定和獲取佈局檢視中的原點檢視,預設是nil。如果設定了原點檢視則總會將原點檢視作為佈局檢視中的最後一個子檢視。原點檢視將會顯示在路徑的座標原點中心上,因此原點佈局是不會參與在路徑中的佈局的。因為中心原點檢視是佈局檢視中的最後一個子檢視,而MyPathLayout重寫了AddSubview方法,因此可以正常的使用這個方法來新增子檢視。
 */@property(nonatomic, strong) UIView *originView;

來設定原點檢視,設定的原點檢視將不會參與到路徑曲線的排列中去,而是放置在座標軸的原點區域位置。原點檢視是一個可選的子檢視,具體則需要根據介面的需求而設定。因為原點檢視也是佈局檢視的一個子檢視,因此當我們用subviews方法時得到的將是所有子檢視,而我們只想要那些排列在路徑曲線中的子檢視(除中心原點檢視)時則可以用如下屬性獲得:

/**
 *返回佈局檢視中所有在曲線路徑中排列的子檢視。如果設定了原點檢視則返回subviews裡面除最後一個子檢視外的所有子檢視,如果沒有原點子檢視則返回subviews
 */@property(nonatomic, strong,readonly) NSArray *pathSubviews;
2. 得到路徑佈局中某個子檢視的位置的自變數。

使用路徑佈局的目的是我們可以建立一些酷炫的佈局效果,如果我們能夠附加一些動畫效果的話,那結果就更加美觀了。既然路徑佈局是子檢視沿著曲線點來佈局的,那如果我們能夠取得這些曲線點的資訊的話,就可以用他來構建一些關鍵幀動畫KeyFrame Animation或者Core Animation中的一些特效。

前面介紹了我們透過三種方程來構建函式,那麼有時候我們希望知道某個子檢視佈局的那個點的自變數的值。舉例來說,假如我們用極座標構建了一個半徑為20的圓函式 :r = 20, 然後子檢視之間的間距我們設定為flexed。同時假如我新增了N個子檢視,現在我想知道某個子檢視在圓路徑佈局所處的角度值。那麼這時候我們就可以透過如下方法來獲取了:

/**
 得到子檢視在曲線路徑中定位時的函式的自變數的值。也就是說在函式中當值等於下面的返回值時,這個檢視的位置就被確定了。方法如果返回NAN則表示這個子檢視沒有定位。
 @param subview 指定的子檢視
 @return 返回指定子檢視在曲線路徑中的自變數值
 */-(CGFloat)argumentFrom:(UIView*)subview;

這個方法的入參是某個路徑佈局中的子檢視,而返回則是這個子檢視在路徑佈局函式中的變數值。就上面的例子來說,他所表示的就是某個子檢視在圓上的角度。因此我們可以透過這個返回值來做一些子檢視角度旋轉的座標變換(透過檢視的transform屬性來實現)。或者角度變化動畫效果等。

3. 獲取兩個子檢視之間的路徑座標點資訊。

有時候我們需要得到佈局檢視裡面兩個子檢視之間的所有曲線路徑點座標,這樣我們可以很方便的做一些幀動畫來實現一些特殊效果。這時候可以透過下面三個方法來完成:

/**
 下面三個函式用來獲取兩個子檢視之間的曲線路徑資料,在呼叫getSubviewPathPoint方法之前請先呼叫beginSubviewPathPoint方法,而呼叫完畢後請呼叫endSubviewPathPoint方法,否則getSubviewPathPoint返回的結果未可知。
 *//**
 開始獲取子檢視路徑資料的方法
 @param full 表示getSubviewPathPoint獲取的是否是全部路徑點。如果為NO則只會獲取子檢視的位置的點
 */-(void)beginSubviewPathPoint:(BOOL)full;/**
 結束獲取子檢視路徑資料的方法
 */-(void)endSubviewPathPoint;/**
 建立從某個子檢視到另外一個子檢視之間的路徑點,返回NSValue陣列,裡面的值是CGPoint。
 @param fromIndex 指定開始的子檢視的索引位置
 @param toIndex 指定結束的子檢視的索引位置。如果有原點子檢視時,這兩個索引值不能算上原點子檢視的索引值。
 @return 返回fromIndex到toIndex之間的所有曲線路徑點陣列
 */-(NSArray<NSValue*>*)getSubviewPathPoint:(NSInteger)fromIndex toIndex:(NSInteger)toIndex;

在獲取兩個子檢視之間的路徑點陣列之前,為了加速效能上處理,我們需要呼叫beginSubviewPathPoint方法,然後再呼叫getSubviewPathPoint方法,最後不再需要路徑點時需要呼叫endSubviewPathPoint方法來釋放一些記憶體。beginSubviewPathPoint方法中的full參數列明快取的點是所有的路徑上的點還是所有子檢視的點。getSubviewPathPoint方法可以得到任意兩個在路徑上的子檢視之間的所有路徑點陣列,路徑點是一個CGPoint型。為了儲存在NSArray上,系統把CGPoint型轉化為了NSValue型來處理。這幾個方法的使用具體可以參考裡面的介紹。

4.獲取函式曲線路徑。

既然路徑佈局是子檢視在一條路徑曲線上排列,那麼就應該有方法能夠得到這條路徑,這可以透過如下方法:

/**
 建立佈局的曲線的路徑。使用者需要負責銷燬返回的值。呼叫者可以用這個方法來獲得曲線的路徑,進行一些繪製的工作。
 @param subviewCount 指定這個路徑上子檢視的數量的個數,如果設定為-1則是按照佈局檢視的子檢視的數量來建立。需要注意的是如果佈局檢視的spaceType為Flexed,Count的話則這個引數設定無效。
 @return 返回指定數量的子檢視的曲線路徑,使用者需要負責銷燬返回的物件。
 */-(CGPathRef)createPath:(NSInteger)subviewCount;

來得到一個曲線路徑物件,需要注意的是你應該負責銷燬這個方法返回的物件。這樣你就可以透過得到的曲線路徑物件來進行一些曲線的繪製了,透過曲線的繪製以及佈局裡面子檢視的結合,就能夠得到一些非常有趣的效果。另外一個方案是因為每個檢視都有一個layerClass屬性,路徑佈局也不例外,因此你可以建立一個MyPathLayout的派生類,並過載其中的layerClass方法如下:

//構建一個路徑佈局的派生類。@interface MyXXXPathLayout:MyPathLayout@end@implementation MyXXXPathLayout+(Class)layerClass
 { return [CAShapeLayer class];
 }

-(id)init
{    self = [super init];    if (self != nil)
    {        CAShapeLayer *shapeLayer = (CAShapeLayer*)self.layer;
        shapeLayer.strokeColor = [UIColor redColor].CGColor;
        shapeLayer.lineWidth = 2;
        shapeLayer.fillColor = nil;  //您可以在這裡設定路徑曲線的顏色、大小、填充方案等等。  
    }    
    return self;
}@end

你需要過載layerClass 並返回一個CAShapeLayer。同時你可以在你的派生類裡面設定CAShapeLayer的各種屬性,這樣你的佈局檢視裡面將會出現一條你所設定的函式的路徑曲線來。具體實現請參考:

5.路徑佈局子檢視之間的距離誤差。

在路徑佈局中子檢視之間的距離並不是直線的等間距,而是曲線的等間距,因此這裡就涉及到了如何保證曲線等間距的問題。我們知道高等數學裡面的微積分中有介紹,要想獲得一條曲線之間兩點之間的長度可以透過如下方法得到。

圖片描述

曲線兩點之間的長度

在實現時因為數學庫裡面並沒有對應的積分函式,而積分的本質是小區域累加,因此MyPathLayout中為了實現檢視之間的等距離也是用了積分累計的方式來計算曲線長度的。這樣在計算時當累加的步長設的越小,那麼等距離將是越精確,否則可能會產生一些距離誤差,因此我們提供了下面這個屬性:

/**
  設定獲取子檢視距離的誤差值。預設是0.5,誤差越小則距離的精確值越大,誤差最低值不能<=0。一般不需要調整這個值,只有那些要求精度非常高的場景才需要微調這個值,比如在一些曲線路徑較短的情況下,透過調小這個值來子檢視之間間距的精確計算。
 */@property(nonatomic, assign) CGFloat distanceError;

用來設定我們在計算時允許的距離誤差值。這個誤差值不能設定為0,而且值越小,誤差也越小,當然也更加消耗計算效能。因此這裡預設設定為0.5 。這個屬性的應用主要是用在哪些區域小而子檢視數量多的場景裡面,具體可以參考:中的例子。

總結

路徑佈局的知識已經介紹完畢。在介面佈局時我們除了能用路徑佈局外佈局體系還分別提供了線性佈局、相對佈局、表格佈局、框架佈局、流式佈局、浮動佈局一共七種佈局,在我的裡面都有對各種佈局進行介紹的文件。具體要使用那種佈局來進行介面佈局,就需要具體的根據你的需求和介面效果圖來完成。總之遇到問題,歡迎大家及時找我交流和解答。



作者:歐陽大哥2013


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3486/viewspace-2822744/,如需轉載,請註明出處,否則將追究法律責任。

相關文章