路徑布局MyPathLayout是MyLayout布局體系中的第7種布局體系,在這種布局體系中您只需要提供一個坐標軸、一個曲線函數(shù)、以及視圖之間的距離這三個要素就可以構造出來一個非常酷炫的界面布局效果。在了解路徑布局之前您可以看看下面幾個用路徑布局實現(xiàn)的效果實例:
曲線
在解析幾何的課程中可以知道一個一元函數(shù)可以在二維平面坐標空間中繪制出一條對應的幾何曲線來。下面是幾種常見的函數(shù)的幾何曲線圖。
一些應用中我們能看到一些UI界面的元素總是按照某些曲線路徑來排列展示,這些特殊效果能夠大大的增強用戶體驗以及增強界面的美觀性。這些布局中視圖按照某些規(guī)則排列在某些函數(shù)曲線之上,或者說我們提供一條路徑曲線,然后子視圖按照這條路徑曲線等距離或者按照某種規(guī)則進行排列。所以基于這種規(guī)律性,我們提出了路徑布局的概念。
路徑布局MyPathLayout是MyLayout布局體系里面的其中一種視圖布局的方法,在路徑布局里面的子視圖總是按照提供的一條函數(shù)曲線和一種定位的規(guī)則進行排列布局。 那么如何來構造這個曲線函數(shù),以及如何來指定這些規(guī)則呢?
坐標軸
我們知道視圖是一個矩形區(qū)域的抽象,而我們在用平面坐標進行曲線繪制時也是要求將自變量和因變量限制在某個區(qū)間當中,區(qū)間也是一個矩形區(qū)域。因此一個視圖的區(qū)域是完全可以當做一個平面坐標的區(qū)域的。對于構建一個平面坐標來說,我們需要指定坐標的原點在哪里,同時我們還要指定坐標中橫軸代表的是自變量還是因變量,同時我們還要指定縱軸中的值在原點以上是正數(shù)還是負數(shù),同時我們還要指定函數(shù)曲線的自變量的開始和結束的取值區(qū)間來構建有限的平面區(qū)域。為了對坐標的表征我們抽象出了一個坐標類:
/**
* 坐標軸設置類,用來描述坐標軸的信息。一個坐標軸具有原點、坐標系類型、開始和結束點、坐標軸對應的值這四個方面的內容。
*/
@interface MyCoordinateSetting : NSObject
/**
*坐標原點的位置,位置是相對位置,默認是(0,0), 假如設置為(0.5,0.5)則在視圖的中間。
*/
@property(nonatomic, assign) CGPoint origin;
/**
* 指定是否是數(shù)學坐標系,默認為NO,表示繪圖坐標系。 數(shù)學坐標系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用來指定坐標的原點在平面區(qū)域的位置,這里的值是一個相對值,默認的(0,0)表示坐標原點位于視圖平面區(qū)域的左上角,而如果您設置的值是(0.5,0.5)則表示位于視圖區(qū)域的中心點的位置。
- 其中的isMath用來指定縱坐標軸也就是y軸的值的方向。在我們學習幾何課程時一般都把縱軸原點以上的值設定為正值,而把原點以下的值設定為負值。而在iOS開發(fā)中則恰恰相反。因為這個屬性值默認是設置為NO的,表示縱軸的值原點往上是負數(shù)而原點往下則是正數(shù)。
- 其中的isReverse則用來指定橫軸上的值代表的是自變量還是因變量
- 其中的start,end則用來表明坐標軸上自變量的取值區(qū)間。如果不設置則根據(jù)坐標原點設置以及視圖的尺寸自動確定,因為坐標軸是一個無窮大的區(qū)域,因此我們必須要限制這個區(qū)域的大小才能映射到真實的視圖矩形區(qū)域中去。
在MyPathLayout中存在一個屬性:
/**
* 坐標系設置,您可以調整坐標系的各種參數(shù)來完成下列兩個方法中的坐標到繪制的映射轉換。
*/
@property(nonatomic, strong, readonly) MyCoordinateSetting *coordinateSetting;
就是用來描述路徑布局中所使用的坐標軸信息的,因此坐標軸是路徑布局中的第一要素。
函數(shù)
當坐標軸設置完成后,我們就需要指定在坐標軸上的曲線了。我們知道在二維坐標系中的一條曲線由無數(shù)個點組成,一個點組(x,y)分別表示x軸上的數(shù)字和y軸上的數(shù)字,這些點是服從某些規(guī)則來進行排列的,而這個規(guī)則我們是可以用數(shù)學函數(shù)來描述,也就是一條曲線將對應一個數(shù)學函數(shù)。為了表示這種(x,y)點規(guī)則的數(shù)學函數(shù),我們可以用如下三種方式來表征:
- 直角坐標系方式: y = ??(x)
- 參數(shù)方程方式: y = ??(t),x = ??(t)
- 極坐標系方式: r = ??(??), y = r * sin(??),x = r * cos(??)
具體用那種方式來描述平面坐標點,則可以根據(jù)具體的需要以及情況。在路徑布局MyPathLayout中我們可以提供上面三種方程的表示:
/**
* 直角坐標普通方程,x是坐標系里面x軸的位置,返回y = f(x)。要求函數(shù)在定義域內是連續(xù)的,否則結果不確定。如果返回的y無效則函數(shù)要返回NAN
*/
@property(nonatomic, copy) CGFloat (^rectangularEquation)(CGFloat x);
/**
*直角坐標參數(shù)方程,t是參數(shù), 返回CGPoint是x軸和y軸的值。要求函數(shù)在定義域內是連續(xù)的,否則結果不確定。如果返回的點無效,則請返回CGPointMake(NAN,NAN)
*/
@property(nonatomic, copy) CGPoint (^parametricEquation)(CGFloat t);
/**
*極坐標方程,angle是極坐標的弧度,返回r半徑。要求函數(shù)在定義域內是連續(xù)的,否則結果不確定。如果返回的點無效,則請返回NAN
*/
@property(nonatomic, copy) CGFloat (^polarEquation)(CGFloat angle);
上面的rectangularEquation, parametricEquation, polarEquation
分別用來表示直角坐標方程函數(shù),參數(shù)方程函數(shù),以及極坐標方程函數(shù)。可以看出三者都是以block方式存在。因此我們只需要在block中實現(xiàn)不同的函數(shù)體即可。不同的函數(shù)體意味著不同的方程,在路徑布局中一個時刻只能有一種函數(shù)生效。從上面提供的三個屬性中我們可以得出如下規(guī)約:
- 每種函數(shù)中如果返回NAN則表示在這個定義域內或者值域內是無值的,也就是函數(shù)通過返回NAN來描述不連續(xù)性。
- 對于直角坐標方程函數(shù)來說x的值的區(qū)間由MyCoordinateSetting中的start和end來指定,默認步長是1,如果不指定開始和結束區(qū)間默認就是布局視圖的尺寸作為區(qū)間。
- 對于參數(shù)方程函數(shù)來說t的值的區(qū)間由MyCoordinateSetting中的start和end來指定,默認步長是1,如果不指定開始和結束區(qū)間默認就是布局視圖的尺寸作為區(qū)間。函數(shù)返回的一定是一個CGPoint型分別表示x和y。
- 對于極坐標方程函數(shù)來說angle的值是弧度值,其區(qū)間由MyCoordinateSetting中的start和end來指定,默認步長是1度。如果不指定則默認是0到2??。
下面是一些常見函數(shù)的例子:
//直線函數(shù) y = a *x + b;
pathLayout.rectangularEquation = ^(CGFloat x)
{
return 2 * x + 3;
};
//正玄函數(shù) y = a* sin(x);
pathLayout.rectangularEquation = ^(CGFloat x)
{
return (CGFloat)(100 * sin(x / 180.0 * M_PI));
};
//擺線函數(shù), 用參數(shù)方程: 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)));
};
//阿基米德螺旋線函數(shù): 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));
};
距離
當一個路徑布局中的坐標和曲線函數(shù)都確定好了以后,接下來就需要確定布局中的子視圖按照什么規(guī)則來進行排列布局了。我們知道函數(shù)曲線是一個連續(xù)的曲線,我們的子視圖將根據(jù)添加的順序沿著這條曲線依次排列。一般的情況下是希望里面的子視圖的中心點在曲線上等距離排列。而且目前路徑布局也只是支持了這種等距離排列的機制。需要注意的是這個等距離并不是兩個子視圖中心點之間的直線距離而是曲線距離。為此我們提供了一個路徑距離的類MyPathSpace。這個類用來描述子視圖之間的路徑距離的類型。他的定義如下:
/**
*子視圖之間的路徑距離類,描述子視圖在路徑上的間隔距離的類型。
*/
@interface MyPathSpace : NSObject
/**浮動距離,根據(jù)布局視圖的尺寸和子視圖的數(shù)量動態(tài)決定*/
+(id)flexed;
/**固定距離,len為長度,每個子視圖之間的距離都是len*/
+(id)fixed:(CGFloat)len;
/**數(shù)量距離,根據(jù)布局視圖的尺寸和指定的數(shù)量count動態(tài)決定。*/
+(id)count:(NSInteger)count;
@end
可以看出MyPathSpace路徑距離可以支持三種類型的距離:
- flexed 浮動距離,這個距離將會根據(jù)布局視圖的尺寸和添加的子視圖的數(shù)量來動態(tài)計算。也就是說子視圖之間的距離會隨著數(shù)量的增加和被壓縮減少。
- fixed 固定距離,這個表示無論添加多少子視圖,子視圖之間的距離總是一個固定的數(shù)字。
- count 數(shù)量距離,這個值表示的是子視圖之間的距離總是按照在一定布局尺寸并且某個具體的數(shù)量下決定的。flexed和count的區(qū)別是前者根據(jù)所有的子視圖數(shù)量來動態(tài)計算間距,而后者則是根據(jù)指定的子視圖數(shù)量來靜態(tài)計算間距。
在路徑布局中提供了一個如下的屬性來指定布局中的子視圖距離類型:
/**
*設置子視圖在路徑曲線上的距離的類型,一共有Flexed, Fixed, MaxCount,默認是Flexed,
*/
@property(nonatomic, strong) MyPathSpace *spaceType;
通過上面的三要素:坐標、函數(shù)、距離我們就可以很簡單的完成路徑布局的工作了,你后續(xù)需要做的只是指定要添加到路徑布局的子視圖的尺寸就可以了,至于位置則會根據(jù)你所指定的三要素自動按照添加的順序進行排列了。
路徑布局MyPathLayout中的各種方法和屬性
1. 原點視圖
在實踐中我們還存在一種場景就是希望某個視圖排列在坐標區(qū)域的中心原點,而不是排列在曲線上,這也是可以實現(xiàn)的,我們可以通過如下屬性:
/**
*設置和獲取布局視圖中的原點視圖,默認是nil。如果設置了原點視圖則總會將原點視圖作為布局視圖中的最后一個子視圖。原點視圖將會顯示在路徑的坐標原點中心上,因此原點布局是不會參與在路徑中的布局的。因為中心原點視圖是布局視圖中的最后一個子視圖,而MyPathLayout重寫了AddSubview方法,因此可以正常的使用這個方法來添加子視圖。
*/
@property(nonatomic, strong) UIView *originView;
來設置原點視圖,設置的原點視圖將不會參與到路徑曲線的排列中去,而是放置在坐標軸的原點區(qū)域位置。原點視圖是一個可選的子視圖,具體則需要根據(jù)界面的需求而設定。因為原點視圖也是布局視圖的一個子視圖,因此當我們用subviews方法時得到的將是所有子視圖,而我們只想要那些排列在路徑曲線中的子視圖(除中心原點視圖)時則可以用如下屬性獲得:
/**
*返回布局視圖中所有在曲線路徑中排列的子視圖。如果設置了原點視圖則返回subviews里面除最后一個子視圖外的所有子視圖,如果沒有原點子視圖則返回subviews
*/
@property(nonatomic, strong,readonly) NSArray *pathSubviews;
2. 得到路徑布局中某個子視圖的位置的自變量。
使用路徑布局的目的是我們可以建立一些酷炫的布局效果,如果我們能夠附加一些動畫效果的話,那結果就更加美觀了。既然路徑布局是子視圖沿著曲線點來布局的,那如果我們能夠取得這些曲線點的信息的話,就可以用他來構建一些關鍵幀動畫KeyFrame Animation或者Core Animation中的一些特效。
前面介紹了我們通過三種方程來構建函數(shù),那么有時候我們希望知道某個子視圖布局的那個點的自變量的值。舉例來說,假如我們用極坐標構建了一個半徑為20的圓函數(shù) :r = 20, 然后子視圖之間的間距我們設置為flexed。同時假如我添加了N個子視圖,現(xiàn)在我想知道某個子視圖在圓路徑布局所處的角度值。那么這時候我們就可以通過如下方法來獲取了:
/**
得到子視圖在曲線路徑中定位時的函數(shù)的自變量的值。也就是說在函數(shù)中當值等于下面的返回值時,這個視圖的位置就被確定了。方法如果返回NAN則表示這個子視圖沒有定位。
@param subview 指定的子視圖
@return 返回指定子視圖在曲線路徑中的自變量值
*/
-(CGFloat)argumentFrom:(UIView*)subview;
這個方法的入參是某個路徑布局中的子視圖,而返回則是這個子視圖在路徑布局函數(shù)中的變量值。就上面的例子來說,他所表示的就是某個子視圖在圓上的角度。因此我們可以通過這個返回值來做一些子視圖角度旋轉的坐標變換(通過視圖的transform屬性來實現(xiàn))。或者角度變化動畫效果等。
3. 獲取兩個子視圖之間的路徑坐標點信息。
有時候我們需要得到布局視圖里面兩個子視圖之間的所有曲線路徑點坐標,這樣我們可以很方便的做一些幀動畫來實現(xiàn)一些特殊效果。這時候可以通過下面三個方法來完成:
/**
下面三個函數(shù)用來獲取兩個子視圖之間的曲線路徑數(shù)據(jù),在調用getSubviewPathPoint方法之前請先調用beginSubviewPathPoint方法,而調用完畢后請調用endSubviewPathPoint方法,否則getSubviewPathPoint返回的結果未可知。
*/
/**
開始獲取子視圖路徑數(shù)據(jù)的方法
@param full 表示getSubviewPathPoint獲取的是否是全部路徑點。如果為NO則只會獲取子視圖的位置的點
*/
-(void)beginSubviewPathPoint:(BOOL)full;
/**
結束獲取子視圖路徑數(shù)據(jù)的方法
*/
-(void)endSubviewPathPoint;
/**
創(chuàng)建從某個子視圖到另外一個子視圖之間的路徑點,返回NSValue數(shù)組,里面的值是CGPoint。
@param fromIndex 指定開始的子視圖的索引位置
@param toIndex 指定結束的子視圖的索引位置。如果有原點子視圖時,這兩個索引值不能算上原點子視圖的索引值。
@return 返回fromIndex到toIndex之間的所有曲線路徑點數(shù)組
*/
-(NSArray<NSValue*>*)getSubviewPathPoint:(NSInteger)fromIndex toIndex:(NSInteger)toIndex;
在獲取兩個子視圖之間的路徑點數(shù)組之前,為了加速性能上處理,我們需要調用beginSubviewPathPoint
方法,然后再調用getSubviewPathPoint
方法,最后不再需要路徑點時需要調用endSubviewPathPoint
方法來釋放一些內存。beginSubviewPathPoint
方法中的full參數(shù)表明緩存的點是所有的路徑上的點還是所有子視圖的點。getSubviewPathPoint
方法可以得到任意兩個在路徑上的子視圖之間的所有路徑點數(shù)組,路徑點是一個CGPoint型。為了存儲在NSArray上,系統(tǒng)把CGPoint型轉化為了NSValue型來處理。這幾個方法的使用具體可以參考PLTest1ViewController里面的介紹。
4.獲取函數(shù)曲線路徑。
既然路徑布局是子視圖在一條路徑曲線上排列,那么就應該有方法能夠得到這條路徑,這可以通過如下方法:
/**
創(chuàng)建布局的曲線的路徑。用戶需要負責銷毀返回的值。調用者可以用這個方法來獲得曲線的路徑,進行一些繪制的工作。
@param subviewCount 指定這個路徑上子視圖的數(shù)量的個數(shù),如果設置為-1則是按照布局視圖的子視圖的數(shù)量來創(chuàng)建。需要注意的是如果布局視圖的spaceType為Flexed,Count的話則這個參數(shù)設置無效。
@return 返回指定數(shù)量的子視圖的曲線路徑,用戶需要負責銷毀返回的對象。
*/
-(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的各種屬性,這樣你的布局視圖里面將會出現(xiàn)一條你所設置的函數(shù)的路徑曲線來。具體實現(xiàn)請參考:PLTest2ViewController
5.路徑布局子視圖之間的距離誤差。
在路徑布局中子視圖之間的距離并不是直線的等間距,而是曲線的等間距,因此這里就涉及到了如何保證曲線等間距的問題。我們知道高等數(shù)學里面的微積分中有介紹,要想獲得一條曲線之間兩點之間的長度可以通過如下方法得到。
在實現(xiàn)時因為數(shù)學庫里面并沒有對應的積分函數(shù),而積分的本質是小區(qū)域累加,因此MyPathLayout中為了實現(xiàn)視圖之間的等距離也是用了積分累計的方式來計算曲線長度的。這樣在計算時當累加的步長設的越小,那么等距離將是越精確,否則可能會產生一些距離誤差,因此我們提供了下面這個屬性:
/**
設置獲取子視圖距離的誤差值。默認是0.5,誤差越小則距離的精確值越大,誤差最低值不能<=0。一般不需要調整這個值,只有那些要求精度非常高的場景才需要微調這個值,比如在一些曲線路徑較短的情況下,通過調小這個值來子視圖之間間距的精確計算。
*/
@property(nonatomic, assign) CGFloat distanceError;
用來設置我們在計算時允許的距離誤差值。這個誤差值不能設置為0,而且值越小,誤差也越小,當然也更加消耗計算性能。因此這里默認設置為0.5 。這個屬性的應用主要是用在哪些區(qū)域小而子視圖數(shù)量多的場景里面,具體可以參考:PLTest4ViewController中的例子。
總結
路徑布局的知識已經介紹完畢。在界面布局時我們除了能用路徑布局外MyLayout布局體系還分別提供了線性布局、相對布局、表格布局、框架布局、流式布局、浮動布局一共七種布局,在我的簡書里面都有對各種布局進行介紹的文檔。具體要使用那種布局來進行界面布局,就需要具體的根據(jù)你的需求和界面效果圖來完成。總之遇到問題,歡迎大家及時找我交流和解答。