为iPhone6设计自适应布局(纯代码实现)

目前网络上已经有很多关于AutoLayout的讲义可供大家学习,大部分的Demo都是通过IB或者Storyboard上完成的。很多人也在思考,到目前iOS 8这个版本,使用代码来实现UI布局是不是合适?今天有时间,使用纯代码写了一小段布局代码,供大家比较。


本文所需要实现的界面布局来自这一篇博客:ADAPTIVE LAYOUTS FOR iPHONE 6,对应的中文翻译版本为:为iPhone6设计自适应布局。读者可以先行阅读以上这篇博客,来了解AutoLayout和Size Class的基本概念,跟着博客中的步骤,使用Storyboard完成上述Demo。


需求

本文Demo需要同时在iPhone 4, iPhone 5, iPhone 6 和 iPhone 6 Plus完成以下布局


并且支持横竖屏旋转,旋转动画如下:

本文的示例代码可以在Github上直接获取。

分析

通过参考图,我们可以发现需要实现的页面布局中:最外层是一个NavigationController。内容区域,在竖屏情况下,把个人信息从上到下布局,在横屏情况下,从左到右布局。

内容区域代码实现

1) 初始化控件

- (void)loadView
{
    [super loadView];

    self.avatarImageView = [self createAvatarImageView];
    [self.view addSubview:self.avatarImageView];

    self.nameLabel = [self createNameLabel];
    [self.view addSubview:self.nameLabel];

    self.timeLabel = [self createTimeLabel];
    [self.view addSubview:self.timeLabel];

    self.descriptionLabel = [self createDescriptionLabel];
    [self.view addSubview:self.descriptionLabel];

    self.photoImageView = [self createPhotoImageView];
    [self.view addSubview:self.photoImageView];
}

- (UIImageView *)createAvatarImageView
{
    UIImageView *avatarImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"avatar"]];
    avatarImageView.translatesAutoresizingMaskIntoConstraints = NO;
    return avatarImageView;
}

- (UILabel *)createNameLabel
{
    UILabel *nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    nameLabel.font = [UIFont systemFontOfSize:12.0f];
    nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
    nameLabel.text = @"Chun Tips";
    return nameLabel;
}

- (UILabel *)createTimeLabel
{
    UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    timeLabel.font = [UIFont systemFontOfSize:11.0f];
    timeLabel.translatesAutoresizingMaskIntoConstraints = NO;
    timeLabel.text = @"2w ago";
    return timeLabel;
}

- (UILabel *)createDescriptionLabel
{
    UILabel *descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    descriptionLabel.font = [UIFont systemFontOfSize:11.0f];
    descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
    descriptionLabel.text = @"Apple, Google, Microsoft, Instagram, Twitter, Facebook and 4 others like this";
    descriptionLabel.numberOfLines = 0;
    return descriptionLabel;
}

- (UIImageView *)createPhotoImageView
{
    UIImageView *photoImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
    photoImageView.translatesAutoresizingMaskIntoConstraints = NO;
    photoImageView.image = [UIImage imageNamed:@"photo"];
    return photoImageView;
}

以上代码,和以往手写代码布局基本雷同。有三点需要注意的地方:

  • 使用AutoLayout布局时初始化控件的frame应该为 CGRectZero
  • 对于使用AutoLayout布局的控件应该设置translatesAutoresizingMaskIntoConstraints为 NO
  • 如果需要让UILabel在AutoLayout中正确的支持多行显示,需要设置numberOfLines==0(默认为1),并且在适当的地方设置 preferredMaxLayoutWidth 这个值

2) 初始化控件好了之后,我们就应该开始让控件支持自动布局:

- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
    //等待实现
}

- (vodi)loadView
{
    //...
    //上面已经实现过的代码

    [self updateConstraintsForTraitCollection:self.traitCollection];
}

需求中我们需要支持iPhone设备的横竖两个方向,在Size Class概念中,UITraitCollection包含了我们所需要的信息。所以我们依据当前ViewController的traitCollection来构造不同的布局约束。

在设备的traitCollection改变时(旋转),我们可以在willTransitionToTraitCollection方法中捕获到相应信息,并开始做旋转动画:(关于该方法的详细描述,可以参考上一篇博客)

- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
    [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
    [self updateConstraintsForTraitCollection:newCollection];

    [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    [self updateConstraintsForTraitCollection:newCollection];
    [self.view setNeedsLayout];
    } completion:nil];
}

3)好了,最后一步,我们开始实现布局代码。

3.1 初始化我们在布局约束中需要的所有Views和一些常量值,放到字典里。初始化需要更新的constraits,放到数组里。

CGFloat const kLayoutPadding = 10.0f; // 每个控件之间的间距为10.0f

- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
   NSDictionary *views = @{@"topLayoutGuide": self.topLayoutGuide, @"avatarImageView": self.avatarImageView, @"nameLabel": self.nameLabel, @"timeLabel": self.timeLabel, @"descriptionLabel": self.descriptionLabel, @"photoImageView": self.photoImageView};
   NSDictionary *metrics = @{@"padding": @(kLayoutPadding)};
   NSMutableArray *updateConstraits = [NSMutableArray array];
}

上面提到的topLayoutGuide属性,在当前的场景下,它对应的NavigationBar,高度也为NavigationBar的高度。有不知道的朋友可以参考之前写过的一篇博客,有详细叙述。

3.2 从Size Class的文档中,我们可以查询到,iPhone在竖屏情况下,水平方向是 Compact, 竖直方向是 Regular,而在横屏情况下,两个方向都是 Compact。从我们的设计稿的需求,我们可以通过判断竖屏方向的Size Class来进行分别约束:

- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
    //...
    //上面已经实现过的代码

   if (collection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
        // 横屏情况
   } else {
        // 竖屏情况
   }

   if (self.constraits) {
     [NSLayoutConstraint deactivateConstraints:self.constraits];
   }

   self.constraits = updateConstraits;

   [NSLayoutConstraint activateConstraints:self.constraits];
}

完成布局约束之后,使用activateConstraints激活布局约束。如果之前有旧的布局约束,应该先使用deactivateConstraints移除。

3.3 实现约束代码:

    // 横屏
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|[photoImageView]-(padding)-[avatarImageView]-(padding)-[nameLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[avatarImageView]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[photoImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide][photoImageView]|" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[avatarImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[nameLabel]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];

    self.descriptionLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - self.view.bounds.size.height + self.topLayoutGuide.length - 2 * kLayoutPadding; //label在横屏下能显示的最大宽度 = 屏幕宽度 - 图片宽度(屏幕高度 - NavigatioBar的高度) - label本身显示的左右间距。

    // 竖屏
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(padding)-[avatarImageView]-(padding)-[nameLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[avatarImageView]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|[photoImageView]|" options:0 metrics:nil views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[avatarImageView]-(padding)-[photoImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
    [updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[nameLabel]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];

    self.descriptionLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 2 * kLayoutPadding; //label在竖屏下能显示的最大宽度 = 屏幕宽度 - label本身显示的左右间距。

    // 共享代码
    [updateConstraits addObject:[NSLayoutConstraint constraintWithItem:self.avatarImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.avatarImageView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
    [updateConstraits addObject:[NSLayoutConstraint constraintWithItem:self.photoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.photoImageView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];

上述布局代码中,有两点需要特别提出:

  • 需要支持多行显示的descriptionLabel应该在不同界面显示的情况下更新preferredMaxLayoutWidth
  • 共享代码是让图片在横竖屏的情况下实现宽高等比(需求是1:1,显示为正方形)。

总结

上述通过纯代码实现了和这篇博客一样的Demo:ADAPTIVE LAYOUTS FOR iPHONE 6。不知道有兴趣的朋友在使用过两个方式实现UI布局之后,有什么感悟,觉得哪一种更加适合现在的生产开发。

在写这篇博客之前,在公司项目中,还一直保留着纯代码使用AutoLayout实现UI的方法。通过最近对Storyboard和IB新功能的进一步了解,会开始尝试在自己的项目中使用。


阅读更多
个人分类: IOS学习之路
上一篇iOS学习应用开发就业课:第7章_060:导航控制器动画
下一篇iOS UI自适应:用“代码”还是“可视化xib”?_个人选择还是xib的autolayout和sizeclasses
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭