栅格布局简介
栅格布局MyGridLayout是MyLayout布局体系里面的第八种布局。这是一种将布局约束设置和视图分离的布局方式,就像HTML中的标签元素和css样式可以进行分离表示和存储。因此栅格布局非常适合于数据内容相同但是展示样式不同的场景,展示样式可以动态配置和变化,甚至于可以从服务器进行动态下发。栅格布局还提供了一种基于JSON语法进行布局格式描述的机制来实现界面布局。
栅格布局的需求场景
- 在众多电商类比如淘宝、天猫、京东等应用的首页中都可以看到商品大都是以分类的形式进行排列展示,不同分类的商品的展示样式不同,并且同一类商品的展示样式也有可能不同。每个商品都会占用一个矩形的区域块,这些矩形区域块则总是以某种布局进行紧凑的排列组合,比如水平的排列或者垂直的排列,或者水平和垂直混合的排列。每个矩形块内的商品基本都是由主标题、付标题、图片、以及一些活动小图标组成,并且点击矩形区块内的商品时就会进入商品的详细页面中去。
这些电商应用的商品排列布局往往都不是固定按某个样式来展示的,往往都会随着时间或者节假日的变化而变化。你会发现他们之间的一个特点就是往往这些商品的数据模型都比较固定但是展示样式却千差万别,因此我们希望这些展示和布局的样式不能写死在代码中而是希望设计成一种可以动态配置的方案来解决这种问题。 - 在一些新闻类中比如早期的Zarker或者今日头条以及网易新闻iPad版本等应用中都是以卡片的形式来展示的,而且这些卡片组合有可能是每一页的样式都不一样,每一页卡片中则由多条不同的新闻按某种顺序紧凑的排列组合在一起,每一条新闻都基本由:主标题、付标题、简介、缩略图等组成。在实现这种卡片样式布局的新闻类应用时我们往往都会先设计出多种不同的展示样式模板,因为新闻的内容相同,我们只需要在不同的页面中应用不同的卡片样式模板即可。
- 有时候希望我们的应用的展示样式是可以从服务器下发来进行动态改变,从而达到灵活多样的效果。
上面的几个例子你会发现需求都有如下特点:
- 界面总是以矩形区块的形式进行有规律的排列组合。
- 每个矩形区块对应一个数据模型,并且数据模型的内容和结构相对稳定。
- 界面的布局排列不固定而是可以灵活多变的。
- 界面中的矩形区块之间总是会有边界线来进行区分和隔离。
- 用户点击这些矩形区块时往往逻辑都是比较统一的进行处理。
这些需求基本都可以通过栅格布局来实现,而且栅格布局也是一种专门解决这类问题的布局体系!
栅格布局的原理
栅格布局的理念有一部分来源于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三个不需要再继续划分的子栅格了。下面就是最终的一种栅格的划分结果:
可以看出通过对栅格的划分最终我们在显示时我们只需要将视图的内容放置到对应的不可再继续划分的栅格里面就可以了,我们将不再进行继续划分的栅格为叶子栅格。我们可以总结出这种栅格的划分法的一些特点:
- 栅格总是按照水平或者垂直的规则来划分为0到多个更小的栅格。正式因为这种划分法,每个栅格我们不需要去记录他的位置和同时记录宽度和高度,而是只要一个尺寸值就可以描述一个栅格了。
- 栅格可以划分为众多的子栅格,并且可以无限的递归划分,从而形成了一颗栅格树。我们把最后不再继续划分的栅格成为叶子栅格,定义为叶子栅格的标准是他是否可以满足用来存放显示的内容,如果某个栅格无法显示某个独立的内容则需要继续进行划分。
- 栅格的这种定义特性,使得它不适合于用来解决那些具有重叠显示效果的场景。
所以我们这里再次为栅格进行定义:所谓栅格就是一种按照特定规则进行排列以及可以进行无限划分的具有树形层次结构以及特定尺寸的矩形区域。这种栅格定义的规则隐藏了位置的概念,以及隐藏了宽高的概念,而是只用一个值就可以描述一个矩形区域的位置和尺寸。而且我们规定只有叶子栅格的区域才用来存放视图的内容。
栅格的属性
为了表征上面对于栅格的定义和描述,我们需要对栅格进行实际的定义。因此我们定义了一个栅格接口: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
待续未完。。。