iOS的一種基於伺服器下發的動態佈局方案(一)

歐陽大哥2013發表於2018-10-24

柵格佈局簡介

柵格佈局MyGridLayoutMyLayout佈局體系裡面的第八種佈局。這是一種將佈局約束設定和檢視分離的佈局方式,就像HTML中的標籤元素和css樣式可以進行分離表示和儲存。因此柵格佈局非常適合於資料內容相同但是展示樣式不同的場景,展示樣式可以動態配置和變化,甚至於可以從伺服器進行動態下發。柵格佈局還提供了一種基於JSON語法進行佈局格式描述的機制來實現介面佈局。

柵格佈局的需求場景

  • 在眾多電商類比如淘寶、天貓、京東等應用的首頁中都可以看到商品大都是以分類的形式進行排列展示,不同分類的商品的展示樣式不同,並且同一類商品的展示樣式也有可能不同。每個商品都會佔用一個矩形的區域塊,這些矩形區域塊則總是以某種佈局進行緊湊的排列組合,比如水平的排列或者垂直的排列,或者水平和垂直混合的排列。每個矩形塊內的商品基本都是由主標題、付標題、圖片、以及一些活動小圖示組成,並且點選矩形區塊內的商品時就會進入商品的詳細頁面中去。 這些電商應用的商品排列布局往往都不是固定按某個樣式來展示的,往往都會隨著時間或者節假日的變化而變化。你會發現他們之間的一個特點就是往往這些商品的資料模型都比較固定但是展示樣式卻千差萬別,因此我們希望這些展示和佈局的樣式不能寫死在程式碼中而是希望設計成一種可以動態配置的方案來解決這種問題。
  • 在一些新聞類中比如早期的Zarker或者今日頭條以及網易新聞iPad版本等應用中都是以卡片的形式來展示的,而且這些卡片組合有可能是每一頁的樣式都不一樣,每一頁卡片中則由多條不同的新聞按某種順序緊湊的排列組合在一起,每一條新聞都基本由:主標題、付標題、簡介、縮圖等組成。在實現這種卡片樣式佈局的新聞類應用時我們往往都會先設計出多種不同的展示樣式模板,因為新聞的內容相同,我們只需要在不同的頁面中應用不同的卡片樣式模板即可。
  • 有時候希望我們的應用的展示樣式是可以從伺服器下發來進行動態改變,從而達到靈活多樣的效果。

上面的幾個例子你會發現需求都有如下特點:

  1. 介面總是以矩形區塊的形式進行有規律的排列組合。
  2. 每個矩形區塊對應一個資料模型,並且資料模型的內容和結構相對穩定。
  3. 介面的佈局排列不固定而是可以靈活多變的。
  4. 介面中的矩形區塊之間總是會有邊界線來進行區分和隔離。
  5. 使用者點選這些矩形區塊時往往邏輯都是比較統一的進行處理。

這些需求基本都可以通過柵格佈局來實現,而且柵格佈局也是一種專門解決這類問題的佈局體系!

柵格佈局的原理

柵格佈局的理念有一部分來源於bootstrap中的功能,以及借鑑了HTML中css和標籤元素分離的思想。在柵格佈局中所有檢視不需要進行任何佈局排列相關的約束設定,檢視只負責內容、顏色、字型等相關屬性的設定,而柵格則負責位置和尺寸對齊以及邊界線相關的屬性的設定。這樣就將內容和佈局進行了徹底的分離,而正是這種分離的機制才使得我們可以完成動態的位置和尺寸的調整。那麼什麼是柵格呢?

柵格其實就是一個格子、一個矩形區域

我們來考察一個UIWindow以及一個UIView。發現他們其實都只是一個抽象的矩形區域。任何一個矩形區域都有位置和尺寸的概念,位置和尺寸則是通過提供的frame屬性來描述和實現的。我們的介面由很多的檢視組成,從佈局的觀點來說,我們的介面其實就是由多個矩形區域來組成,而所謂的佈局其實就是分別設定每個矩形區域的位置和尺寸。當位置和尺寸設定好後,我們只需要在對應的矩形區域內填充內容就可以了,然後系統再分別渲染每個矩形區域內的內容,這樣就呈現出了我們的介面了。

為了達到我們的內容和佈局分離的目的,就需要將矩形區域進行抽象和處理,因為我們就將一個矩形區域定義為一個柵格。那麼是不是說一個柵格就能滿足條件呢?答案是否定的,既然上面說了我們的介面是由多個矩形區域組成,那麼同樣的在一個柵格佈局中也應該是由多個柵格組成。如何來對柵格進行拆分,柵格和柵格之間的關係又是如何的?以及如何用柵格來描述一個介面呢?這裡我們可以參考一下下面的一個介面:

介面效果圖

在這個介面中,那麼我們首先可以將整個介面的矩形區域當做為一個柵格G。而這個介面又可以看做是由上、中、下三個矩形區域組成,因此我們可以將柵格G垂直的從上到下劃分為A,B,C三個子柵格。A,B,C三個柵格還可以繼續進行劃分,其中A柵格區域又可以水平的從左到右劃分為A1,A2,A3三個子柵格,這樣劃分後A1,A2,A3三個子柵格里面就可以對應填充具體的內容了,我們將沒有必要再對A1,A2,A3進行繼續劃分了。同樣的B柵格則可以水平的劃分為從左到右的B1,B2兩個柵格,B1柵格里面可以填充具體的內容了,因此不需要進一步劃分,而B2柵格我們還需要繼續進行垂直的從上到下劃分為B21,B22兩個柵格,這次劃分後將不需要再次進行劃分了;同理C柵格我們也可以同A柵格一樣水平的從左到右依次的劃分為C1,C2,C2三個不需要再繼續劃分的子柵格了。下面就是最終的一種柵格的劃分結果:

可以看出通過對柵格的劃分最終我們在顯示時我們只需要將檢視的內容放置到對應的不可再繼續劃分的柵格里面就可以了,我們將不再進行繼續劃分的柵格為葉子柵格。我們可以總結出這種柵格的劃分法的一些特點:

  1. 柵格總是按照水平或者垂直的規則來劃分為0到多個更小的柵格。正式因為這種劃分法,每個柵格我們不需要去記錄他的位置和同時記錄寬度和高度,而是隻要一個尺寸值就可以描述一個柵格了。
  2. 柵格可以劃分為眾多的子柵格,並且可以無限的遞迴劃分,從而形成了一顆柵格樹。我們把最後不再繼續劃分的柵格成為葉子柵格,定義為葉子柵格的標準是他是否可以滿足用來存放顯示的內容,如果某個柵格無法顯示某個獨立的內容則需要繼續進行劃分。
  3. 柵格的這種定義特性,使得它不適合於用來解決那些具有重疊顯示效果的場景。

所以我們這裡再次為柵格進行定義:所謂柵格就是一種按照特定規則進行排列以及可以進行無限劃分的具有樹形層次結構以及特定尺寸的矩形區域。這種柵格定義的規則隱藏了位置的概念,以及隱藏了寬高的概念,而是隻用一個值就可以描述一個矩形區域的位置和尺寸。而且我們規定只有葉子柵格的區域才用來存放檢視的內容。

柵格的屬性

為了表徵上面對於柵格的定義和描述,我們需要對柵格進行實際的定義。因此我們定義了一個柵格介面:MyGrid, 這個介面的定義在MyGrid.h中能夠看到。下面我們就來具體介紹一下這個介面。

柵格的動作和事件處理機制

我們使用柵格除了希望能夠顯示內容外,還希望其能提供響應事件處理邏輯,比如使用者觸控某個柵格時,希望柵格能夠做出迴應,同時還希望柵格進行事件處理時還能使用柵格中儲存的附加資料。除此之外我們還希望柵格具有能夠唯一標識自己的屬性或者某些柵格具有相同的屬性。因此我們將柵格對事件的響應處理能力進行抽象而構建了一個柵格的基介面MyGridAction。

/**
 柵格動作介面,您可以觸控柵格來執行特定的動作和事件。
 */
@protocol MyGridAction<NSObject>


/**
 柵格的標籤標識,用於在事件中區分柵格。
 */
@property(nonatomic)  NSInteger tag;


/**
 柵格的動作資料,這個資料是柵格的擴充套件資料,您可以在動作中使用這個附加的資料來進行一系列操作。他可以是一個數值,也可以是個字串,甚至可以是一段JS指令碼。
 */
@property(nonatomic, strong) id actionData;



/**
 設定柵格的事件,如果取消柵格事件則設定target為nil

 @param target action事件的呼叫者
 @param action action事件,格式為:-(void)handle:(id<MyGrid>)sender
 */
-(void)setTarget:(id)target action:(SEL)action;

@end

複製程式碼

在上面的柵格動作定義中我們可以看到tag屬性用來對柵格進行標識和進行分類;setTarget:action:方法則可以為柵格設定使用者觸控柵格時的響應邏輯;actionData則是可以設定附加在柵格上的任意資料,具體的資料的意義是由使用者進行定義的,因此它可以是一個URL,也可以是一個字串,甚至可以是一段JS指令碼。因為我們對柵格佈局的定位是可以基於伺服器下發的動態佈局解決方案。因此我們希望除了介面佈局能支援動態化外,還希望我們的業務邏輯也可以一定程度的動態化(要完成實現業務邏輯動態化實際中是沒有那麼簡單的,而且蘋果也是不允許業務邏輯能夠在不稽核的前提下進行更新處理)。因此我們可以藉助actionData中的資料來支援柵格佈局的一部分業務邏輯的動態化的能力。比如下面的程式碼:


id<MyGrid> gird = //這裡假設某處獲取了柵格,並且柵格的定義資料是從伺服器動態下發的(包括actionData)。

[grid setTarget:self action:@selector(handleAction:)];

..........

-(void)handleAction:(id<MyGrid>)grid
{
      if (grid.tag == xxx)
     {  //假設tag為xxx時actionData的值是URL

          構建一個UIWebView,然後將actionData的值傳遞給UIWebview
     }
     else if (grid.tag == yyy)
    {//假設tag為yyy時actionData的值是一段JS指令碼
         構建一個JSContext物件,並執行actionData所描述的指令碼。
    }
    else
   {
        //..其他型別的資料處理。
   }
}

複製程式碼
柵格的基本屬性

上面曾經介紹過柵格其實是一個特定尺寸的矩形區域,而且柵格是一顆具有父子關係的樹形資料結構。下面就是柵格介面的具體定義,可以看出他是從MyGridAction介面派生

/**
 柵格協議。用來描述柵格矩形區域,所以一個柵格就是一個矩形區域。 這個介面用來描述柵格的一些屬性以及柵格的新增和刪除。柵格可以按某個方向拆分為眾多子柵格,而且這個過程可以遞迴進行。
 所有柵格佈局中的子檢視都將依次放入葉子柵格的區域中。
 */
@protocol MyGrid <MyGridAction>


//設定和獲取柵格的尺寸
@property(nonatomic, assign) CGFloat measure;


//得到父柵格。根柵格的父柵格為nil
@property(nonatomic, weak, readonly) id<MyGrid> superGrid;


/**
 得到所有子柵格
 */
@property(nonatomic, strong, readonly) NSArray<id<MyGrid>> *subGrids;


/**
 克隆出一個新柵格以及其下的所有子柵格。
 */
-(id<MyGrid>)cloneGrid;


/**
 柵格內子柵格之間的間距。
 */
@property(nonatomic, assign) CGFloat subviewSpace;

/**
 柵格內子柵格或者葉子柵格內檢視的四周內邊距。
 */
@property(nonatomic, assign) UIEdgeInsets padding;


/**
 柵格內子柵格或者葉子柵格內檢視的對齊停靠方式.
 
 1.對於非葉子柵格來說只能設定一個方向的停靠。具體只能設定左中右或者上中下
 2.對於葉子柵格來說,如果設定了gravity 則填充的子檢視必須要設定明確的尺寸。
 */
@property(nonatomic, assign) MyGravity gravity;

/**
 佔位標誌,只用葉子柵格,當設定為YES時則表示這個格子只用於佔位,子檢視不能填充到這個柵格中。
 */
@property(nonatomic, assign) BOOL placeholder;


/**
  錨點標誌,表示這個柵格雖然是非葉子柵格,也可以用來填充檢視。如果將非葉子柵格的錨點標誌設定為YES,那麼這個柵格也可以用來填充子檢視,一般用來當做背景檢視使用。
 */
@property(nonatomic, assign) BOOL anchor;

/**
 重疊檢視的對齊停靠方式
 對於葉子柵格來說,如果設定了gravity 則填充的子檢視必須要設定明確的尺寸
 */
@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;

/**如果您不需要考慮國際化的問題則請用這個屬性設定左邊邊界線,否則用leadingBorderline*/
@property(nonatomic, strong) MyBorderline *leftBorderline;
/**如果您不需要考慮國際化的問題則請用這個屬性設定右邊邊界線,否則用trailingBorderline*/
@property(nonatomic, strong) MyBorderline *rightBorderline;



/**
 新增行柵格,返回新的柵格。其中的measure可以設定如下的值:
 1.大於等於1的常數,表示固定尺寸。
 2.大於0小於1的常數,表示佔用整體尺寸的比例
 3.小於0大於-1的常數,表示佔用剩餘尺寸的比例
 4.MyLayoutSize.wrap 表示尺寸由子柵格包裹
 5.MyLayoutSize.fill 表示佔用柵格剩餘的尺寸
 */
-(id<MyGrid>)addRow:(CGFloat)measure;

/**
 新增列柵格,返回新的柵格。其中的measure可以設定如下的值:
 1.大於等於1的常數,表示固定尺寸。
 2.大於0小於1的常數,表示佔用整體尺寸的比例
 3.小於0大於-1的常數,表示佔用剩餘尺寸的比例
 4.MyLayoutSize.wrap 表示尺寸由子柵格包裹
 5.MyLayoutSize.fill 表示佔用柵格剩餘的尺寸
 */
-(id<MyGrid>)addCol:(CGFloat)measure;

//新增柵格,返回被新增的柵格。這個方法和下面的cloneGrid配合使用可以用來構建那些需要重複新增柵格的場景。
-(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;


//用字典的方式來構造柵格。
@property(nonatomic, strong) NSDictionary *gridDictionary;

@end

複製程式碼

待續未完。。。


歡迎大家訪問我的github地址簡書地址

相關文章