柵格布局簡介
柵格布局MyGridLayout是MyLayout布局體系里面的第八種布局。這是一種將布局約束設(shè)置和視圖分離的布局方式,就像HTML中的標(biāo)簽元素和css樣式可以進(jìn)行分離表示和存儲。因此柵格布局非常適合于數(shù)據(jù)內(nèi)容相同但是展示樣式不同的場景,展示樣式可以動態(tài)配置和變化,甚至于可以從服務(wù)器進(jìn)行動態(tài)下發(fā)。柵格布局還提供了一種基于JSON語法進(jìn)行布局格式描述的機(jī)制來實現(xiàn)界面布局。
柵格布局的需求場景
- 在眾多電商類比如淘寶、天貓、京東等應(yīng)用的首頁中都可以看到商品大都是以分類的形式進(jìn)行排列展示,不同分類的商品的展示樣式不同,并且同一類商品的展示樣式也有可能不同。每個商品都會占用一個矩形的區(qū)域塊,這些矩形區(qū)域塊則總是以某種布局進(jìn)行緊湊的排列組合,比如水平的排列或者垂直的排列,或者水平和垂直混合的排列。每個矩形塊內(nèi)的商品基本都是由主標(biāo)題、付標(biāo)題、圖片、以及一些活動小圖標(biāo)組成,并且點(diǎn)擊矩形區(qū)塊內(nèi)的商品時就會進(jìn)入商品的詳細(xì)頁面中去。
這些電商應(yīng)用的商品排列布局往往都不是固定按某個樣式來展示的,往往都會隨著時間或者節(jié)假日的變化而變化。你會發(fā)現(xiàn)他們之間的一個特點(diǎn)就是往往這些商品的數(shù)據(jù)模型都比較固定但是展示樣式卻千差萬別,因此我們希望這些展示和布局的樣式不能寫死在代碼中而是希望設(shè)計成一種可以動態(tài)配置的方案來解決這種問題。 - 在一些新聞類中比如早期的Zarker或者今日頭條以及網(wǎng)易新聞iPad版本等應(yīng)用中都是以卡片的形式來展示的,而且這些卡片組合有可能是每一頁的樣式都不一樣,每一頁卡片中則由多條不同的新聞按某種順序緊湊的排列組合在一起,每一條新聞都基本由:主標(biāo)題、付標(biāo)題、簡介、縮略圖等組成。在實現(xiàn)這種卡片樣式布局的新聞類應(yīng)用時我們往往都會先設(shè)計出多種不同的展示樣式模板,因為新聞的內(nèi)容相同,我們只需要在不同的頁面中應(yīng)用不同的卡片樣式模板即可。
- 有時候希望我們的應(yīng)用的展示樣式是可以從服務(wù)器下發(fā)來進(jìn)行動態(tài)改變,從而達(dá)到靈活多樣的效果。
上面的幾個例子你會發(fā)現(xiàn)需求都有如下特點(diǎn):
- 界面總是以矩形區(qū)塊的形式進(jìn)行有規(guī)律的排列組合。
- 每個矩形區(qū)塊對應(yīng)一個數(shù)據(jù)模型,并且數(shù)據(jù)模型的內(nèi)容和結(jié)構(gòu)相對穩(wěn)定。
- 界面的布局排列不固定而是可以靈活多變的。
- 界面中的矩形區(qū)塊之間總是會有邊界線來進(jìn)行區(qū)分和隔離。
- 用戶點(diǎn)擊這些矩形區(qū)塊時往往邏輯都是比較統(tǒng)一的進(jìn)行處理。
這些需求基本都可以通過柵格布局來實現(xiàn),而且柵格布局也是一種專門解決這類問題的布局體系!
柵格布局的原理
柵格布局的理念有一部分來源于bootstrap中的功能,以及借鑒了HTML中css和標(biāo)簽元素分離的思想。在柵格布局中所有視圖不需要進(jìn)行任何布局排列相關(guān)的約束設(shè)置,視圖只負(fù)責(zé)內(nèi)容、顏色、字體等相關(guān)屬性的設(shè)置,而柵格則負(fù)責(zé)位置和尺寸對齊以及邊界線相關(guān)的屬性的設(shè)置。這樣就將內(nèi)容和布局進(jìn)行了徹底的分離,而正是這種分離的機(jī)制才使得我們可以完成動態(tài)的位置和尺寸的調(diào)整。那么什么是柵格呢?
柵格其實就是一個格子、一個矩形區(qū)域
我們來考察一個UIWindow以及一個UIView。發(fā)現(xiàn)他們其實都只是一個抽象的矩形區(qū)域。任何一個矩形區(qū)域都有位置和尺寸的概念,位置和尺寸則是通過提供的frame屬性來描述和實現(xiàn)的。我們的界面由很多的視圖組成,從布局的觀點(diǎn)來說,我們的界面其實就是由多個矩形區(qū)域來組成,而所謂的布局其實就是分別設(shè)置每個矩形區(qū)域的位置和尺寸。當(dāng)位置和尺寸設(shè)置好后,我們只需要在對應(yīng)的矩形區(qū)域內(nèi)填充內(nèi)容就可以了,然后系統(tǒng)再分別渲染每個矩形區(qū)域內(nèi)的內(nèi)容,這樣就呈現(xiàn)出了我們的界面了。
為了達(dá)到我們的內(nèi)容和布局分離的目的,就需要將矩形區(qū)域進(jìn)行抽象和處理,因為我們就將一個矩形區(qū)域定義為一個柵格。那么是不是說一個柵格就能滿足條件呢?答案是否定的,既然上面說了我們的界面是由多個矩形區(qū)域組成,那么同樣的在一個柵格布局中也應(yīng)該是由多個柵格組成。如何來對柵格進(jìn)行拆分,柵格和柵格之間的關(guān)系又是如何的?以及如何用柵格來描述一個界面呢?這里我們可以參考一下下面的一個界面:

在這個界面中,那么我們首先可以將整個界面的矩形區(qū)域當(dāng)做為一個柵格G。而這個界面又可以看做是由上、中、下三個矩形區(qū)域組成,因此我們可以將柵格G垂直的從上到下劃分為A,B,C三個子?xùn)鸥?。A,B,C三個柵格還可以繼續(xù)進(jìn)行劃分,其中A柵格區(qū)域又可以水平的從左到右劃分為A1,A2,A3三個子?xùn)鸥?,這樣劃分后A1,A2,A3三個子?xùn)鸥窭锩婢涂梢詫?yīng)填充具體的內(nèi)容了,我們將沒有必要再對A1,A2,A3進(jìn)行繼續(xù)劃分了。同樣的B柵格則可以水平的劃分為從左到右的B1,B2兩個柵格,B1柵格里面可以填充具體的內(nèi)容了,因此不需要進(jìn)一步劃分,而B2柵格我們還需要繼續(xù)進(jìn)行垂直的從上到下劃分為B21,B22兩個柵格,這次劃分后將不需要再次進(jìn)行劃分了;同理C柵格我們也可以同A柵格一樣水平的從左到右依次的劃分為C1,C2,C2三個不需要再繼續(xù)劃分的子?xùn)鸥窳?。下面就是最終的一種柵格的劃分結(jié)果:
可以看出通過對柵格的劃分最終我們在顯示時我們只需要將視圖的內(nèi)容放置到對應(yīng)的不可再繼續(xù)劃分的柵格里面就可以了,我們將不再進(jìn)行繼續(xù)劃分的柵格為葉子?xùn)鸥?。我們可以總結(jié)出這種柵格的劃分法的一些特點(diǎn):
- 柵格總是按照水平或者垂直的規(guī)則來劃分為0到多個更小的柵格。正式因為這種劃分法,每個柵格我們不需要去記錄他的位置和同時記錄寬度和高度,而是只要一個尺寸值就可以描述一個柵格了。
- 柵格可以劃分為眾多的子?xùn)鸥瘢⑶铱梢詿o限的遞歸劃分,從而形成了一顆柵格樹。我們把最后不再繼續(xù)劃分的柵格成為葉子?xùn)鸥瘢x為葉子?xùn)鸥竦臉?biāo)準(zhǔn)是他是否可以滿足用來存放顯示的內(nèi)容,如果某個柵格無法顯示某個獨(dú)立的內(nèi)容則需要繼續(xù)進(jìn)行劃分。
- 柵格的這種定義特性,使得它不適合于用來解決那些具有重疊顯示效果的場景。
所以我們這里再次為柵格進(jìn)行定義:所謂柵格就是一種按照特定規(guī)則進(jìn)行排列以及可以進(jìn)行無限劃分的具有樹形層次結(jié)構(gòu)以及特定尺寸的矩形區(qū)域。這種柵格定義的規(guī)則隱藏了位置的概念,以及隱藏了寬高的概念,而是只用一個值就可以描述一個矩形區(qū)域的位置和尺寸。而且我們規(guī)定只有葉子?xùn)鸥竦膮^(qū)域才用來存放視圖的內(nèi)容。
柵格的屬性
為了表征上面對于柵格的定義和描述,我們需要對柵格進(jìn)行實際的定義。因此我們定義了一個柵格接口:MyGrid, 這個接口的定義在MyGrid.h中能夠看到。下面我們就來具體介紹一下這個接口。
柵格的動作和事件處理機(jī)制
我們使用柵格除了希望能夠顯示內(nèi)容外,還希望其能提供響應(yīng)事件處理邏輯,比如用戶觸摸某個柵格時,希望柵格能夠做出回應(yīng),同時還希望柵格進(jìn)行事件處理時還能使用柵格中保存的附加數(shù)據(jù)。除此之外我們還希望柵格具有能夠唯一標(biāo)識自己的屬性或者某些柵格具有相同的屬性。因此我們將柵格對事件的響應(yīng)處理能力進(jìn)行抽象而構(gòu)建了一個柵格的基接口MyGridAction。
/**
柵格動作接口,您可以觸摸柵格來執(zhí)行特定的動作和事件。
*/
@protocol MyGridAction<NSObject>
/**
柵格的標(biāo)簽標(biāo)識,用于在事件中區(qū)分柵格。
*/
@property(nonatomic) NSInteger tag;
/**
柵格的動作數(shù)據(jù),這個數(shù)據(jù)是柵格的擴(kuò)展數(shù)據(jù),您可以在動作中使用這個附加的數(shù)據(jù)來進(jìn)行一系列操作。他可以是一個數(shù)值,也可以是個字符串,甚至可以是一段JS腳本。
*/
@property(nonatomic, strong) id actionData;
/**
設(shè)置柵格的事件,如果取消柵格事件則設(shè)置target為nil
@param target action事件的調(diào)用者
@param action action事件,格式為:-(void)handle:(id<MyGrid>)sender
*/
-(void)setTarget:(id)target action:(SEL)action;
@end
在上面的柵格動作定義中我們可以看到tag屬性用來對柵格進(jìn)行標(biāo)識和進(jìn)行分類;setTarget:action:方法則可以為柵格設(shè)置用戶觸摸柵格時的響應(yīng)邏輯;actionData則是可以設(shè)置附加在柵格上的任意數(shù)據(jù),具體的數(shù)據(jù)的意義是由使用者進(jìn)行定義的,因此它可以是一個URL,也可以是一個字符串,甚至可以是一段JS腳本。因為我們對柵格布局的定位是可以基于服務(wù)器下發(fā)的動態(tài)布局解決方案。因此我們希望除了界面布局能支持動態(tài)化外,還希望我們的業(yè)務(wù)邏輯也可以一定程度的動態(tài)化(要完成實現(xiàn)業(yè)務(wù)邏輯動態(tài)化實際中是沒有那么簡單的,而且蘋果也是不允許業(yè)務(wù)邏輯能夠在不審核的前提下進(jìn)行更新處理)。因此我們可以借助actionData中的數(shù)據(jù)來支持柵格布局的一部分業(yè)務(wù)邏輯的動態(tài)化的能力。比如下面的代碼:
id<MyGrid> gird = //這里假設(shè)某處獲取了柵格,并且柵格的定義數(shù)據(jù)是從服務(wù)器動態(tài)下發(fā)的(包括actionData)。
[grid setTarget:self action:@selector(handleAction:)];
..........
-(void)handleAction:(id<MyGrid>)grid
{
if (grid.tag == xxx)
{ //假設(shè)tag為xxx時actionData的值是URL
構(gòu)建一個UIWebView,然后將actionData的值傳遞給UIWebview
}
else if (grid.tag == yyy)
{//假設(shè)tag為yyy時actionData的值是一段JS腳本
構(gòu)建一個JSContext對象,并執(zhí)行actionData所描述的腳本。
}
else
{
//..其他類型的數(shù)據(jù)處理。
}
}
柵格的基本屬性
上面曾經(jīng)介紹過柵格其實是一個特定尺寸的矩形區(qū)域,而且柵格是一顆具有父子關(guān)系的樹形數(shù)據(jù)結(jié)構(gòu)。下面就是柵格接口的具體定義,可以看出他是從MyGridAction接口派生
/**
柵格協(xié)議。用來描述柵格矩形區(qū)域,所以一個柵格就是一個矩形區(qū)域。 這個接口用來描述柵格的一些屬性以及柵格的添加和刪除。柵格可以按某個方向拆分為眾多子?xùn)鸥?,而且這個過程可以遞歸進(jìn)行。
所有柵格布局中的子視圖都將依次放入葉子?xùn)鸥竦膮^(qū)域中。
*/
@protocol MyGrid <MyGridAction>
//設(shè)置和獲取柵格的尺寸
@property(nonatomic, assign) CGFloat measure;
//得到父柵格。根柵格的父柵格為nil
@property(nonatomic, weak, readonly) id<MyGrid> superGrid;
/**
得到所有子?xùn)鸥? */
@property(nonatomic, strong, readonly) NSArray<id<MyGrid>> *subGrids;
/**
克隆出一個新柵格以及其下的所有子?xùn)鸥瘛? */
-(id<MyGrid>)cloneGrid;
/**
柵格內(nèi)子?xùn)鸥裰g的間距。
*/
@property(nonatomic, assign) CGFloat subviewSpace;
/**
柵格內(nèi)子?xùn)鸥窕蛘呷~子?xùn)鸥駜?nèi)視圖的四周內(nèi)邊距。
*/
@property(nonatomic, assign) UIEdgeInsets padding;
/**
柵格內(nèi)子?xùn)鸥窕蛘呷~子?xùn)鸥駜?nèi)視圖的對齊??糠绞?
1.對于非葉子?xùn)鸥駚碚f只能設(shè)置一個方向的??俊>唧w只能設(shè)置左中右或者上中下
2.對于葉子?xùn)鸥駚碚f,如果設(shè)置了gravity 則填充的子視圖必須要設(shè)置明確的尺寸。
*/
@property(nonatomic, assign) MyGravity gravity;
/**
占位標(biāo)志,只用葉子?xùn)鸥?,?dāng)設(shè)置為YES時則表示這個格子只用于占位,子視圖不能填充到這個柵格中。
*/
@property(nonatomic, assign) BOOL placeholder;
/**
錨點(diǎn)標(biāo)志,表示這個柵格雖然是非葉子?xùn)鸥?,也可以用來填充視圖。如果將非葉子?xùn)鸥竦腻^點(diǎn)標(biāo)志設(shè)置為YES,那么這個柵格也可以用來填充子視圖,一般用來當(dāng)做背景視圖使用。
*/
@property(nonatomic, assign) BOOL anchor;
/**
重疊視圖的對齊??糠绞? 對于葉子?xùn)鸥駚碚f,如果設(shè)置了gravity 則填充的子視圖必須要設(shè)置明確的尺寸
*/
@property(nonatomic, assign) MyGravity overlap;
/**頂部邊界線*/
@property(nonatomic, strong) MyBorderline *topBorderline;
/**頭部邊界線*/
@property(nonatomic, strong) MyBorderline *leadingBorderline;
/**底部邊界線*/
@property(nonatomic, strong) MyBorderline *bottomBorderline;
/**尾部邊界線*/
@property(nonatomic, strong) MyBorderline *trailingBorderline;
/**如果您不需要考慮國際化的問題則請用這個屬性設(shè)置左邊邊界線,否則用leadingBorderline*/
@property(nonatomic, strong) MyBorderline *leftBorderline;
/**如果您不需要考慮國際化的問題則請用這個屬性設(shè)置右邊邊界線,否則用trailingBorderline*/
@property(nonatomic, strong) MyBorderline *rightBorderline;
/**
添加行柵格,返回新的柵格。其中的measure可以設(shè)置如下的值:
1.大于等于1的常數(shù),表示固定尺寸。
2.大于0小于1的常數(shù),表示占用整體尺寸的比例
3.小于0大于-1的常數(shù),表示占用剩余尺寸的比例
4.MyLayoutSize.wrap 表示尺寸由子?xùn)鸥癜? 5.MyLayoutSize.fill 表示占用柵格剩余的尺寸
*/
-(id<MyGrid>)addRow:(CGFloat)measure;
/**
添加列柵格,返回新的柵格。其中的measure可以設(shè)置如下的值:
1.大于等于1的常數(shù),表示固定尺寸。
2.大于0小于1的常數(shù),表示占用整體尺寸的比例
3.小于0大于-1的常數(shù),表示占用剩余尺寸的比例
4.MyLayoutSize.wrap 表示尺寸由子?xùn)鸥癜? 5.MyLayoutSize.fill 表示占用柵格剩余的尺寸
*/
-(id<MyGrid>)addCol:(CGFloat)measure;
//添加?xùn)鸥?,返回被添加的柵格。這個方法和下面的cloneGrid配合使用可以用來構(gòu)建那些需要重復(fù)添加?xùn)鸥竦膱鼍啊?-(id<MyGrid>)addRowGrid:(id<MyGrid>)grid;
-(id<MyGrid>)addColGrid:(id<MyGrid>)grid;
-(id<MyGrid>)addRowGrid:(id<MyGrid>)grid measure:(CGFloat)measure;
-(id<MyGrid>)addColGrid:(id<MyGrid>)grid measure:(CGFloat)measure;
//從父柵格中刪除。
-(void)removeFromSuperGrid;
//用字典的方式來構(gòu)造柵格。
@property(nonatomic, strong) NSDictionary *gridDictionary;
@end
待續(xù)未完。。。