项目干货挖掘4——如何优雅地使用AutoLayout自动布局

附注:标题中“优雅地”三个字体现了作者对逼格的不懈追求。呵呵,装个逼,下面开始正题。


自动布局简介

一开始iOS写界面布局是通过frame固定位置及尺寸的。因为当时iPhone手机的屏幕尺寸是统一的,但后来随着iPhone手机尺寸变多,苹果新增了autoresizingMask这个东西,它其实就是UIView的一个属性,意为“自动伸缩”。通过它能进行简单的自动布局,主要是完成父子视图之间的自动布局,但对于非父子视图等比较复杂的视图之间的自动布局,就显得力不从心了。随后,苹果便推出了功能更加强大的AutoLayout,它几乎能完成任何复杂关系的布局。这便是自动布局的发展简介。

UIView的autoresizingMask属性的定义:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

可以看到它是支持位运算符的枚举,也即可以像下面这样使用。它代表该视图greenView相对于父视图,它的宽和高是可以伸缩的,并非固定的。

greenView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

AutoLayout的约束——NSLayoutConstraint

一句话解释AutoLayout是通过给某视图添加“约束”来布局的,这些“约束”确定了该视图与其他视图的位置关系及自身尺寸等问题。

好了,现在假如我们想把一个UIView以这样的形式布局:该UIView的顶部距离父视图顶部100px,左边距离父视图的左边20px,右边距离父视图也20px,而其高度为300px。
AutoLayout的代码要这样写:

    UIView *greenView = [[UIView alloc] init];
    greenView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:greenView];
    
    
    // greenView上边距view上边80
    NSLayoutConstraint *constraint_Top = [NSLayoutConstraint constraintWithItem:greenView
                                                                      attribute:NSLayoutAttributeTop
                                                                      relatedBy:NSLayoutRelationEqual
                                                                         toItem:self.view
                                                                      attribute:NSLayoutAttributeTop
                                                                     multiplier:1.f
                                                                       constant:80.f];
    // greenView左边距view左边20
    NSLayoutConstraint *constraint_Left = [NSLayoutConstraint constraintWithItem:greenView
                                                                  attribute:NSLayoutAttributeLeft
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:self.view
                                                                  attribute:NSLayoutAttributeLeft
                                                                 multiplier:1.f
                                                                   constant:20.f];
    // greenView右边距view右边20
    NSLayoutConstraint *constraint_Right = [NSLayoutConstraint constraintWithItem:greenView
                                                                       attribute:NSLayoutAttributeRight
                                                                       relatedBy:NSLayoutRelationEqual
                                                                          toItem:self.view
                                                                       attribute:NSLayoutAttributeRight
                                                                      multiplier:1.f
                                                                        constant:-20.f];
    // greenView的高度为300
    NSLayoutConstraint *constraint_Height = [NSLayoutConstraint constraintWithItem:greenView
                                                                         attribute:NSLayoutAttributeHeight
                                                                         relatedBy:NSLayoutRelationEqual
                                                                            toItem:nil
                                                                         attribute:0
                                                                        multiplier:1.f
                                                                          constant:400.f];
    greenView.translatesAutoresizingMaskIntoConstraints = NO;
    [greenView addConstraint:constraint_Height];
    [self.view addConstraints:@[constraint_Top, constraint_Left, constraint_Right]];

看完上面的代码你也许该说该“What The Fuck?!”了,心想,写个这么简单的布局竟然要写这么一大段代码吗?
确实,如果在项目实践中,直接使用苹果提供的这一套API进行开发,简直是不可想象的,估计没人会这么做吧。但我们可以基于此进行封装,使其提供的API对于程序员来说语义明了,而且最好短小精悍。

不过在进行封装前,我们还得先把一些东西搞清楚。请继续往下看。


约束应该添加给哪个UIView?

可以注意到上面的代码里,与父视图有相对关系的“约束”是添加给父视图self.view的。而表示greenView高度的约束却是添加给greenView的。这又是为什么?这里面有什么道理呢?
其中的道理是:如果是设置宽、高的固定长度,而和别的视图没有相对关系,就把该约束添加给自己;若该约束是和别的视图的一种相对关系,则添加给两个视图的最小公共父视图。

上面的例子里,greenView的高度只是设置自己的高度,和其他视图并无关系,所以把约束添加给greenView自己,而其他三个约束分别代表和父视图self.viewtop/left/right的三个方向边距,是和别的视图有相对关系的,所以要添加给两者中的公共父视图,即self.view

拷贝几张图来体会:

01.png
02.png
03.png

开始封装AutoLayout

我们进行布局时,常常描述为:bView的top距离aView的bottom60px。或者bView的left距离aView的right20px。
总之它的思维方式是bView的top/bottom/left/right和aView的top/bottom/left/right的一种距离关系。
我们新建一个AutoLayoutObject类来表示“约束”。

** AutoLayoutObject.h **

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#define LAYEqual(a1, a2, m, c)          [a1 addEqualConstraint:a2 multiplier:m constant:c]
#define LAYLessThan(a1, a2, m, c)       [a1 addLessThanConstraint:a2 multiplier:m constant:c]
#define LAYMoreThan(a1, a2, m, c)       [a1 addMoreThanConstraint:a2 multiplier:m constant:c]
#define LAYEqualC(a, c)                 [a addEqualConstant:c]
#define LAYLessThanC(a, c)              [a addLessThanConstant:c]
#define LAYMoreThanC(a, c)              [a addMoreThanConstant:c]
#define LAYM(a, m)              [a addEqualMultiplier:m]



@interface AutoLayoutObject : NSObject


@property (nonatomic, strong)id                         item; // 某视图
@property (nonatomic, assign)NSLayoutAttribute          attribute; // 属性,top/bottom/left/right/centerX/centerY等
@property (nonatomic, assign)NSLayoutRelation           relation; // 等于 or 小于等于 or 大于等于
@property (nonatomic, assign)CGFloat                    multiplier; // 伸缩形变量
@property (nonatomic, assign)CGFloat                    constant; // 常量,代表长度或距离


// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant;

// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant;

// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant;

// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;



@end

** AutoLayoutObject.m **

#import "AutoLayoutObject.h"
#import "UIView+AutoLayout.h"



@implementation AutoLayoutObject


- (id)init
{
    self = [super init];
    if(self)
    {
        // 赋初值(默认值)
        _item = nil;
        _attribute = NSLayoutAttributeNotAnAttribute;
        _relation = NSLayoutRelationEqual; // 默认是相等
        _multiplier = 1.f;
        _constant = 0.f;
    }
    
    return self;
}



// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant
{
    
    UIView *superView = (UIView *)[self.item superview];
    
    switch (self.attribute)
    {
        // left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
        case NSLayoutAttributeLeft:{
            [self addEqualConstraint:superView.left multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeRight:{
            [self addEqualConstraint:superView.right multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeTop:{
            [self addEqualConstraint:superView.top multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeBottom:{
            [self addEqualConstraint:superView.bottom multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterX:{
            [self addEqualConstraint:superView.centerX multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterY:{
            [self addEqualConstraint:superView.centerY multiplier:1.f constant:constant];
            break;
        }
        
        // 设width 和 heigth为常量值
        case NSLayoutAttributeWidth:
        case NSLayoutAttributeHeight:{
            [self addEqualConstraint:nil multiplier:1.f constant:constant];
            break;
        }
            
            
        default:
            break;
    }
}


// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant
{
    UIView *superView = (UIView *)[self.item superview];
    
    switch (self.attribute)
    {
            // left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
        case NSLayoutAttributeLeft:{
            [self addLessThanConstraint:superView.left multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeRight:{
            [self addLessThanConstraint:superView.right multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeTop:{
            [self addLessThanConstraint:superView.top multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeBottom:{
            [self addLessThanConstraint:superView.bottom multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterX:{
            [self addLessThanConstraint:superView.centerX multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterY:{
            [self addLessThanConstraint:superView.centerY multiplier:1.f constant:constant];
            break;
        }
            
            // 设width 和 heigth为常量值
        case NSLayoutAttributeWidth:
        case NSLayoutAttributeHeight:{
            [self addLessThanConstraint:nil multiplier:1.f constant:constant];
            break;
        }
            
            
        default:
            break;
    }
}


// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant
{
    // 省略...
}


// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationEqual];
}


// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationLessThanOrEqual];
}


// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationGreaterThanOrEqual];
}



// 全能方法
- (void)addGenericConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant relation:(NSLayoutRelation)relation
{
    if(!self.item){
        return;
    }

    // 确定出视图aView和bView的最小公共父类ancestorView,最终要给该视图添加约束
    UIView *aView = self.item;
    UIView *bView = secondLayoutObj.item;
    UIView *ancestorView;
    if([aView isDescendantOfView:bView]){
        ancestorView = bView;
    }else if([bView isDescendantOfView:aView]){
        ancestorView = aView;
    }else if([aView isDescendantOfView:bView.superview]){
        ancestorView = bView.superview;
    }else if([bView isDescendantOfView:aView.superview]){
        ancestorView = aView.superview;
    }else{
        ancestorView = aView.superview;
    }
    
    NSLayoutConstraint *layoutConstraint = [NSLayoutConstraint constraintWithItem:self.item
                                                                        attribute:self.attribute
                                                                        relatedBy:relation
                                                                           toItem:secondLayoutObj.item
                                                                        attribute:secondLayoutObj.attribute
                                                                       multiplier:multiplier
                                                                         constant:constant];
    
    [ancestorView addConstraint:layoutConstraint];
}
@end


但是为了得到某UIView的这个自定义的“约束”对象,我们得提前新建一个UIView的分类:

#import <UIKit/UIKit.h>
#import "AutoLayoutObject.h"


@interface UIView (AutoLayout)


+ (id)create;


- (AutoLayoutObject *)top;
- (AutoLayoutObject *)bottom;
- (AutoLayoutObject *)left;
- (AutoLayoutObject *)right;
- (AutoLayoutObject *)leading;
- (AutoLayoutObject *)trailing;

- (AutoLayoutObject *)width;
- (AutoLayoutObject *)height;
- (AutoLayoutObject *)centerX;
- (AutoLayoutObject *)centerY;
- (AutoLayoutObject *)baseline;
- (AutoLayoutObject *)notAnAttribute;



- (void)removeAllConstraint;

- (void)removeAutoLayoutObject:(AutoLayoutObject *)constraint;


@end


UIView+AutoLayout中我们不仅写了返回view各种“约束”对象的接口方法,而且也提供了初始化UIView的类方法,这不但使初始化一个UIView更简洁,而且主要的目的是为了关闭view的autoresizingMask属性。

#import "UIView+AutoLayout.h"

@implementation UIView (AutoLayout)



+ (id)create
{
    UIView *aView = [[self alloc] init];
    if(aView){
        aView.translatesAutoresizingMaskIntoConstraints = NO; // 默认是支持autoresizingMask的,但它可能会和autoLayout冲突,所以我们关闭autoresizingMask属性。
    }
    
    return aView;
}


- (AutoLayoutObject *)top
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeTop];
}

- (AutoLayoutObject *)bottom
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeBottom];
}

- (AutoLayoutObject *)left
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeft];
}

- (AutoLayoutObject *)right
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeRight];
}

- (AutoLayoutObject *)leading
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeading];
}

- (AutoLayoutObject *)trailing
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeTrailing];
}


- (AutoLayoutObject *)width
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeWidth];
}

- (AutoLayoutObject *)height
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeHeight];
}

- (AutoLayoutObject *)centerX
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterX];
}

- (AutoLayoutObject *)centerY
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterY];
}

- (AutoLayoutObject *)baseline
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeBaseline];
}

- (AutoLayoutObject *)notAnAttribute
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeNotAnAttribute];
}



- (void)removeAllConstraint
{
    [self removeAllConstraint];
}

- (void)removeAutoLayoutObject:(AutoLayoutObject *)autoLayoutObj
{
    for(NSLayoutConstraint *constranit in self.constraints)
    {
        if(autoLayoutObj.attribute == constranit.firstAttribute){
            [self removeConstraint:constranit];
        }
    }
}



// 生成当前UIView的autoLayoutObj对象
- (AutoLayoutObject *)autoLayoutObjWithAttribute:(NSLayoutAttribute)attribute
{
    AutoLayoutObject *autoLayoutObj = [[AutoLayoutObject alloc] init];
    autoLayoutObj.item = self;
    autoLayoutObj.attribute = attribute;
    
    return autoLayoutObj;
}


@end

以上AutoLayoutObject类和UIView分类UIView+AutoLayout是便是完成自动布局的全部东西。

好了,我们通过这个封装后的自动布局来完成本文开头的那个布局。是不是精炼了很多。(我们把已然封装后提供的接口方法再次进行了宏定义,最终我们使用与布局方法相对应的宏来进行布局,这样更简短,更明了。)

    UIView *greenViewiew = [UIView create];
    greenViewiew.backgroundColor = [UIColor greenColor];
    [self.view addSubview:greenViewiew];
    
    LAYEqual(greenViewiew.top, self.view.top, 1, 80.f);
    LAYEqual(greenViewiew.left, self.view.left, 1, 20.f);
    LAYEqual(greenViewiew.right, self.view.right, 1, -20.f);
    LAYEqualC(greenViewiew.height, 300.f);

约束为一个范围的情况

我们在实际开发中经常会碰到,某视图布局的需求是最大不得超过多少,且最小不得小于多少。是在一个范围内的。
比如一个UILabel,它里面的文字很多时,正常情况下系统会自动根据文本的多少来更新高度,而不需要你自己添加约束。

    UILabel *lab = [UILabel create];
    lab.numberOfLines = 0;
    lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
    lab.backgroundColor = [UIColor greenColor];
    [self.view addSubview:lab];
    LAYEqual(lab.left, self.view.left, 1, 30.f);
    LAYEqual(lab.right, self.view.right, 1, -30.f);
    LAYEqual(lab.top, self.view.top, 1, 100.f);

此时不需要你约束该lab的高度,它会根据文本多少来自动更新高度。

lab01.png

但是往往需求中,产品经理要求这里文字很多时,该视图也不能无限制变高,得有个限度;当文字很少时,高度也不能太小,也要有个限度。这个在我们上面的封装的自动布局中是支持的。

    UILabel *lab = [UILabel create];
    lab.numberOfLines = 0;
    lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
    lab.backgroundColor = [UIColor greenColor];
    [self.view addSubview:lab];
    LAYEqual(lab.left, self.view.left, 1, 30.f);
    LAYEqual(lab.right, self.view.right, 1, -30.f);
    LAYEqual(lab.top, self.view.top, 1, 100.f);
    LAYMostC(lab.height, 100.f);

即便文字很多,高度也为100.f这个上限:


lab02.png

关于自动布局的代码我也上传至GitHub上了,求Star支持。
YWAutoLayoutGitHub地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容