• 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];//添加新的约束信息
    }

    上面提到布局是垂直自上而下的,而且宽度需要随着屏幕的宽度改变而改变。从这里我们可以得出两个结论。

    1. 宽度上要有一个约束,约束的具体信息是宽度随着父视图的宽度变化,还要把间距考虑进去。
    2. 所有添加到同一个父视图中的subviews按照顺序自上而下依序排列。

    具体代码如下

    -(void)updateSubViewConstraints:(UIView *)subView{
        UIEdgeInsets margin=subView.margin;
        NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-left-[view]-right-|" options:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加宽度的约束
        [self addConstraints:constraints];
        //获取同级下的上一个视图的,以便做垂直的自上而下排列
        NSInteger index=[self.subviews indexOfObject:subView];
        UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
        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:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
        [self.superview addConstraints:constraints];
        constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 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:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];
        [self addConstraints:constraints];
        
        constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 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:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : self}];
        [self.superview addConstraints:constraints];
        constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view]-bottom-|" options:0 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:0 metrics:@{ @"top" : @(margin.top),@"bottom":@(margin.bottom)} views:@{ @"view" : subView}];
            [self addConstraints:constraints];
            
            NSInteger index=[self.subviews indexOfObject:subView];
            UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
            
            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:0 metrics:@{ @"left" : @(margin.left),@"right":@(margin.right)} views:@{ @"view" : subView}];//添加宽度的约束
            [self addConstraints:constraints];
            //获取同级下的上一个视图的,以便做垂直的自上而下排列
            NSInteger index=[self.subviews indexOfObject:subView];
            UIView *preView=index==0?nil:[self.subviews objectAtIndex:index-1];
            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的原理

  • 相关阅读:
    Flask莫名其妙特别慢
    MySQL老是提示视图没有主键
    Mysql写入中文出错
    Sqlite向MySql导入数据
    大智慧专业财务PFFIN(N,M)函数N的取值一览表
    js的技巧
    拍拍贷年化收益率的推算
    Sqlite的多表连接更新
    Kali Linux 64位架构安装Veil-Evasion
    修改Kali Linux 2020.1主题颜色
  • 原文地址:https://www.cnblogs.com/dagehaoshuang/p/4012870.html
Copyright © 2020-2023  润新知