IOS 自动布局-UIStackPanel和UIGridPanel(二)
在上一篇中我提到了如何使用stackpanel和gridpanel来实现自动布局。而在这一篇中我着重讲解下其中的原理。
在(UIPanel UIStackPanel UIGridPanel)中主要是使用了NSLayoutConstraint这个类来实现的,因此为了看懂下面的代码请务必先了解NSLayoutConstraint的使用方法。
先考虑下这样一个场景,现在有一个自上而下垂直的布局,水平方向的宽度跟屏幕分辨率的宽度保持一致,垂直方向高度不变,各个视图间的间距不变,在用户切换横屏和竖屏的时候只有视图的宽度是改变的,而高度和视图间的间距不变。这样一个场景也能模拟我们的应用在不同分辨率上适配。
针对上面这个场景,那么我们势必要给UIView两个属性,就是描述UIView高宽和UIView之间间距的属性,这里定义为size和margin属性,size的类型是CGSize,而margin的数据类型是UIEdgeInsets(描述该UIView的四个方向的间距)。这两个属性是以扩展属性实现的。
代码如下:
@interface UIView(UIPanelSubView)
//设置view的大小
@property (nonatomic,assign)CGSize size;
//view距离左上右下的间距
@property (nonatomic,assign)UIEdgeInsets margin;
@end
既然有了这两个属性,那么意味着只要我修改了两个属性的任何一个属性,都能实时的改变UIView的外观,那么我们这里就需要有一个方法来充值UIView的实现,这里添加一个方法resetConstraints,用来重置约束。
这样完整的class定义是这样的
@interface UIView(UIPanelSubView)
//设置view的大小
@property (nonatomic,assign)CGSize size;
//view距离左上右下的间距
@property (nonatomic,assign)UIEdgeInsets margin;
//重置约束
-(void)resetConstraints;
@end
完整的实现代码如下:
@implementation UIView(UIPanelSubView)
char* const uiviewSize_str = "UIViewSize";
-(void)setSize:(CGSize)size{
objc_setAssociatedObject(self, uiviewSize_str, NSStringFromCGSize(size), OBJC_ASSOCIATION_RETAIN);
//先将原来的高宽约束删除
for(NSLayoutConstraint *l in self.constraints){
switch (l.firstAttribute) {
case NSLayoutAttributeHeight:
case NSLayoutAttributeWidth:
[self removeConstraint:l];
break;
default:
break;
}
}
//添加高度约束
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.height]];
//添加宽度约束
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.width]];
} -(CGSize)size{
return CGSizeFromString(objc_getAssociatedObject(self, uiviewSize_str));
} char* const uiviewMargin_str = "UIViewMargin";
-(void)setMargin:(UIEdgeInsets)margin{
objc_setAssociatedObject(self, uiviewMargin_str, NSStringFromUIEdgeInsets(margin), OBJC_ASSOCIATION_RETAIN); if(self.superview){//只有在有父视图的情况下,才能更新约束
[self.superview updateConstraints];
}
} -(UIEdgeInsets)margin{
return UIEdgeInsetsFromString(objc_getAssociatedObject(self, uiviewMargin_str));
} -(void)resetConstraints{
[self removeConstraints:self.constraints];
}
@end
现在有了这个扩展类就可以继续上面的布局需求了。我们希望当把UIView添加到superview的时候对该UIView添加各种约束信息。代码如下:
-(void)didAddSubview:(UIView *)subview{
[super didAddSubview:subview];
subview.translatesAutoresizingMaskIntoConstraints=NO;//要实现自动布局,必须把该属性设置为no
[self removeConstraintsWithSubView:subview];//先把subview的原来的约束信息删除掉
[self updateSubViewConstraints:subview];//添加新的约束信息
}
上面提到布局是垂直自上而下的,而且宽度需要随着屏幕的宽度改变而改变。从这里我们可以得出两个结论。
- 宽度上要有一个约束,约束的具体信息是宽度随着父视图的宽度变化,还要把间距考虑进去。
- 所有添加到同一个父视图中的subviews按照顺序自上而下依序排列。
具体代码如下
-(void)updateSubViewConstraints:(UIView *)subView{
UIEdgeInsets margin=subView.margin;
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options: metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加宽度的约束
[self addConstraints:constraints];
//获取同级下的上一个视图的,以便做垂直的自上而下排列
NSInteger index=[self.subviews indexOfObject:subView];
UIView *preView=index==?nil:[self.subviews objectAtIndex:index-];
if(preView){//如果该subview有排序比它更靠前的视图
//该subview的顶部紧靠上一个视图的底部
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:(margin.top+preView.margin.bottom)]]; }else{
//该subview的顶部紧靠父视图的顶部
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:margin.top]];
} }
至此,我们已经实现了一个可以自动自上而下排列的stackpanel。继续考虑一个问题,如果我们动态的删除其中的一个子视图,我们会发现所有的约束机会都失效了,为什么?因为从上面的代码中可以看出,我们之所以能够实现自上而下的布局,完全是依赖余有序的前后视图的各种"依赖约束",也就是NSLayoutConstraint中的relatedBy是上一个视图,这就好比一条链子上的各个有序节点,一旦你把链子上的一个节点拿掉,那么原来的前后关系就改变了,因此约束也就失效了。
而为了能够实现在UIView从superview中移除的时候不影响整个的约束信息,那么我们必须重置约束信息,也就是我们应该在superview的didRemoveSubview这个方法中来重置,但是很遗憾,没有这个方法,苹果只给了我们willRemoveSubview方法,我目前没有想到其他方法,只能在willRemoveSubview这个方法上考虑去实现。现在问题又来了,willRemoveSubview这个方法被调用的时候该subview事实上还没有被删掉,只是告诉你将要被删除了。这里我采用了一个取巧的方法,说实话这样的代码不应该出现的,但是没办法,只能先将就用下。也就是在willRemoveSubview的方法里面,再调一次subview的removeFromSuperview的方法,这样当removeFromSuperview调用完毕的时候就表明该subview已经被移除了,但是这样一来就会造成循环调用了,因此我们还需要一个bool参数来标记该subview是有已经被删除了,因此我们需要在上面提到的UIPanelSubView类中添加一个不公开的属性isRemoved,该属性在UIVIew被添加到superview中的时候设置为no,被remove的时候设置为yes。
具体代码如下:
-(void)didAddSubview:(UIView *)subview{
[super didAddSubview:subview];
[subview setIsRemoved:NO];//标记为未删除
subview.translatesAutoresizingMaskIntoConstraints=NO;//要实现自动布局,必须把该属性设置为no
[self removeConstraintsWithSubView:subview];//先把subview的原来的约束信息删除掉
[self updateSubViewConstraints:subview];//添加新的约束信息
} -(void)willRemoveSubview:(UIView *)subview{
if(![subview isRemoved]){//因为没有didRemoveSubView方法,所以只能采用这样的方式来达到目的了
[subview setIsRemoved:YES];//标记为已删除
[subview removeFromSuperview];//再调用一次removeFromSuperview,这样调用完毕该方法,那么表明该subview已经被移除了
[self updateConstraints];//重置约束
}
}
-(void)updateConstraints{
[super updateConstraints];
for(UIView * v in self.subviews) {
[self updateSubViewConstraints:v];
}
}
这样就实现了subview被移除的时候仍然能有效约束。
现在当我们把UIStackPanel添加ViewController的view中的时候,发现旋转屏幕的时候里面的布局没有跟着变。这是因为我们以上的约束信息都是UIStackPanel和它的子视图的,但是UIStackPanel没有建立起跟它的父视图的约束,这样当然不能实现自动布局啦。要解决这个问题,也很简单。对UIStackPanel添加一个属性isBindSizeToSuperView,是否把UIStackPanel的高宽跟父视图的高宽绑定。
-(void)setIsBindSizeToSuperView:(BOOL)isBindSizeToSuperView{
if(_isBindSizeToSuperView!=isBindSizeToSuperView){
_isBindSizeToSuperView=isBindSizeToSuperView;
if(isBindSizeToSuperView){
self.translatesAutoresizingMaskIntoConstraints=NO;
if(self.superview){
[self bindSizeToSuperView];
}
}else{
self.translatesAutoresizingMaskIntoConstraints=YES;
}
}
} -(void)bindSizeToSuperView{
UIEdgeInsets margin=self.margin;
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options: metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
[self.superview addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options: metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : self}];
[self.superview addConstraints:constraints];
}
这样我们已经完全实现了开头提到的布局要求。
下面贴出完整的代码
@interface UIView(UIPanelSubView)
//设置view的大小
@property (nonatomic,assign)CGSize size;
//view距离左上右下的间距
@property (nonatomic,assign)UIEdgeInsets margin;
//重置约束
-(void)resetConstraints;
@end @interface UIPanel : UIView @property (nonatomic,assign)BOOL isBindSizeToSuperView;//是否把高宽绑定到父视图
//更新某个字视图的约束信息
-(void)updateSubViewConstraints:(UIView *)subView; //删除属于subView的NSLayoutConstraint
-(void)removeConstraintsWithSubView:(UIView *)subView;
@end @interface UIStackPanel : UIPanel
@property (nonatomic,assign)BOOL isHorizontal;//是否水平布局
@end
@implementation UIView(UIPanelSubView)
char* const uiviewSize_str = "UIViewSize";
-(void)setSize:(CGSize)size{
objc_setAssociatedObject(self, uiviewSize_str, NSStringFromCGSize(size), OBJC_ASSOCIATION_RETAIN);
//先将原来的高宽约束删除
for(NSLayoutConstraint *l in self.constraints){
switch (l.firstAttribute) {
case NSLayoutAttributeHeight:
case NSLayoutAttributeWidth:
[self removeConstraint:l];
break;
default:
break;
}
}
//添加高度约束
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.height]];
//添加宽度约束
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:size.width]];
} -(CGSize)size{
return CGSizeFromString(objc_getAssociatedObject(self, uiviewSize_str));
} char* const uiviewMargin_str = "UIViewMargin";
-(void)setMargin:(UIEdgeInsets)margin{
objc_setAssociatedObject(self, uiviewMargin_str, NSStringFromUIEdgeInsets(margin), OBJC_ASSOCIATION_RETAIN); if(self.superview){//只有在有父视图的情况下,才能更新约束
[self.superview updateConstraints];
}
} -(UIEdgeInsets)margin{
return UIEdgeInsetsFromString(objc_getAssociatedObject(self, uiviewMargin_str));
} //用来标记该视图一否已经被删除
char* const uiviewIsRemoved_str = "UIViewIsRemoved";
-(void)setIsRemoved:(BOOL)isRemoved{
objc_setAssociatedObject(self, uiviewIsRemoved_str, @(isRemoved), OBJC_ASSOCIATION_RETAIN);
} -(BOOL)isRemoved{
return [objc_getAssociatedObject(self, uiviewIsRemoved_str) boolValue];
} -(void)resetConstraints{
[self removeConstraints:self.constraints];
if(self.superview && [self.superview respondsToSelector:@selector(updateSubViewConstraints:)]){
[self.superview performSelector:@selector(removeConstraintsWithSubView:) withObject:self];
[self.superview performSelector:@selector(updateSubViewConstraints:) withObject:self];
[self updateConstraints];
}
} @end @implementation UIPanel -(void)setIsBindSizeToSuperView:(BOOL)isBindSizeToSuperView{
if(_isBindSizeToSuperView!=isBindSizeToSuperView){
_isBindSizeToSuperView=isBindSizeToSuperView;
if(isBindSizeToSuperView){
self.translatesAutoresizingMaskIntoConstraints=NO;
if(self.superview){
[self bindSizeToSuperView];
}
}else{
self.translatesAutoresizingMaskIntoConstraints=YES;
}
}
} -(void)didAddSubview:(UIView *)subview{
[super didAddSubview:subview];
[subview setIsRemoved:NO];//标记为未删除
subview.translatesAutoresizingMaskIntoConstraints=NO;//要实现自动布局,必须把该属性设置为no
[self removeConstraintsWithSubView:subview];//先把subview的原来的约束信息删除掉
[self updateSubViewConstraints:subview];//添加新的约束信息
} -(void)updateSubViewConstraints:(UIView *)subView{
UIEdgeInsets margin=subView.margin;
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options: metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];
[self addConstraints:constraints]; constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options: metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : subView}];
[self addConstraints:constraints];
} -(void)willRemoveSubview:(UIView *)subview{
if(![subview isRemoved]){//因为没有didRemoveSubView方法,所以只能采用这样的方式来达到目的了
[subview setIsRemoved:YES];//标记为已删除
[subview removeFromSuperview];//再调用一次removeFromSuperview,这样调用完毕该方法,那么表明该subview已经被移除了
[self updateConstraints];//重置约束
}
} -(void)removeConstraintsWithSubView:(UIView *)subView{
for(NSLayoutConstraint *l in self.constraints){
if(l.firstItem==subView){
[self removeConstraint:l];
}
}
} -(void)updateConstraints{
[super updateConstraints];
for(UIView * v in self.subviews) {
[self updateSubViewConstraints:v];
}
} -(void)bindSizeToSuperView{
UIEdgeInsets margin=self.margin;
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options: metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
[self.superview addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options: metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : self}];
[self.superview addConstraints:constraints];
} -(void)didMoveToSuperview{
[super didMoveToSuperview];
if(self.isBindSizeToSuperView){
[self bindSizeToSuperView];
}
}
@end @implementation UIStackPanel -(void)setIsHorizontal:(BOOL)isHorizontal{
if(_isHorizontal!=isHorizontal){
_isHorizontal=isHorizontal;
[self updateConstraints];
}
} -(void)updateSubViewConstraints:(UIView *)subView{
UIEdgeInsets margin=subView.margin;
if(self.isHorizontal){
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options: metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : subView}];
[self addConstraints:constraints]; NSInteger index=[self.subviews indexOfObject:subView];
UIView *preView=index==?nil:[self.subviews objectAtIndex:index-]; if(preView){
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeRight multiplier:1.0f constant:(margin.left+preView.margin.left)]]; }else{
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0f constant:margin.left]];
} }else{
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options: metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加宽度的约束
[self addConstraints:constraints];
//获取同级下的上一个视图的,以便做垂直的自上而下排列
NSInteger index=[self.subviews indexOfObject:subView];
UIView *preView=index==?nil:[self.subviews objectAtIndex:index-];
if(preView){//如果该subview有排序比它更靠前的视图
//该subview的顶部紧靠上一个视图的底部
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:preView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:(margin.top+preView.margin.bottom)]]; }else{
//该subview的顶部紧靠父视图的顶部
[self addConstraint:[NSLayoutConstraint constraintWithItem:subView
attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:margin.top]];
}
}
}
@end
下一篇介绍uigridpanel的原理
IOS 自动布局-UIStackPanel和UIGridPanel(二)的更多相关文章
- IOS 自动布局-UIStackPanel和UIGridPanel(三)
在这一篇了我将继续讲解UIGridPanel. 在iphone的app里面可以经常看到一些九宫格布局的应用,做过html开发的对这类布局应该是很熟悉的.在IOS中要实现这样的布局方法还是蛮多的,但是我 ...
- IOS 自动布局-UIStackPanel和UIGridPanel(四)
为什么说scrollview的自动化布局是难点? 对scrollview做自动化布局,无非就是想对scrollview里面的subviews来做自动化布局.但是scrollview里面的subview ...
- IOS 自动布局-UIStackPanel和UIGridPanel(一)
我以前是做windows phone开发的,后来转做IOS的开发,因此很多windows phone上面的开发经验也被我带到了IOS中.其实有些经验本身跟平台无关,跟平台有关的无非就是实现方法而已.好 ...
- IOS 自动布局-UIStackPanel和UIGridPanel(五)
试想这样的一个需求场合,一个button靠右显示,并且距离superView的顶部和右边间距分别为10和5.如下图所示: 要实现这样的需求,如果不用自动布局技术,那么我们能想到的就是老老实实的使用绝对 ...
- iOS如何获取网络图片(二)
ios如何获取图片(二)无沙盒下 解决问题 *解决问题1:tableView滑动卡顿,图片延时加载 解决方法:添加异步请求,在子线程里请求网络,在主线程刷新UI *解决问题2:反复请求网络图片,增加用 ...
- iOS开发Swift篇—(二)变量和常量
iOS开发Swift篇—(二)变量和常量 一.语言的性能 (1)根据WWDC的展示 在进行复杂对象排序时Objective-C的性能是Python的2.8倍,Swift的性能是Python的3.9倍 ...
- iOS 自动布局详细介绍
1. 自动布局的理解 iOS自动布局很有用,可以在不同size的屏幕上运行,原先看的头痛,还是习惯用最蠢的[UIScreen mainScreen].bounds.size.width等来布局,后来实 ...
- iOS开发CoreAnimation解读之二——对CALayer的分析
iOS开发CoreAnimation解读之二——对CALayer的分析 一.UIView中的CALayer属性 1.Layer专门负责view的视图渲染 2.自定义view默认layer属性的类 二. ...
- iOS 11开发教程(二十二)iOS11应用视图实现按钮的响应(2)
iOS 11开发教程(二十二)iOS11应用视图实现按钮的响应(2) 此时,当用户轻拍按钮后,一个叫tapButton()的方法就会被触发. 注意:以上这一种方式是动作声明和关联一起进行的,还有一种先 ...
随机推荐
- Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础
来源作者:candycat http://blog.csdn.net/candycat1992/article/ 概述 总体来说,在Unity中我们需要配合使用材质和Unity Shader才能达 ...
- [转]SqlServer索引的原理与应用
索引的概念 索引的用途:我们对数据查询及处理速度已成为衡量应用系统成败的标准,而采用索引来加快数据处理速度通常是最普遍采用的优化方法. 索引是什么:数据库中的索引类似于一本书的目录,在一本书中使用目录 ...
- Java设计模式之单例模式 - Singleton
用来创建独一无二的,是能有一个实例的对象的入场券.告诉你一个好消息,单例模式的类图可以说是所有模式的类图中最简单的,事实上,它的类图上只有一个类!但是,可不要兴奋过头,尽管从类设计的视角来说很简单,但 ...
- Java GUI设置图标
ImageIcon是Icon接口的一个实现类. ImageIcon类的构造函数: ImageIcon() ImageIcon(String filename) //本地图片文件 ImageIcon ...
- spring boot图片上传至远程服务器并返回新的图片路径
界面上传图片时考虑到可能会有用户的图片名称一致,使用UUID来对图片名称进行重新生成. //UUIDUtils public class UUIDUtils { public static Strin ...
- Power BI 连接到 Azure 账单,自动生成报表,可刷新
开始研究Azure官网等,提供的链接都是错误的,躺了很大的一个坑,配置后根本无法获取账单信息,经过多次查询找到了方向,过来记录一下: 错误的地址(应该是适用于全球版,国内版无法这样获取): https ...
- hihoCoder hiho一下 第四十六周 博弈游戏·Nim游戏·三( sg函数 )
题意: 给出几堆石子数量,每次可以取走一堆中任意数量的石头,也可以将一堆分成两堆,而不取.最后取走者胜. 思路: 先规矩地计算出sg值,再对每个数量查SG值就可以了.最后求异或和.和不为0的就是必赢. ...
- 刷新本地DNS缓存的方法
http://www.cnblogs.com/rubylouvre/archive/2012/08/31/2665859.html 常有人问到域名解析了不是即时生效的嘛,怎么还是原来的呢?答案就是在本 ...
- 单表操作ORM
博客园 首页 新随笔 联系 管理 订阅 随笔- 0 文章- 339 评论- 29 Django基础五之django模型层(一)单表操作 本节目录 一 ORM简介 二 单表操作 三 章节作业 ...
- 转义字符 & sizeof & strlen
在定义了数组大小时: sizeof是运算符,表示编译时分配的空间大小,即数组定义的大小,char t[20] = "sfa".sizeof: 20; strlen: 3.在未定义数 ...