开源UI界面布局框架MyLayout1.9发布

18,829 阅读26分钟

MyLayout是一套功能全面的iOS开源UI界面布局框架。它囊括了前端所有流行的界面布局技术和解决方案,同时具有如下七大特点:

  • 功能强大。它可以减少我们在开发UI界面时所花费的时间以及减少需要适配多种设备而所消耗的时间。实践表明使用MyLayout进行界面布局时可以减少几乎50%的工作量。

  • 性能优越。MyLayout内部实现是基于frame计算来完成布局的,所以同等界面下性能是AutoLayout的5倍左右,因此复杂界面选择MyLayout将是最佳实践。

  • 布局体系丰富。MyLayout提供了iOS、Android、HTML/CSS等前端中的所有流行布局实现。因此无论你之前工作在何种平台上都可以选择熟悉的布局类上手进行开发操作。MyLayout还支持从服务器进行动态布局下发的能力。

  • 系统结合紧密。MyLayout可以同时和AutoLayout技术进行结合使用,同时可以用在XIB和Storyboard中进行可视化布局,同时还支持SizeClass技术用于多设备适配处理。

  • 多语言实现。MyLayout提供了OC语言版本的实现,同时也提供了Swift语言版本的实现:TangramKit。二者的语法和使用方式相似,您可以任意选择一种语言进行代码布局。

  • 国际化支持。MyLayout支持LTR和RTL两种方向的布局,其中的RTL模式可以用来支持希伯来语系的布局方式。

  • 无版本限制。MyLayout并没有操作系统版本上的使用限制,理论上它最低甚至可以支持到iOS5.0。

下面的表格列出的是MyLayout所提供的九大布局类所实现的功能以及和其它系统的对标能力:

布局类名 功能介绍 对标功能
MyLinearLayout 线性布局
提供视图依次从上往下或者从左往右进行单行单列排列的能力
iOS:UIStackView
Android:LinearLayout
Flutter:Row、Column
SwiftUI:HStack、VStack
MyFloatLayout 浮动布局:
提供视图通过上下左右浮动停靠而进行排列布局的能力
CSS: float
MyFlowLayout 流式布局:
提供视图按垂直或者水平方向依次进行排列并且在满足特定条件(一行内的数量和尺寸值满足约定值)后会换行进行继续排列布局的能力
独有
MyFlexLayout 弹性布局:
提供一个盒内的子视图可以进行伸缩对齐和换行排列并且满足flex规约的布局能力
CSS:flexbox
MyGridLayout 栅格布局:
提供了一种基于单元格进行垂直和水平的无限拆分而进行布局的能力,栅格布局同时具有布局动态下发的能力
CSS:类似Bootstrap、Grid
MyTableLayout 表格布局:
提供基于行列控制的表格布局的能力
Android: TableLayout、GridLayout
HTML: table、tr、td
MyRelativeLayout 相对布局:
提供一种通过设置视图之间的尺寸和位置的相互依赖约束来实现布局的能力
iOS:AutoLayout
Android:RelativeLayout、PercentRelativeLayout、ConstraintLayout
MyFrameLayout 框架布局:
提供视图在父视图上某个方位进行停靠以及层叠摆放布局的能力
Android:FrameLayout
MyPathLayout 路径布局:
提供子视图的位置通过数学函数运算而进行定位排列的能力
独有
SizeClass 提供了根据屏幕尺寸和横竖屏而进行差异布局设置的能力。上述所有布局都支持SizeClass的功能 iOS:SizeClass
CSS: 类似Bootstrap

在这些众多布局类中有些布局类提供了子视图的有规律的布局排列,比如线性布局、流式布局、表格布局、浮动布局、路径布局、弹性布局、栅格布局。有些布局类则提供了通过子视图之间的约束限制来实现布局排列,比如浮动布局、相对布局、框架布局。有些布局类则需要通过多个层次嵌套来实现界面需求,比如线性布局、流式布局、表格布局、弹性布局。有些布局类则可以把界面需求拍平而只用单层排版就能实现所需功能,比如浮动布局、相对布局、栅格布局。有些布局类则可以实现一些特殊排列,比如路径布局可以根据提供的数学函数来实现视图根据特定路径曲线来进行排列展示。有些布局类则可以提供从服务器进行动态下发以及用JSON进行布局描述的能力,比如栅格布局。有些布局类则可以实现和HTML/CSS对标的能力,比如浮动布局和弹性布局。因此在实现界面需求时,我们可以灵活运用。在选择布局时我将使用布局类的优先级列出来,供大家参考:


浮动布局->流式布局->线性布局->弹性布局->栅格布局->相对布局->框架布局->表格布局->路径布局


您可以从如下地址下载这两个版本的工程DEMO:

👉OC语言版本MyLayout:  github.com/youngsoft/M… 👉Swift语言版本TangramKit: github.com/youngsoft/T…

1.9.0新特性

这次1.9.0版本的升级无论是新功能的添加、代码的重构、性能的提升都做了大量的改进,新增和改进的功能主要有:

  • 弹性布局flexbox的实现MyFlexLayout
  • 最值约束
  • 视图尺寸和位置的压缩
  • 环绕和拉伸停靠的支持
  • 拖放类MyLayoutDragger实现布局内视图的拖放
  • iOS13的黑白模式的适配支持
  • 流式布局自定义行内对齐
  • 流式布局和浮动布局对基线对齐的支持
  • 重构和添加了对布局视图进行布局时的动画支持能力
  • 完善和扩充对布局和视图尺寸自适应设置支持
  • 重构了流式布局和相对布局的实现,提升了所有布局的性能
  • 修复了线上的BUG

下面是新版本的上述功能的详细介绍:

1. 弹性布局MyFlexLayout

flexbox是目前Web前端比较流行的布局框架。它提供了一种在一个盒子内子视图依次排列并可以进行换行排列和进行拉伸和压缩的功能。目前也有很多将flexbox移植到native客户端的解决方案。当然flexbox也有一定的缺陷:比如不支持重叠覆盖、不支持相对间距、不支持行和列间距的统一设置、不支持不规则排列等等问题。

在以前的版本中流式布局MyFlowLayout就可以实现flexbox的大多数特性并且在此基础上进行了更多复杂功能的扩展。因为其语法和设置方式和flexbox不兼容,因此对于flexbox的喜爱者来说是增加了学习和使用的成本。而这次的新版本则提供了一个新的布局类:弹性布局MyFlexLayout:

/*
 * 弹性布局是为了兼容flexbox语法而建立了一个布局,它是从MyFlowLayout派生。在MyFlowLayout中也是支持类似flexbox的一些特性的
 * 因为它的属性和flexbox不兼容,所以提供一个新的类MyFlexLayout来完全支持flexbox.
 */
@interface MyFlexLayout:MyFlowLayout


/**
 用于弹盒布局视图自身的布局设置
 */
@property(nonatomic, strong, readonly) id<MyFlexBox> myFlex;

@end

从上面的类定义中可以看出这个布局类是从流式布局MyFlowLayout类派生,我们可以通过类中的myFlex属性来进行弹性布局视图的相关属性设置。myFlex中提供了链式语法以及属性设置语法两种操作形式,您可以选择喜欢的方式来操作和使用弹性布局。下面是属性myFlex的接口MyFlexBox的详细定义:

@protocol MyFlexBox <MyFlexItem>

@property(nonatomic, strong) id<MyFlexBoxAttrs> attrs;

/**
 设置或检索伸缩盒对象的子元素在父容器中的位置。默认值:MyFlexDirection_Row
 */
-(id<MyFlexBox> (^)(MyFlexDirection))flex_direction;
/**
 设置或检索伸缩盒对象的子元素超出父容器时是否换行。默认值:MyFlexWrap_NoWrap
 */
-(id<MyFlexBox> (^)(MyFlexWrap))flex_wrap;
/**
 同时设置检索伸缩盒对象的子元素在父容器中的位置和伸缩盒对象的子元素超出父容器时是否换行。二者通过 | 运算进行组合
 */
-(id<MyFlexBox> (^)(int))flex_flow;
/**
 设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式。可选值为:MyFlexGravity_Flex_Start | MyFlexGravity_Flex_End | MyFlexGravity_Center | MyFlexGravity_Space_Between | MyFlexGravity_Space_Around 中的一个,默认值为MyFlexGravity_Flex_Start
 */
-(id<MyFlexBox> (^)(MyFlexGravity))justify_content;
/**
 设置或检索弹性盒子元素在侧轴(纵轴)方向上的对齐方式。可选值为:MyFlexGravity_Flex_Start | MyFlexGravity_Flex_End | MyFlexGravity_Center | MyFlexGravity_Baseline | MyFlexGravity_Stretch中的一个,默认值为MyFlexGravity_Flex_Start
 */
-(id<MyFlexBox> (^)(MyFlexGravity))align_items;
/**
 设置或检索弹性盒堆叠伸缩行的对齐方式。可选值为:MyFlexGravity_Flex_Start | MyFlexGravity_Flex_End | MyFlexGravity_Center | MyFlexGravity_Between | MyFlexGravity_Around | MyFlexGravity_Stretch中的一个,默认值为MyFlexGravity_Stretch
 */
-(id<MyFlexBox> (^)(MyFlexGravity))align_content;


/**
 指定主轴的子条目的数量。只有在flex_wrap设置为wrap时才有效。默认值是0表示会根据条目的尺寸自动进行换行。
 */
-(id<MyFlexBox> (^)(NSInteger))item_size;
/**
 指定布局视图中每页的条目数量。这个值必须是item_size的倍数。
 */
-(id<MyFlexBox> (^)(NSInteger))page_size;
/**
 指定布局会根据条目的尺寸自动排列,默认值是NO。
 */
-(id<MyFlexBox> (^)(BOOL))auto_arrange;


/**
 设置弹性盒的内边距
 */
-(id<MyFlexBox> (^)(UIEdgeInsets))padding;
/**
 设置弹性盒内所有条目视图之间的垂直间距
 */
-(id<MyFlexBox> (^)(CGFloat))vert_space;
/**
 设置弹性盒内所有条目视图之间的水平间距
 */
-(id<MyFlexBox> (^)(CGFloat))horz_space;

@end

而对于弹性盒视图中的条目子视图(item)来说则可以通过UIView的一个分类扩展提供的myFlex进行属性设置:

@interface UIView(MyFlexLayout)

/**
 用于弹盒视图中的子视图的布局设置。
 */
@property(nonatomic, strong, readonly) id<MyFlexItem> myFlex;

@end

条目视图的myFlex属性的实现接口:MyFlexItem的定义如下:

@protocol MyFlexItem

@property(nonatomic, strong, readonly) id<MyFlexItemAttrs> attrs;
@property(nonatomic, weak, readonly) __kindof UIView *view;


/**
 视图的宽度设置,如果宽度设置为大于0小于1则表明是相对于父视图宽度的比重值,如果是MyLayoutSize.wrap则表明宽度自适应,如果是MyLayoutSize.fill则表明宽度和父视图相等,如果是MyLayoutSize.empty则表明不设置宽度值。 其他的值就是一个固定宽度值。
 */
-(id<MyFlexItem> (^)(CGFloat))width;


/**
 视图的宽度设置,percent表明占用父视图宽度的百分比值,inc表明在百分比值的基础上的增量值。
 */
-(id<MyFlexItem> (^)(CGFloat percent, CGFloat inc))width_percent;


/**
 最小宽度限制设置
 */
-(id<MyFlexItem> (^)(CGFloat))min_width;

/**
 最大宽度限制设置
 */
-(id<MyFlexItem> (^)(CGFloat))max_width;
/**
 视图的高度设置,如果高度设置为大于0小于1则表明是相对于父视图高度的比重值,如果是MyLayoutSize.wrap则表明高度自适应,如果是MyLayoutSize.fill则表明高度和父视图相等,如果是MyLayoutSize.empty则表明不设置高度值,其他的值就是一个固定高度值。
 */
-(id<MyFlexItem> (^)(CGFloat))height;

/**
 视图的高度设置,percent表明占用父视图高度的百分比值,inc表明在百分比值的基础上的增量值。
 */
-(id<MyFlexItem> (^)(CGFloat percent, CGFloat inc))height_percent;


/**
 最小高度限制设置
 */
-(id<MyFlexItem> (^)(CGFloat))min_height;

/**
 最大高度限制设置
 */
-(id<MyFlexItem> (^)(CGFloat))max_height;

//视图的外间距设置。
/**
 视图的顶部外间距设置
 */
-(id<MyFlexItem> (^)(CGFloat))margin_top;
/**
 视图的底部外间距设置
 */
-(id<MyFlexItem> (^)(CGFloat))margin_bottom;
/**
 视图的左边外间距设置
 */
-(id<MyFlexItem> (^)(CGFloat))margin_left;
/**
 视图的右边外间距设置
 */
-(id<MyFlexItem> (^)(CGFloat))margin_right;
/**
 视图的四周外间距设置
 */
-(id<MyFlexItem> (^)(CGFloat))margin;
/**
 视图的可视设置
 */
-(id<MyFlexItem> (^)(MyVisibility))visibility;


//添加到父视图中
-(__kindof UIView* (^)(UIView*))addTo;

//添加子视图
-(id<MyFlexItem> (^)(UIView*))add;

/**
 条目在弹盒中的排列顺序,值越大越往后排。
 */
-(id<MyFlexItem> (^)(NSInteger))order;
/**
 设置或检索弹性盒的扩展比率。默认值为0表示不扩展
 */
-(id<MyFlexItem> (^)(CGFloat))flex_grow;
/**
 设置或检索弹性盒的收缩比率。默认值为1表示当条目尺寸超过弹性盒尺寸后会进行压缩。值越大压缩比越大
 */
-(id<MyFlexItem> (^)(CGFloat))flex_shrink;
/**
 设置或检索弹性盒伸缩基准值。默认值为MyFlex_Auto表示由其他属性决定,如果值为大于0小于1则表示相对值,其他为一个固定的尺寸值。
 */
-(id<MyFlexItem> (^)(CGFloat))flex_basis;
/**
 设置或检索弹性盒子元素自身在侧轴(纵轴)方向上的对齐方式。可选值为:MyFlexGravity_Flex_Start | MyFlexGravity_Flex_End | MyFlexGravity_Center | MyFlexGravity_Baseline | MyFlexGravity_Stretch中的一个,默认值为MyFlex_Auto
 */
-(id<MyFlexItem> (^)(MyFlexGravity))align_self;

@end

从上面的定义中可以看出因为其设置和使用方法都和flexbox规约几乎保持一致,因此对于熟悉flexbox的人来说使用几乎是零成本。比如我们用MyFlexLayout来实现下面这个界面:

弹性盒示例

代码实现如下:

-(void)viewDidLoad{
    [super viewDidLoad];

   //用链式语法创建一个弹性布局,宽度和父视图一致,高度自适应
   MyFlexLayout *layout = MyFlexLayout.new.myFlex
  .flex_direction(MyFlexDirection_Row)
  .flex_wrap(MyFlexWrap_Wrap)
  .align_content(MyFlexGravity_Center)
  .align_items(MyFlexGravity_Flex_End)
  .vert_space(10)
  .horz_space(10)
  .padding(UIEdgeInsetsMake(10, 10, 10, 10))
  .marign_top(50)
  .width(MyLayoutSize.fill)
  .height(MyLayoutSize.wrap)
  .addTo(self.view);


 UILabel *itemA = UILabel.new.myFlex
.width(MyLayoutSize.fill)
.height(30)
.addTo(layout);

UILabel *itemB = UILabel.new.myFlex
.flex_grow(1)
.align_self(MyFlexGravity_Flex_Start)
.height(30)
.addTo(layout);

UILabel *itemC = UILabel.new.myFlex
.flex_grow(1)
.height(40)
.addTo(layout);

UILabel *itemD = UILabel.new.myFlex
.flex_grow(1)
.height(50)
.addTo(layout);

layout.backgroundColor = [UIColor grayColor];
itemA.text = @"A";
itemA.backgroundColor = [UIColor redColor];
itemB.text = @"B";
itemB.backgroundColor = [UIColor greenColor];
itemC.text = @"C";
itemC.backgroundColor = [UIColor blueColor];
itemD.text = @"D";
itemD.backgroundColor = [UIColor yellowColor];
}

除了使用链式语法进行布局和条目样式设置外,还可以直接通过属性赋值来进行样式设置。您可以通过MyFlexBox中的attrs以及MyFlexItem中的attrs这两个数据成员来以属性值的形式进行布局的和条目的样式设置。为了更好的演示MyFlexLayout的使用,我在MyLayout的Demo工程中建立了一个Flex布局(FlexLayout)。您可以在那里看到弹性布局相关的所有操作。

2.最值约束

👉设想一个场景:某个视图的宽度在竖屏下是屏幕宽度的一半,而在横屏下则是屏幕高度的一半。换句话说就是视图的宽度是屏幕宽度和高度中的最小值的一半。 👉再设想一个场景:某个视图的右边位置希望跟另外两个视图中最靠右的那个位置对齐,换句话说就是视图的右边位置是另外两个视图右边位置的最大值。

我们称这种某个视图的位置或者尺寸是一个位置集合或者尺寸集合中的最大值或者最小值的约束为最值约束。用表达式如下:

位置 = MAX(位置1,位置2,位置3,...) 或者 位置 = MIN(位置1,位置2,位置3,...)
尺寸 = MAX(尺寸1,尺寸2,尺寸3,...)或者 尺寸 = MIN(尺寸1,尺寸2,尺寸3,...)

位置最值约束

MyLayout为了实现对位置最值的支持,在数组类NSArray上建立了一个扩展分类:

//位置最值扩展分类
@interface NSArray(MyLayoutMostPos)
//从数组中得到最小的位置值。要求数组的元素必须是MyLayoutPos或者NSNumber类型
@property(nonatomic, readonly) MyLayoutMostPos *myMinPos;
//从数组中得到最小的位置值。要求数组的元素必须是MyLayoutPos或者NSNumber类型
@property(nonatomic, readonly) MyLayoutMostPos *myMaxPos;

@end

我们可以通过数组中的myMinPos和myMaxPos两个只读属性来分别获取最小值和最大值的最值对象,获取位置最值对象时要求数组中的元素只能是NSNumber以及MyLayoutPos类的实例对象,它表明最值是这些具体数字或者位置对象中的最大或者最小值。比如下面的代码:

//A视图的左边位置是B视图左边位置,C视图右边位置,100这三个值中的最小的一个
A.leftPos.equalTo(@[B.leftPos, C.rightPos, @100].myMinPos);
//A视图的垂直居中位置是B视图顶部位置、100、C视图底部位置这三个值中的最大一个。
A.centerYPos.equalTo(@[B.topPos, @100, C.bottomPos].myMaxPos);

//A视图的左边位置是B视图左边位置+20、C视图右边位置-20 这两个位置中的最大一个。
A.leftPos.equalTo(@[B.leftPos.clone(20), C.rightPos.clone(20)].myMaxPos);

在上面的最后一个例子中我们看到使用了MyLayoutPos对象的clone方法,这个方法的作用是clone一个新的对象并带上一定的偏移值。MyLayoutPos中的clone方法就是专门为最值约束使用的,主要为了解决那些获取最值时希望在某个位置的偏移的场景。

目前只有相对布局下的子视图才支持位置最值约束设置,其他布局下的子视图不支持。同时在设置位置最值约束的时候,要求数组内的元素的位置约束计算必须要在当前视图的位置约束计算之前完成,否则得到的结果将未可知。

尺寸最值

MyLayout为了实现对尺寸最值的支持,在数组类NSArray上建立了一个扩展分类:

//尺寸最值扩展分类
@interface NSArray(MyLayoutMostSize)
//从数组中得到最小的尺寸值。要求数组的元素必须是MyLayoutSize或者NSNumber类型
@property(nonatomic, readonly) MyLayoutMostSize *myMinSize;
//从数组中得到最大的尺寸值。要求数组的元素必须是MyLayoutSize或者NSNumber类型
@property(nonatomic, readonly) MyLayoutMostSize *myMaxSize;

@end

我们可以通过数组中的myMinSize和myMaxSize两个只读属性来分别获取最小值和最大值的最值对象。获取尺寸最值对象时要求数组中的元素只能是NSNumber以及MyLayoutSize类的实例对象,它表明最值是这些具体数字或者尺寸对象中的最大或者最小值。比如下面的例子:

//A视图的宽度是B视图的宽度,C视图的高度,100这三个值中的最小的一个
A.widthSize.equalTo(@[B.widthSize, C.heightSize, @100].myMinSize);
//A视图的高度是A视图自身高度,B视图高度的一半加20,100这三个值中的最大一个。
A.heightSize.equalTo(@[@(MyLayoutSize.wrap), B.heightSize.clone(20, 0.5), @100].myMaxSize);

在上面的最后一个例子中我们看到使用了MyLayoutSize对象的clone方法,这个方法的作用是clone一个新的尺寸对象并带上一定的倍数和增量值。我们还可以用一个特殊的尺寸值MyLayoutSize.wrap在最值数组中,它表明自身的尺寸也参与最值比较中。

最值尺寸约束设置,可以应用在所有布局下的视图中以及布局本身。但是在使用最值约束时,要求数组内的元素的尺寸约束计算必须要在当前视图的尺寸约束计算之前完成,否则得到的结果将未可知。

3.视图尺寸和位置的压缩

在一些场景中我们希望当所有子视图的尺寸总和超过布局视图的尺寸时为了能让所有子视图都得到完全的显示而需要对子视图的尺寸进行适当的压缩,对于位置也是如此。这时候就需要应用到视图尺寸和位置的压缩技术了。举例来说:假如一个横向的水平线性布局的宽度是120,里面的三个子视图A,B,C的宽度和间距分别为:A左间距20,A宽度30, B左间距10,B宽度60, C左间距20,C宽度40。在不进行压缩时界面显示的效果如下:

未压缩前

为了实现压缩的能力在MyLayoutSize和MyLayoutPos两个类中分别提供了一个新的属性shrink。这个属性值的意义表明当位置和尺寸超过布局视图时的压缩比重值。值越大表明被压缩的比重越大,值为0表明不会被压缩。系统默认的压缩比重值被设置为0。就以上面的例子来说假如我们分别设置视图A,B,C的宽度和间距的压缩比例值如下:

A.leftPos.equalTo(@20).shrink = 1;
A.widthSize.equalTo(@30).shrink = 1;
B.leftPos.equalTo(@10);
B.widthSize.equalTo(@50).shrink = 2;
C.leftPos.equalTo(@20).shrink = 1;
C.widthSize.equalTo(@40);

这样在不压缩的情况所有子视图的间距和宽度总和为:20+30+10+50+20+40 = 170 ,减去布局视图的宽度120后超出了50。而上述设置的压缩比重值的总和为:1+1+2+1 = 5。因此最终的每个位置和尺寸被压缩后的结果值分别为:

A的左间距 = 20 - 50 * (1/5.0) = 10
A的宽度 = 30 - 50 *(1/5.0) = 20
B的左间距 = 10  不会被压缩
B的宽度 = 50 - 50 *(2/5.0) = 30
C的左间距 = 20 - 50 *(1/5.0) = 10
C的宽度 = 40 不会被压缩

最终界面展示的效果如下:

位置和尺寸压缩后的界面

目前只有线性布局、框架布局、流式布局、表格布局、弹性布局下的子视图的宽度和尺寸才支持压缩特性,其他布局中的子视图不支持。而且压缩的特性只有在所有子视图的尺寸超出的时候才生效否则是不生效的。

需要注意的是弹性布局中的子视图的压缩特性一般不通过直接设置shrink属性来实现,而是通过设置flex_shrink来实现。

视图的压缩属性和视图的weight属性的区别是前者是用于视图尺寸的压缩,而后者则是用于视图尺寸的拉伸。具体的weight属性的使用请参考相关的文档和DEMO。

4.环绕和拉伸停靠

我们可以通过设置布局视图的gravity属性来设置布局内子视图的整体停靠和对齐特性。在新版本中为了实现flexbox中的一些能力,特别增加了4个停靠属性:

MyGravity_Horz_Around
MyGravity_Horz_Stretch
MyGravity_Vert_Around
MyGravity_Vert_Stretch

位置的拉伸和环绕

在以前的版本中如果我们希望拉伸子视图之间的间距时可以通过MyGravity_Horz_Between或者MyGravity_Vert_Between来实现。拉伸间距时第一个以及最后一个子视图离父布局视图的间距将是0,而子视图之间的间距将会平分剩余的空间。而MyGravity_Horz_Around和MyGravity_Vert_Around则是第一个和最后一个子视图离父布局视图的间距是子视图之间的间距的一半。下面的界面展示了Between和Around的区别:

位置

尺寸的拉伸和环绕

在以前的版本中如果我们希望填充拉伸所有子视图之间的尺寸来占满布局视图的尺寸时我们可以通过MyGravity_Horz_Fill或者MyGravity_Vert_Fill来实现。这两个停靠属性的功能会将布局视图中的剩余空间均匀的分配到所有子视图(设置有尺寸自适应的布局视图除外)的尺寸之上,而不管子视图是否设置了尺寸约束与否,从而实现子视图之间的尺寸拉伸效果。 而MyGravity_Horz_Stretch以及MyGravity_Vert_Stretch则效果和填充是一样的,只不过它只会拉伸那些没有设置尺寸约束的子视图以及设置了尺寸自适应的子视图(设置了尺寸自适应的布局视图除外)。下面的界面展示了Fill 和Stretch的区别:

尺寸

目前只有线性布局、流式布局、浮动布局、框架布局、弹性布局中才具有整体停靠和对齐设置的效果,其他布局不支持。

5.布局中子视图的拖放

在一些应用中我们可以通过拖放功能来调整子视图的位置或者进行一些其他处理。MyLayout以前的版本中实现了这么一个DEMO。新版本中我们将DEMO中拖放的能力进行了抽象而形成了一个新的拖放类:MyLayoutDragger。 在使用拖放类实现拖放功能时需要如下几个步骤:

  1. 从布局视图类中通过createLayoutDragger方法创建一个拖放类实例对象,并保存起来。

  2. 对添加到布局视图中的子视图分别添加如下事件:

    [可以被拖放的子视图 addTarget:self action:@selector(handleTouchDrag:withEvent:) forControlEvents:UIControlEventTouchDragInside]; //注册拖动事件。
    [可以被拖放的子视图 addTarget:self action:@selector(handleTouchDrag:withEvent:) forControlEvents:UIControlEventTouchDragOutside]; //注册外面拖动事件。
    [可以被拖放的子视图 addTarget:self action:@selector(handleTouchDown:withEvent:) forControlEvents:UIControlEventTouchDown]; //注册按下事件
    [可以被拖放的子视图 addTarget:self action:@selector(handleTouchUp:withEvent:) forControlEvents:UIControlEventTouchUpInside]; //注册抬起事件
    [可以被拖放的子视图 addTarget:self action:@selector(handleTouchUp:withEvent:) forControlEvents:UIControlEventTouchCancel]; //注册终止事件
  1. 分别在对应的事件处理方法中,调用拖放器对象的相关方法:
- (IBAction)handleTouchDown:(id)sender withEvent:(UIEvent*)event {
    //拖动子视图开始处理。
    [self.dragger dragView:sender withEvent:event];
}

- (IBAction)handleTouchUp:(id)sender withEvent:(UIEvent*)event {
    //停止子视图拖动处理。
    [self.dragger dropView:sender withEvent:event];
}

- (IBAction)handleTouchDrag:(id)sender withEvent:(UIEvent*)event {
    //子视图拖动中处理。
    [self.dragger dragginView:sender withEvent:event];
}

这样就可以自动实现对子视图的拖放功能了。我们还可以通过拖放器对象来进行一些特性化设置,比如可以设置拖放的动画时长、可以设置哪些子视图在拖放时不会移动、以及是否可以在拖放时实现悬停效果等等。具体的演示代码请参考DEMO工程中的:FLLTest3ViewController

6.iOS13的黑白模式适配

iOS13以后提供了黑白模式适配的能力。对于MyLayout来说因为具有对边界线的支持的能力,边界线内部实现是采用的CALayer来实现,而CALayer对颜色的输入是CGColorRef对象,因此为了支持黑白模式适配也进行版本升级,以便让边界线也能实现黑白模式适配的能力。

7.流式布局的行内对齐控制

在流式布局中我们可以通过设置gravity属性和arrangedGravity属性来设置布局内子视图的整体停靠特性以及行内子视图之间的对齐特性。然而在实际中我们可能希望某些行的停靠对齐属性和其他行是不一样的,也就是希望能够定制每行的停靠对齐属性。这样通过行的停靠对齐属性就可以不通过插入占位视图或者不需要进行多层嵌套来实现我们的界面需求。(如果用线性布局来实现多行多列则需要进行多个布局层次的嵌套处理)。就比如下面的这个界面:

流式布局的行对齐自定义

为了支持行内对齐停靠自定义处理,流式布局提供了一个新的属性:

/**
 单独为某一行定制的水平和垂直停靠对齐属性,默认情况下布局视图的gravity和arrangedGravity作用于所有行以及行内的停靠对齐。如果你想单独定制某一行的停靠对齐方式时
 可以通过设置这个block属性。
 lineGravity的入参分别是布局对象、当前行的索引(0开始)、当前行的条目视图数量、是否是最后一行四个参数。
 函数返回的是此行以及行内的停靠对齐方式,如果返回MyGravity_None则表示使用布局默认的gravity和arrangedGravity停靠对齐属性。
 */
@property(nonatomic, copy) MyGravity (^lineGravity)(MyFlowLayout *layout, NSInteger lineIndex, NSInteger itemCount, BOOL isLastLine);

我们可以通过这个block的形式的属性来进行行内停靠对齐的自定义处理。具体的行内对齐停靠的使用可以参考DEMO工程中的FLLTest4ViewControllerFLLTest9ViewController

8.流式布局和浮动布局对基线对齐的支持

新版本中对于垂直流式布局以及垂直浮动布局中的每一行子视图之间新增加了对基线对齐的支持。你可以通过设置流式布局的arrangedGravity的值为MyGravity_Vert_Baseline。以及设置浮动布局的gravity的值为MyGravity_Vert_Baseline来实现行内的基线对齐。其中基线的标准视图是行内的第一个文本视图。这样整个布局体系中水平线性布局、相对布局、垂直流式布局、垂直浮动布局、弹性布局都可以实现行内基线对齐的能力了。

9.布局动画的支持和扩展

动画的适当使用会增强用户的体验效果。MyLayout中如果我们调整了子视图的约束后希望有动画效果,那么可以调用布局视图的方法:

/**
 *设置布局时的动画。并指定时间,选项,和完成时的处理,这个动画只会在调用后的下次布局时执行一次。
 @param duration 指定动画的时间间隔
 */
-(void)layoutAnimationWithDuration:(NSTimeInterval)duration;
-(void)layoutAnimationWithDuration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion;

上述的方法调用后系统会在下一个布局周期发生时自动执行动画效果。在使用动画方法时我们可以指定动画的时长以及一些选项还有动画完成后的回调处理。

10.完善和扩充视图尺寸的自适应设置支持

所谓尺寸自适应就是视图的尺寸根据自身的内容和视图内的子视图的尺寸来动态确定自身的尺寸,从而形成所谓的包裹的效果。尺寸自适应的目的是为了让视图中的所有内容都得到完全的展示。

老版本的尺寸自适应设置

视图的自适应尺寸也算是一种特殊的尺寸。在老版本中如果我们想让某个视图的宽度自适应时可以通过设置wrapContentWidth 属性为YES即可,而让视图的高度自适应时则可以通过设置wrapContentHeight属性为YES即可。而要设置视图的具体尺寸时则需要通过widthSize或者heightSize来实现。为了设置尺寸而分别使用两个属性来操作这是不合理的方式。因此新版本中不再建议使用wrapContentWidth和wrapContentHeight以及wrapContentSize来设置尺寸自适应了,而是建议使用新的设置方式。

新版本的尺寸自适应设置

新版本中将尺寸的自适应设置合并到了widthSize和heightSize中。因为自适应也是一种尺寸值,只不过是特殊值。下面的代码是老版本和新版本的设置方法:

//老的方法
A.wrapContentWidth = YES;  
//新的方法1
A.widthSize.equalTo(@(MyLayoutSize.wrap));
//新的方法2
A.myWidth = MyLayoutSize.wrap;

//老的方法:
B.wrapContentSize = YES;
//新的方法:
B.mySize = CGSizeMake(MyLayoutSize.wrap, MyLayoutSize.wrap);

//老的读取和判断的方法
if (A.wrapContentWidth) {}
//新的判断和读取的方法1
if (A.widthSize.isWrap){}
//新的判断和读取的方法2
if (A.myWidth == MyLayoutSize.wrap){}

在新版本中我们除了可以设置MyLayoutSize.wrap为尺寸自适应外,在MyLayoutSize类中还定义了另外两个类属性:MyLayoutSize.fill和MyLayoutSize.empty。它们也可以用来简化尺寸的设置。

  • MyLayoutSize.wrap:代表尺寸自适应
  • MyLayoutSize.fill: 代表尺寸占用父视图的剩余空间
  • MyLayoutSize.empty: 代表清除尺寸约束

比如下面的代码是等价的:

A.widthSize.equalTo(@(MyLayoutSize.wrap)) <==> A.myWidth = MyLayoutSize.wrap;
A.widthSize.equalTo(A.superview.widthSize) <==> A.myWidth = MyLayoutSize.fill;
A.widthSize.equalTo(nil) <==> A.myWidth = MyLayoutSize.empty;

结束语


由于语法篇幅MyLayout中的很多功能都没有介绍到,如果您想进一步了解的话可以到github中下载对应的工程demo来进行详细了解。

👉MyLayout: github.com/youngsoft/M…

👉TangramKit: github.com/youngsoft/T…

👉我的掘金主页: juejin.cn/user/852876…