FlexLayout入门系列(二)

Demo写来一时爽,配套解说火葬场啊……

Yoga简介

Facebook出品的Yoga是一个实现了Flexbox布局方式的跨平台布局引擎。底层代码开头是用C语言写的,后来改用C++来写了。

YogaiOS平台上的实现叫做YogaKit,然后FlexLayout的作者Luc给它套了一层,让它写起来更简洁直观,并且支持链式语法。

如果对它的性能有疑问,客官可以先看看Luc做的布局引擎跑分测试项目,都说跑分是浮云,用户体验才是王道,我们简单看看结果好了:

当然啦,WWDC2018里,苹果爸爸说Autolayout有巨大的提升,希望Luc早日更新华山论剑结果。

基本Flexbox概念

这部分客官直接看看这篇文章就可以了,里面介绍得很详细,我也是从小看着它长大的,哦不对,是看着它入门的。

FlexLayout做了什么

下面的内容是基于你已经基本领悟了上节提到的文章的情况下写的,不想重复提太多基本概念。 如果你发现本节内容看不下去,那你可以先跳过,反正你会回来看的(或者你会自己看源码)。

简单聊聊FlexLayout,你看上篇的代码里到处都是flex字眼,它是个什么东西呢?

public extension UIView {
    public var flex: Flex {
        if let flex = objc_getAssociatedObject(self, &flexLayoutAssociatedObjectHandle) as? Flex {
            return flex
        } else {
            let flex = Flex(view: self)
            objc_setAssociatedObject(self, &flexLayoutAssociatedObjectHandle, flex, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return flex
        }
    }
}
复制代码

用了大家喜闻乐见的Runtime关联属性,给UIView增加了一个叫flex的属性,有的话直接返回,没有的话,立即创建一个再返回,初始化方法传了view本身进去:

public private(set) weak var view: UIView?
private let yoga: YGLayout

init(view: UIView) {
    self.view = view
    self.yoga = view.yoga
    
    // Enable flexbox and overwrite Yoga default values.
    yoga.isEnabled = true
}
复制代码

很简单的代码,view强引用flexflex对象弱引用view,调用YogaKit的方法开启Flexbox布局。

然后是这个常用到的方法:

public func define(_ closure: (_ flex: Flex) -> Void) -> Flex {
    closure(self)
    return self
}
复制代码

传进来一个闭包,闭包要求有一个Flex对象做参数给它,然后执行这个闭包,把自己(flex)传给闭包,然后返回自己(flex)。简单来说就是为了让代码具有缩进层次,阅读起来更具辨识度。

?还要特别注意的是这两个方法:

// 1
public func addItem() -> Flex {
    let view = UIView()
    return addItem(view)
}

// 2
public func addItem(_ view: UIView) -> Flex {
    if let host = self.view {
        host.addSubview(view)
        return view.flex
    } else {
        preconditionFailure("Trying to modify deallocated host view")
    }
}
复制代码

先看2,把参数view添加到flex所属的view,作为它的子view,然后返回子view的flex对象,换言之,a.addItem(b).marginTop(10)的效果是把b作为a的subview,然后设置b的顶部外边距为10

再看1,不传参数的话,新建一个UIView并传给2的方法,换言之,a.addItem().height(1)的效果是添加一个view到a里,这个view的高度是1,匿名,除了asubviews数组,没有其他引用可以访问到这个view了。

Flex类剩余的其他扩展方法基本上都是转化为操作YogaKit的代码,让你用得省心,写得舒心。

这么说来可能很抽象,不过不要紧,下面我们就开始真枪实弹搞对象。

布局基础演示

布局方向 - direction

假设我们现在随便放蓝、绿、洋红三个UIView,代码如下:

import UIKit
import FlexLayout

class DemonstrationVC: UIViewController {
    fileprivate var rootFlex = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor(white: 0.96, alpha: 1)
        view.addSubview(rootFlex)
        
        let blueView = UIView()
        blueView.backgroundColor = .blue
        let greenView = UIView()
         greenView.backgroundColor = .green
        let magentaView = UIView()
        magentaView.backgroundColor = .magenta
        // 1
        rootFlex.flex.addItem(blueView).width(100).height(50)
        rootFlex.flex.addItem(greenView).width(100).height(50)
        rootFlex.flex.addItem(magentaView).width(100).height(50)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        rootFlex.frame = view.bounds
        rootFlex.flex.layout()
    }
}
复制代码

上面的代码我只粘贴这一次了,为了节省篇幅,后文的讲解基本只展示布局所需的核心代码。看看效果:

Inspector看看层级:

可以看到没有避开导航栏,子view是从父view(rootFlex)的左上角开始布局的,方向是纵向,一个view挨着一个排列。

因为Yoga在移动端是【以纵向作为主轴(main-axis)】的,相当于rootFlex默认设置了.direction(.column)了,下面我们来试试改变主轴为横向,并且粗暴地避开导航栏,在// 1下面插入两行再Command + R

rootFlex.flex.direction(.row)
rootFlex.flex.marginTop(64)
复制代码

可以看到rootFlexY值已经是64了,或许你已经注意到了,我从未提及的那一小撮代码:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    rootFlex.frame = view.bounds
    rootFlex.flex.layout()
}
复制代码

简单来说就是,有行为引起viewDidLayoutSubviews执行的时候,rootFlex把自己设置成和self.view一样大小,然后调用FlexLayoutlayout(mode:)方法布局。

这个方法会根据你之前曾经设置给flex的所有属性(外边距,宽度,高度等),按参数mode指定的方式开始计算布局,调整view的frame。

参数mode后面再说,一步步来嘛。

说好的链式调用呢?下面来改成等价的写法:

//        rootFlex.flex.direction(.row)
//        rootFlex.flex.marginTop(64)
//        rootFlex.flex.addItem(blueView).width(100).height(50)
//        rootFlex.flex.addItem(greenView).width(100).height(50)
//        rootFlex.flex.addItem(magentaView).width(100).height(50)
        
rootFlex.flex.direction(.row).marginTop(64).define { flex in
    flex.addItem(blueView).width(100).height(50)
    flex.addItem(greenView).width(100).height(50)
    flex.addItem(magentaView).width(100).height(50)
}
复制代码

有没有觉得多年白内障突然一觉醒来就视力5.2了?

唔,夸张了点,但是这样写很明显容易理解:rootFlex的主轴方向是横向,rootFlex顶部外边距是64,它添加了3个子view……

主轴对齐方式 - justifyContent

现在我接到新需求,需要让这三个view靠屏幕右边,一个挨着一个,怎么办呢?

因为我们的rootFlex主轴布局方向是.row横向的,要修改主轴的item对齐方式就要用.justifyContent()来修改:

rootFlex.flex.direction(.row).justifyContent(.end).marginTop(64).define { flex in
    flex.addItem(blueView).width(100).height(50)
    flex.addItem(greenView).width(100).height(50)
    flex.addItem(magentaView).width(100).height(50)
}
复制代码

如果不设置justifyContent,那么它默认是.start的,其他的模式你可以对着文档自行试试就明白了。

侧轴(cross-axis)对齐方式 - alignItems

侧轴就是和主轴90°交叉的那个轴,rootFlex主轴是横向****,那么它的侧轴就是纵向,通过.alignItems()来设置,默认是.stretch(拉伸)的,啥意思?我们去掉三个view的侧轴方向的尺寸试试(这里就是纵向的尺寸,即高度啦):

rootFlex.flex.direction(.row).justifyContent(.end).marginTop(64).define { flex in
    flex.addItem(blueView).width(100)
    flex.addItem(greenView).width(100)
    flex.addItem(magentaView).width(100)
}
复制代码

wow,好暴力……要注意的是,baseline模式是废的,Yoga并没有实现它,所以嘛,憧憬一下就好了。

问题又来了,我们的绿view要放头顶,哦,你们的,我头顶不要绿……

rootFlex.flex.direction(.row).justifyContent(.end).marginTop(64).define { flex in
    flex.addItem(blueView).width(100)
    flex.addItem(greenView).width(100).height(50)
    flex.addItem(magentaView).width(100)
}
复制代码

高度的优先级会比侧轴要高,你明确指定了高度,它就不会拉长你了:

试试其他值,唔,让你们中间绿:

rootFlex.flex.direction(.row).justifyContent(.end).alignItems(.center)
    .marginTop(64).define { flex in
    flex.addItem(blueView).width(100)
    flex.addItem(greenView).width(100).height(50)
    flex.addItem(magentaView).width(100)
}
复制代码

哎呀,怎么只有一片绿,不见了其他两个小伙伴?因为你没设置他们的高度呀……重新设置他们的高度为80,不贴代码了,看疗效:

唔,这时候我还是想绿在头顶怎么办?可以用自己的.alignSelf()来覆盖父盒子的.alignItems()设置:

rootFlex.flex.direction(.row).justifyContent(.end).alignItems(.center)
    .marginTop(64).define { flex in
    flex.addItem(blueView).width(100).height(80)
    flex.addItem(greenView).width(100).height(50).alignSelf(.start)
    flex.addItem(magentaView).width(100).height(80).alignSelf(.end)
}
复制代码

?不错不错

--

自动换行 - wrap

先来点准备随机色的代码:

/// 搞些浅色的随机色
fileprivate extension UIColor {
    static var random: UIColor {
        return UIColor(red: randomFloat(), green: randomFloat(),
                       blue: randomFloat(), alpha: 1)
    }
    
    static func randomFloat() -> CGFloat {
        return 1 - CGFloat(arc4random_uniform(200)) / 255.0
    }
}
复制代码

然后我们来排布12个按钮看看是什么效果:

// 生成12个按钮, 随机背景色
let views: [UIButton] = (0..<12).map{
    let v = UIButton()
    v.backgroundColor = .random
    v.setTitle(String($0), for: .normal)
    return v
}
    
rootFlex.flex.direction(.row).marginTop(64).define { flex in
    for v in views {
        flex.addItem(v).width(70).height(50)
    }
}
复制代码

可以看到subview超过了父viewrootFlex的显示范围,想让它在剩余空间放不下下一个的情况下自动换行的话,可以用.wrap

rootFlex.flex.direction(.row).wrap(.wrap).marginTop(64).define { flex in
    for v in views {
        flex.addItem(v).width(70).height(50)
    }
}
复制代码

当然你也可以用wrapReverse,那么左上角的就是1110……这样排列了。

唔,如果你觉得49右边留白太难看,可以考虑下主轴居中:

rootFlex.flex.direction(.row).justifyContent(.center).wrap(.wrap).marginTop(64).define { flex in
    ...
}
复制代码

你就会惊喜地发现它换了一种难看的方式。

可以在按钮之间加点间距(为了多出一个,我把前面数组改成13个按钮了):

rootFlex.flex.direction(.row).wrap(.wrap).marginTop(64).define { flex in
    for v in views {
        flex.addItem(v).width(70).height(50).marginRight(10).marginTop(10)
    }
}
复制代码

当这种多行的情况你想控制内容整体的侧轴布局时,可以用.alignContent试试:

rootFlex.flex.direction(.row).wrap(.wrap).alignContent(.spaceAround)
            .marginTop(64).define { flex in
    ...
}
复制代码

其他值请自行测试咯。

增长和压缩权重 - grow, shrink

让我我们回到三点式做演示:

let viewA = UIView()
viewA.backgroundColor = .blue
let viewB = UIView()
viewB.backgroundColor = .green
let viewC = UIView()
viewC.backgroundColor = .magenta
    
rootFlex.flex.direction(.row).marginTop(64).define { flex in
    flex.addItem(viewA).width(80).height(50)
    flex.addItem(viewB).width(80).height(50)
    flex.addItem(viewC).width(80).height(50)
}
复制代码

我想左右护法固定宽度,中间的绿自动获得父盒子的剩余空间,就要用到.grow

rootFlex.flex.direction(.row).marginTop(64).define { flex in
    flex.addItem(viewA).width(80).height(50)
    flex.addItem(viewB).height(50).grow(1)
    flex.addItem(viewC).width(80).height(50)
}
复制代码

试试让洋红view也分一杯羹:

rootFlex.flex.direction(.row).marginTop(64).define { flex in
    flex.addItem(viewA).width(80).height(50)
    flex.addItem(viewB).height(50).grow(2)
    flex.addItem(viewC).height(50).grow(1)
}
复制代码

除了蓝色的宽度之后,父盒子剩余的宽度分2份给绿色view,分1份给洋红view。

.shrink的作用是在父盒子(superview)空间不足以放下所有子盒子(subview)的时候,如何压缩子盒子。假设我们把三个view的宽度都设置为150rootFlex宽度只有375,肯定是放不下最后一个的,现在设置当放不下的时候压缩绿色的宽度:

rootFlex.flex.direction(.row).marginTop(64).define { flex in
    flex.addItem(viewA).width(150).height(50)
    flex.addItem(viewB).width(150).height(50).shrink(1)
    flex.addItem(viewC).width(150).height(50)
}
复制代码

布局实战 - 小说简介

上节说了一些基本概念,本节结合一个简单界面来演示一下如何用FlexLayout来布局。

如下图,这个是起点iOS端的小说介绍页(不会被告吧?),我们只用一部分来做布局演示,状态栏和导航栏我们忽略掉:

我个人的做法是,先在脑海里按盒子模型来给它的结构大概拆分成不同的盒子组合,我看起来是这样的:

灰色的就是背景图啦(实际上好像没有蔓延到淡红色的描述那里,后文再研究),洋红(紫色?)我画少了一条,别在意这种细节。

整体父盒子是白色,主轴为纵向,有两个子盒子,由子盒子撑开父盒子高度,据此可以写出核心代码如下:

import UIKit
import FlexLayout


class NovelSummaryVC: UIViewController {
    fileprivate var rootFlex = UIView()
    fileprivate var summaryView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor(white: 0.96, alpha: 1)
        view.addSubview(rootFlex)
        
        rootFlex.flex.marginTop(64).define { flex in
            flex.addItem(summaryView).height(30%).backgroundColor(.white).define { flex in
                
            }
            flex.addItem().grow(1).backgroundColor(.lightGray)
        }
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        rootFlex.frame = view.bounds
        rootFlex.flex.layout()
    }
}
复制代码

我们的目标是白色的summaryView,由于它还没有subview来撑开它的大小,所以我先给它一个30%来做做样子,不然你都看不见它啦。还有.backgroundColor()也是很方便用于布局时候的调试,调整完成之后去掉就行啦。

接着我们来看看浅绿色的盒子:

  • 它的主轴方向是横向的,两边距离父盒子外边距为15
  • 封面图片view应该是足以撑开整个浅色盒子的,宽高比例测量出来是170:230,宽度占父盒子的24.6%(170/690),高度不必指定,设定宽高比之后会自动计算出来,与蓝色view的间距是22
  • 除了封面图片view(和22间距)之后,剩余空间就是蓝色view的,蓝色view的高度假设一定能放得下5个洋红盒子

// VC添加一个属性
fileprivate let coverImgV = UIImageView()

// 布局代码
rootFlex.flex.marginTop(64).define { flex in
    flex.addItem(summaryView).height(30%).backgroundColor(.white).define { flex in
        // 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15
        flex.addItem().direction(.row).marginHorizontal(15).backgroundColor(.green).define { flex in
            // 封面图, 右外边距22
            flex.addItem(coverImgV).marginRight(22).width(24.6%).aspectRatio(17/23).backgroundColor(.lightGray)
            // 浅蓝色盒子
            flex.addItem().grow(1).shrink(1).backgroundColor(.blue)
        }
    }
    flex.addItem().grow(1).backgroundColor(.lightGray)
}
复制代码

唔,模拟器尺寸和设计图似乎很像样了,对了,我发现上面的示意图确实需要修正,背景图不应该覆盖到浅红色view的。接下来搞浅红色的描述view:

  • 它顶部距离浅绿色view大概12
  • 内部有一个UILabel,单行,超长就截断,文字居中,顶部底部距离父盒子顶部还有点边距,直接设置父盒子的内边距(padding)处理

先来搞一个UILabel,顺便把封面图片搞上,免得一坨坨颜色那么难看,不敢用人家封面,免得侵权呀,自己搞一个。

// VC增加属性
fileprivate let descLabel = UILabel()

coverImgV.image = UIImage(named: "novel_cover")
descLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
descLabel.text = "“玄门羽衣白云心,一琴一剑一丹青”"
descLabel.textAlignment = .center
descLabel.textColor = UIColor(0.77, 0.64, 0.48, 1)

rootFlex.flex.marginTop(64).define { flex in
    flex.addItem(summaryView).height(30%).backgroundColor(.white).define { flex in
        // 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15
        flex.addItem().direction(.row).marginHorizontal(15).backgroundColor(.green).define { flex in
            // 封面图, 右外边距22
            flex.addItem(coverImgV).marginRight(22).width(24.6%).aspectRatio(17/23)
            // 浅蓝色盒子
            flex.addItem().grow(1).shrink(1).backgroundColor(.blue)
        }
        // 浅红色盒子, padding(上, 左, 下, 右)
        flex.addItem().marginTop(12).padding(12, 10, 8, 10)
            .backgroundColor(UIColor(white: 0.9, alpha: 1)).define { flex in
            flex.addItem(descLabel)
        }
    }
    flex.addItem().grow(1).backgroundColor(.lightGray)
}
复制代码

行了,我知道封面丑。

这个时候我们summaryView盒子的高度已经可以推算出来了,现在把height(30%)去掉可以了,顺便那个绿底色也可以干掉了,辣眼睛:

再看蓝色view:

  • 5个subview,主轴方向为.row,等间距分布对齐为.spaceBetween
  • 星星什么的我就懒得搞了,找个图糊弄一下

先定义一下相关的view先(真不想写这种多余的注释):

/// 书名
fileprivate let bookNameLabel = UILabel()
/// 作者
fileprivate let authorLabel = UILabel()
/// 级别
fileprivate let levelLabel = UILabel()
/// 阅读量
fileprivate let readCountLabel = UILabel()
/// 分类
fileprivate let categoryLabel = UILabel()
/// 字数, 状态
fileprivate let wordCoundAndStateLabel = UILabel()        

// 统一设置样式
fileprivate func configLabel(_ label: UILabel, text: String) {
    label.text = text
    label.font = UIFont.systemFont(ofSize: 12)
    label.textColor = .white        
}
复制代码

然后设置一下文本,添加到蓝色view里:

configLabel(bookNameLabel, text: "心魔")
bookNameLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
configLabel(authorLabel, text: "沁纸花青")
levelLabel.text = "Lv4"
levelLabel.font = UIFont.systemFont(ofSize: 10)
levelLabel.textColor = UIColor(0, 0.8, 1, 1)
levelLabel.textAlignment = .center
levelLabel.layer.borderColor = UIColor(0, 0.8, 1, 1).cgColor
levelLabel.layer.borderWidth = 1
levelLabel.layer.cornerRadius = 4
levelLabel.layer.masksToBounds = true
configLabel(readCountLabel, text: "466万人读过")
configLabel(categoryLabel, text: "仙侠 | 幻想修真")
configLabel(wordCoundAndStateLabel, text: "266.0万字 | 连载")

rootFlex.flex.marginTop(64).define { flex in
    flex.addItem(summaryView).backgroundColor(.white).define { flex in
        // 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15
        flex.addItem().direction(.row).marginHorizontal(15).define { flex in
            // 封面图, 右外边距22
            flex.addItem(coverImgV).marginRight(22).width(24.6%).aspectRatio(17/23)
            // 浅蓝色盒子
            flex.addItem().justifyContent(.spaceBetween).grow(1).shrink(1).backgroundColor(.blue).define { flex in
                flex.addItem(bookNameLabel)
                flex.addItem(authorLabel)
                flex.addItem(readCountLabel)
                flex.addItem(categoryLabel)
                flex.addItem(wordCoundAndStateLabel)
            }
        }
        // 浅红色盒子, padding(上, 左, 下, 右)
        flex.addItem().marginTop(12).padding(12, 10, 8, 10).backgroundColor(.black).define { flex in
            flex.addItem(descLabel)
        }
    }
    flex.addItem().grow(1).backgroundColor(.lightGray)
}
复制代码

好像还可以,蓝色太辣眼睛了,我们搞个背景图上来,然后干掉蓝色背景:

// VC增加一个属性
fileprivate let bgImgV = UIImageView(image: UIImage(named: "novel_bg"))

rootFlex.flex.marginTop(64).define { flex in
    flex.addItem(summaryView).backgroundColor(.white).define { flex in        
    // 1
    flex.addItem(bgImgV).position(.absolute).left(0).top(0).width(100%).height(100%)
        // 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15
        flex.addItem().direction(.row).marginHorizontal(15).define { flex in
            ...(略)...
复制代码

哟呵,好像帅了不少,先来看看这个.position(.absolute)是什么意思。

.position的默认值是relative,就是默认情况下你不写就是相对布局的意思啦。

这里我们给bgImgV的布局方式明确指定为absolute绝对布局,意思就是它的位置不再受限于上一个盒子,下一个盒子,而是相对于父盒子的左上角。

然后通过top()left()right()bottom()这几个方法来设置自己的位置,我这里设置的就是左边和顶边距离父盒子为0,然后宽度高度和父盒子一样。

唔,好像顶部太贴近导航栏了,给浅绿色盒子搞个顶部外边距就行了:

// 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15
flex.addItem().direction(.row).marginTop(12).marginHorizontal(15).define { flex in
    ...(略)...
复制代码

顺眼多了,对了,还差名牌包包LV还没上呢,套一个盒子就好了:

// 浅蓝色盒子
flex.addItem().justifyContent(.spaceBetween).grow(1).shrink(1).define { flex in
    flex.addItem(bookNameLabel)
    flex.addItem().direction(.row).define { flex in
        flex.addItem(authorLabel)
        flex.addItem(levelLabel)
    }
    flex.addItem(readCountLabel)
    flex.addItem(categoryLabel)
    flex.addItem(wordCoundAndStateLabel)
}
复制代码

这个Lv有点挫呀……给左右加点内边距吧,还有左边也不要太靠近花青姐姐啦:

// 浅蓝色盒子
flex.addItem().justifyContent(.spaceBetween).grow(1).shrink(1).define { flex in
    flex.addItem(bookNameLabel)
    flex.addItem().direction(.row).define { flex in
        flex.addItem(authorLabel)
        flex.addItem(levelLabel).paddingHorizontal(6).marginLeft(8)
    }
    flex.addItem(readCountLabel)
    flex.addItem(categoryLabel)
    flex.addItem(wordCoundAndStateLabel)
}
复制代码

好像没那么挫了。要注意的是,LabeltextAlignment要是.center才显得出水平方向的padding效果,不然增加的宽度全部在右边了。

好了,星星相信你也会搞了,懒得演示了。

下面我们来演示一下,有时候descLabel不需要显示的情况。

先给浅红色盒子做一个变量来方便引用它:

// VC添加一个属性
fileprivate let descContainer = UIView()


// 浅红色盒子, padding(上, 左, 下, 右)
flex.addItem(descContainer).marginTop(12).padding(12, 10, 8, 10)
    .backgroundColor(.black).define { flex in
    flex.addItem(descLabel)
}

// viewDidLoad的最后添加
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    self.descContainer.flex.isIncludedInLayout = false
    self.descContainer.isHidden = true
    self.view.setNeedsLayout()
    self.view.layoutIfNeeded()
}
复制代码

哎哟,底部外边距没有了……因为是靠浅红色view的顶部外边距做的,现在我们改成浅绿色盒子的底部外边距吧:

// 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15,顶底各12外边距
flex.addItem().direction(.row).marginVertical(12).marginHorizontal(15).define { flex in
    ...(略)...
}
// 浅红色盒子, padding(上, 左, 下, 右)
flex.addItem(descContainer).padding(12, 10, 8, 10)
...(略)...
复制代码

唔,可以了,好像少了点啥……封面右下角有个大V!

没事,我今天才摸索怎么用Sketch,我给你画了个高清无码的,随便用:

// VC增加一个属性
fileprivate let verifiedImgV = UIImageView(image: UIImage(named: "novel_v"))


// 浅绿色盒子, 横向排列子盒子, 距离父盒子(summaryView)左右外边距为15,顶底各12外边距
flex.addItem().direction(.row).marginVertical(12).marginHorizontal(15).define { flex in
    flex.addItem().marginRight(22).width(24.6%).aspectRatio(17/23).define { flex in
        // 封面图, 右外边距22
        flex.addItem(coverImgV)
        flex.addItem(verifiedImgV).position(.absolute).right(-5).bottom(-5).size(20)
    }
    // 浅蓝色盒子
    ...(略)...
复制代码

原本的封面盒子现在用来装两个东西了,一个是封面view,一个是绝对定位的verifiedView,宽高都是20,在右下角,超出superview了。


完整代码在github


很简单的一个界面,啰啰嗦嗦废话多,希望小白都能看懂。

下一篇我们来讲讲layout(mode:)的参数作用,以及如何结合到UIScrollView使用。

文中用到的所有素材均为学习使用,请不要用于商业用途,否则后果说不定很严重,自负啊!

转载于:https://juejin.im/post/5b51aa2c6fb9a04fea5898bb

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值