FlexLayout入门系列(四)

FlexLayout入门系列(四)

本篇我们来搞搞UITableView的不等高cell

其实有前几篇打底,你看FlexLayout的官方文档或Demo应该轻松加愉快了。看我写的会比较啰嗦,不过我还是要坚持写完这最后一篇的,给本系列画上一个圆满的问号。

这次我们要模仿的是掘金iOS端的【沸点】-【推荐】界面:

juejin_recommend

你应该可以在脑海里想到怎么拆分盒子了,不过我还是循例画一下吧:

嗯,就是一个cell,有点要注意的是,紫色和青色的单图以及9图,三者只会有一个是可见的。
9图不足的时候,如果是4图要特殊排列(田字)
蓝框的文字至少1行,最多似乎4~5行。话题标签不一定存在。

为了方便加载头像,pod引入Kingfisher

mock数据我为你准备好啦,从掘金抓来的,然后准备了一个数据提供者来加载json

class TweetStore {

static let shared = TweetStore()
private(set) var items = [TweetItem]()

private init() {
loadFromFile()
}

private func loadFromFile() {
let url = Bundle.main.url(forResource: "tweets.json", withExtension: nil)!
let data = try! Data(contentsOf: url)
items = try! JSONDecoder().decode([TweetItem].self, from: data)
}

var count: Int {
return items.count
}

func item(at index: Int) -> TweetItem {
return items[index]
}
}

我把每一条动态叫做TweetItem吧,至于Codable这东西我这是第二次使用,应该大家比我熟得多了,不班门弄斧了。

UITableView就更不用说了,各位都是轻车熟路,老马识途,老汉…………额,看关键代码(直接看Demo代码也可以):

view.addSubview(tbv)
tbv.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNonzeroMagnitude))
//tbv.estimatedRowHeight = 0
tbv.estimatedSectionFooterHeight = 0
tbv.estimatedSectionHeaderHeight = 0
tbv.separatorStyle = .none
tbv.register(TweetCell.self, forCellReuseIdentifier: "\(TweetCell.self)")
tbv.delegate = self
tbv.dataSource = self


extension TweetListVC: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return TweetStore.shared.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = TweetStore.shared.item(at: indexPath.section)
let cell = tableView.dequeueReusableCell(withIdentifier: "\(TweetCell.self)", for: indexPath) as! TweetCell
cell.configWith(item)
return cell
}
// func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// return 60
// }

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 10
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNonzeroMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}
}

都是很简单的代码,稍微注意的是,我把heightForRowAt注释了,tbv.estimatedRowHeight = 0屏蔽了估算行高。

然后是cell的部分代码:

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configUI()
}


private func configUI() {
selectionStyle = .none
contentLabel.numberOfLines = 0
...(一些基本样式设置略)...

contentView.flex.paddingHorizontal(13).define { flex in
flex.addItem().direction(.row).alignItems(.center).marginTop(13).define{ flex in
flex.addItem(avatarImgV).size(40)
flex.addItem().justifyContent(.spaceBetween).alignSelf(.stretch).marginHorizontal(10)
.paddingVertical(2).grow(1).shrink(1).define{ flex in
flex.addItem(nameLabel)
flex.addItem(userIntroLabel)
}
flex.addItem(followBtn).height(20).paddingHorizontal(6)
flex.addItem(menuBtn).marginLeft(5).marginRight(-5)
}
flex.addItem(contentLabel).marginTop(13).maxHeight(130).grow(1).shrink(1)
}
}

// 根据动态的具体信息,赋值到各view
func configWith(_ item: TweetItem) {
avatarImgV.kf.setImage(with: URL(string: item.user?.avatarLarge ?? ""),
options: [.transition(.fade(0.3))])
nameLabel.text = item.user?.username
let job = item.user?.jobTitle ?? ""
let company = item.user?.company ?? ""
if job.isEmpty && company.isEmpty {
userIntroLabel.text = item.createdAt
} else {
userIntroLabel.text = [job, company].joined(separator: " @ ")
.trimmingCharacters(in: CharacterSet(charactersIn: " @"))
}
let isFollowed = item.user?.currentUserFollowed ?? false
followBtn.setTitle(isFollowed ? "已关注" : "+ 关注", for: .normal)
contentLabel.text = item.content
}


func layout() {
contentView.flex.layout(mode: .adjustHeight)
}

override func sizeThatFits(_ size: CGSize) -> CGSize {
contentView.frame.size = size
layout()
return contentView.frame.size
}

override func layoutSubviews() {
contentView.frame = bounds
layout()
}

可以看到contentLabel有个maxHeight(130),顾名思义,它的最大高度不能超过130咯。

这里重点就是布局计算方式是adjustHeight,根据contentView的宽度来限制子盒子,然后布局,最后得出子盒子的最大高度,赋值给contentViewheight

那么contentView的宽度从哪里来?layoutSubviews里设置了它的frame为cell的bounds,当然就包括宽度了。

然后另一个导致重新计算布局的方法是sizeThatFits(_ size:),传进来一个size,赋值给contentView,然后计算布局,最后返回contentView的最终尺寸(其实宽度不变,只是高度变了)。

cell实现了sizeThatFits(_ size:),UITableView在iOS 11+又开启了自动计算行高,自动开启估算行高,跑起来就是这样的了:

在iOS 10跑一下就跪了:

还是手动开启自动计算吧:

tbv.rowHeight = UITableView.automaticDimension
tbv.estimatedRowHeight = 300

compare_1

不过你上下滑几下就会发现不正常了:

肯定是复用导致的问题啦……其实是由于Yoga的优化计算机制导致的。

具体是怎样的机制,得你去看源码或者文档咯,据老夫的猜测,应当是计算了并布局了一次之后,它认为你的界面的子view大部分的frame是不会变化的,如果有变化,你得调用markdirty()告诉Yoga,这个view已经脏了,下次再叫Yoga计算布局的时候,它就只会计算这部分脏了的节点(以及收到它尺寸影响的其他节点),以提高布局计算速度。

还要注意的是,markDirty()只对叶子节点生效。

好了,都是老夫的猜测而已,具体情况请查阅文档。

我们回到cell看看,主要是contentLabel的文字内容长度变化了,导致了它的高度变化了,所以我们应该对它进行markDirty()

contentLabel.flex.markDirty()
setNeedsLayout()

setNeedsLayout()就是告诉UIKit下个更新周期(update cycle)要重新布局拉……会导致重新走layoutSubviews,从而导致layout()

咦?你问我userIntroLabel的内容也改变了,为什么不用markDirty()?这个嘛,骚年你认真看看,它本来的size就已经固定了,1行高,就那么宽:

假设这里名字和简介是紧挨着横排的,那才有必要markDirty()

好了,本篇完,搬砖去吧。

拉着我干嘛?啥?你还想看图片的布局?行,那老夫就勉为其难提点一二吧🙏……

我先用话题标签这个做个示范,圆角性能什么的,自己想嘛,我一讲布局demo的不包这项服务的呀:

topicBtn.layer.cornerRadius = 12.5
topicBtn.layer.borderColor = UIColor(0, 0.5, 1, 1).cgColor
topicBtn.layer.borderWidth = 1

contentView.flex.paddingHorizontal(13).define { flex in
...(略)...
flex.addItem(contentLabel).marginTop(13).maxHeight(130).grow(1).shrink(1)
flex.addItem(topicBtn).alignSelf(.start).height(25).marginTop(12).paddingHorizontal(10)
}


func configWith(_ item: TweetItem) {
...(略)...
// 默认隐藏并且不布局话题按钮
topicBtn.flex.isIncludedInLayout = false
topicBtn.isHidden = true
if let topicTitle = item.topic?.title {
topicBtn.flex.isIncludedInLayout = true
topicBtn.isHidden = false
topicBtn.setTitle(topicTitle, for: .normal)
}
contentLabel.text = item.content
contentLabel.flex.markDirty()
setNeedsLayout()
}

呐,就是这样啦,flex.isIncludedInLayout = true就可以让它不参与到布局计算中,isHidden就让它隐藏起来。每次隐藏一个view都要写两行代码挺麻烦的,extension to the rescue!

extension Flex {
/// 是否进行布局计算及显示
public var isLayoutAndShow: Bool {
set {
isIncludedInLayout = newValue
self.view?.isHidden = !newValue
}
get {
return isIncludedInLayout && (self.view?.isHidden ?? false)
}
}
}


// 默认隐藏并且不布局话题按钮
topicBtn.flex.isLayoutAndShow = false
if let topicTitle = item.topic?.title {
topicBtn.flex.isLayoutAndShow = true
topicBtn.setTitle(topicTitle, for: .normal)
}

效果一级棒啦!😌

横竖图片相信你也知道怎么做了吧?本来打算用两个UIImageView的,想想一个似乎也够用,不多说,直接上code:

contentView.flex.paddingHorizontal(13).define { flex in
...(略)...
flex.addItem(contentLabel).marginTop(13).maxHeight(130).grow(1).shrink(1)
flex.addItem(singleImgV).marginTop(10)
flex.addItem(topicBtn).alignSelf(.start).height(25).marginTop(12).paddingHorizontal(10)
}


func configWith(_ item: TweetItem) {
...(略)...
singleImgV.flex.isLayoutAndShow = false
if item.pictures.isEmpty {
// 没有图片

} else if item.pictures.count == 1 {
// 1张图片
singleImgV.flex.isLayoutAndShow = true
// 提取宽高尺寸
let url = item.pictures[0]
let widths = url.regexFind(pattern: "w=([0-9]+)", atGroupIndex: 1)
let heights = url.regexFind(pattern: "h=([0-9]+)", atGroupIndex: 1)
// 重新计算图片尺寸
if let width = widths.first, let height = heights.first, let wI = Int(width), let hI = Int(height) {
let (w, h) = scaleSize(CGSize(width: CGFloat(wI), height: CGFloat(hI)),
toMax: CGSize(width: ScreenWidth * 0.65, height: 170))
singleImgV.flex.width(w).height(h)
}
singleImgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
} else if item.pictures.count == 4 {
// 4张图片

} else {
// 多张图片
}

...(略)...
setNeedsLayout()
}

图片尺寸限制的计算方法是拍拍脑袋随便写的:

/// 把origSize按比例缩放到maxSize限定的范围内
private func scaleSize(_ origSize: CGSize, toMax maxSize: CGSize) -> (CGFloat, CGFloat) {
var w = origSize.width ; var h = origSize.height
let maxWidth = ScreenWidth * 0.65
let maxHeight: CGFloat = 170
let ratio = w / h
if w > h, w > maxWidth {
w = maxWidth
h = w / ratio
} else if h > w, h > maxWidth {
h = maxHeight
w = h * ratio
}
return (w, h)
}

哦,还少给你一个正则搜索是吗:

extension String {
/// 正则查找,返回匹配结果数组,可以指定返回匹配的第groupIndex组
func regexFind(pattern: String, atGroupIndex index: Int = 0, options: NSRegularExpression.Options = []) -> [String] {
var result = [String]()
let regex = try! NSRegularExpression(pattern: pattern, options: options)
regex.enumerateMatches(in: self, options: [], range: NSRange(location: 0, length: self.count)) {
(checkingRes, flag, shouldStop) in
guard let range = checkingRes?.range(at: index) else { return }
let start = self.index(self.startIndex, offsetBy: range.lowerBound)
let end = self.index(self.startIndex, offsetBy: range.upperBound)
let substr = String(self[start..<end])
result.append(substr)
}
return result
}
}

来个掉帧的演示:

下面来讲讲9个图片怎么说唱,哦,不是rap,是wrap……

  • 图片之间的间距是4
  • 图片的宽度为【(屏幕宽度 - 左右外边距 - 2个间距) / 列数】

然后我的代码是:

fileprivate let imagesContainer = UIView()
fileprivate var imgs = [UIImageView]()

private func configUI() {
...(略)...
// 生成9个图片view
imgs = (0..<9).map {
let imgv = UIImageView()
imgv.tag = $0
return imgv
}

contentView.flex.paddingHorizontal(13).define { flex in
...(略)...
flex.addItem(contentLabel).marginTop(13).maxHeight(130).grow(1).shrink(1)
flex.addItem(imagesContainer).direction(.row).wrap(.wrap).define{ flex in
// 每个图片view宽度
let imgWidth = CGFloat(floor(ScreenWidth - 2 * 13/*左右外边距*/ - 2 * imageSpacing) / 3/*列数*/)
for imgV in imgs {
flex.addItem(imgV).width(imgWidth).aspectRatio(1).marginRight(imageSpacing)
.marginTop(imageSpacing).backgroundColor(.lightGray)
}
}
flex.addItem(singleImgV).marginTop(10)
...(略)...
}
}

设定了每个图片的宽度,1:1,右边距和上边距是4,自动换行排列,搞个背景色跑起来看看先……

咦?这也太不给面子了吧?看看Inspector怎么说:

imagesContainer的宽度是349,有奖口算:(375 - 13 * 2 - 4 * 2) / 3 = ?

= 341 / 3 = 113.666 截断得 113,说明图片的尺寸是正确的,放不下第三列的原因是,第三列的图片也有个右边距呀……
总共需要的宽度113 * 3 + 4 * 3 = 351,说明imagesContainer的宽度不足咯导致第三列换行放置了。

这个时候就要有骚操作登场了:

flex.addItem(imagesContainer).direction(.row).wrap(.wrap).marginRight(-2).define{ flex in

给右外边距增加了-2……结果imagesContainer的宽度就够351了,但是细心的你一看,右边距怎么和左边不同了!

唔,还记得被我们搞掉的666吗?113.666变成了113,三列就少了1.998咯,无限接近2。简单来说,现在最左边图片距离屏幕左边为13,最右边的图片距离屏幕最右边是15。

唉,骚操作搞多了,会被人反搞的……

flex.addItem(imagesContainer).direction(.row).wrap(.wrap).marginRight(-4)
.marginLeft(1).define{ flex in

意思就是,把imagesContainer整体加大一点(以免第三列图片再次换行),然后左边内边距留空1(相当于所有图片右移了1),加上父盒子的内边距13,就够14啦……14 + 113 * 3 + 4 * 2 = 361 = 375 - 14,呼,我都差点圆不回来了……

第一行的顶部有边距4了,我们再给imagesContainer顶部加10吧。

flex.addItem(imagesContainer).direction(.row).wrap(.wrap).marginRight(-4)
.marginLeft(1).marginTop(10).define{ flex in

不上图了,接着按图片数组的数量来显示隐藏就好了:

// 默认隐藏图片容器和里面所有图片
imagesContainer.flex.isLayoutAndShow = false
imgs.forEach{ $0.flex.isLayoutAndShow = false }
singleImgV.flex.isLayoutAndShow = false

if item.pictures.isEmpty {
// 没有图片
} else if item.pictures.count == 1 {
...(略)...
} else if item.pictures.count == 4 {
// 4张图片
} else {
// 多张图片
imagesContainer.flex.isLayoutAndShow = true
for (imgV, url) in zip(imgs, item.pictures) {
imgV.flex.isLayoutAndShow = true
imgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
}
}

好像我的mock数据里并没有刚好4张图片的情况呢……行吧,我再去编辑一下,复制几个待会再测试。

额,突然想起没有截图,掘金的4个图片的时候是按【田】字布局的,而我们目前的布局如果按9图布局把代码填充进去的话:

} else if item.pictures.count == 4 {
// 4张图片
imagesContainer.flex.isLayoutAndShow = true
for (imgV, url) in zip(imgs, item.pictures) {
imgV.flex.isLayoutAndShow = true
imgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
}
}

那可怎么办呢?皇上,还记得大明湖畔的夏雨荷吗?当imagesContainer的宽度不足以放下3列的时候它就换行了……来来来,骚操作走起:

} else if item.pictures.count == 4 {
// 4张图片
imagesContainer.flex.marginRight(0)
imagesContainer.flex.isLayoutAndShow = true
for (imgV, url) in zip(imgs, item.pictures) {
imgV.flex.isLayoutAndShow = true
imgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
}
} else {
// 多张图片
imagesContainer.flex.marginRight(-4)
imagesContainer.flex.isLayoutAndShow = true
for (imgV, url) in zip(imgs, item.pictures) {
imgV.flex.isLayoutAndShow = true
imgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
}
}

悄悄问圣僧,老夫骚不骚?

剩下的,给每个图片加个Tap,回调方法里获得tag,然后搞个闭包把tag做参数暴露到cell的属性blabla……自己搞呀,咱布局demo不包这个服务的。

底下还有个工具条是吧,我给你搞完它:

private func createButton(title: String, icon: String) -> UIButton {
let btn = UIButton()
btn.setTitle(title, for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 14)
btn.setTitleColor(UIColor(white: 0.75, alpha: 1), for: .normal)
btn.setImage(UIImage(named: icon), for: .normal)
return btn
}

private func configUI() {
...(略)...
likeBtn = createButton(title: "9", icon: "icon_like")
commentBtn = createButton(title: "1", icon: "icon_comment")
shareBtn = createButton(title: "", icon: "icon_share")

contentView.flex.paddingHorizontal(13).define { flex in
...(略)...
flex.addItem(topicBtn).alignSelf(.start).height(25).marginTop(12).paddingHorizontal(10)
// 分隔线
flex.addItem().height(1).marginTop(13).backgroundColor(UIColor(white: 0.92, alpha: 1))
flex.addItem().direction(.row).justifyContent(.spaceAround).height(38).define{ flex in
flex.addItem(likeBtn)
flex.addItem(commentBtn)
flex.addItem(shareBtn)
}
}

}

好像分隔线有点不对路?是因为contentView的整体水平内边距影响了它,不管了,你开动下脑筋就能搞定了。(P.S. 加多个盒子)

对了,文中一大堆链接好像很难看,我们也搞个【☍网页链接】呗,点击处理事件你自己搞哦,真男人,我只做表面功夫:



func configWith(_ item: TweetItem) {
...(略)...
// 替换内容里面的链接
let content = replaceLinkIn(text: targetStr,
font: UIFont.systemFont(ofSize: 16),
textColor: UIColor(white: 0.3, alpha: 1))
contentLabel.attributedText = content
contentLabel.flex.markDirty()
setNeedsLayout()
}


private func replaceLinkIn(text: String, font: UIFont, textColor: UIColor, onTap: ((String)->Void)?) -> NSMutableAttributedString {
let content = NSMutableAttributedString(string: text)
// 字体
let fontAttr: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor]
content.addAttributes(fontAttr, range: NSRange(location: 0, length: content.length))
let urls = text.findAllURLs()
for (url, subRange) in urls.reversed() {
// 去掉原始链接
content.replaceCharacters(in: subRange, with: "")
// 准备替换的字符串
let icon = NSTextAttachment()
icon.image = UIImage(named: "icon_link")
icon.bounds = CGRect(x: 0, y: font.descender, width: 14, height: 14)
let attr = NSMutableAttributedString()
attr.append(NSAttributedString(attachment: icon))
attr.append(NSAttributedString(string: "网页链接"))
attr.addAttributes([.foregroundColor: UIColor(red: 0.09, green: 0.49, blue: 1, alpha: 1)],
range: NSRange(location: 0, length: attr.length))
// 插入到被替换的原始链接的起始位置
content.insert(attr, at: subRange.location)
}
return content
}

似乎像模像样了,不过头像还是让我有点方,简单地搞个无棱的怎样?

func configWith(_ item: TweetItem) {
let roundProcessor = RoundCornerImageProcessor(cornerRadius: 20,
targetSize: CGSize(width: 40, height: 40))
avatarImgV.kf.setImage(with: URL(string: item.user?.avatarLarge ?? ""),
options: [.transition(.fade(0.3)),
.scaleFactor(UIScreen.main.scale),
.processor(roundProcessor),
.cacheSerializer(FormatIndicatedCacheSerializer.png)])
...(略)...
}

话说每次都要重新计算每个图片的宽高,好浪费资源……缓存一下?

class TweetItem: Codable {
true...(略)...
var attributedText: NSMutableAttributedString? // 带样式的内容
var picsSize: [String: CGSize] = [:] // 保存图片缩放后的尺寸

enum CodingKeys: String, CodingKey {
case uid, user, content, pictures, url, urlTitle, urlPic, commentCount, likedCount, isLiked, createdAt, updatedAt, topicId, topic, isTopicRecommend, folded
}
}


...(略)...
} else if item.pictures.count == 1 {
// 1张图片
singleImgV.flex.isLayoutAndShow = true
let url = item.pictures[0]
let key = url.md5
// 提取宽高尺寸
if let size = item.picsSize[key] {
singleImgV.flex.width(size.width).height(size.height)
} else {
let widths = url.regexFind(pattern: "w=([0-9]+)", atGroupIndex: 1)
let heights = url.regexFind(pattern: "h=([0-9]+)", atGroupIndex: 1)
// 重新计算图片尺寸
if let width = widths.first, let height = heights.first, let wI = Int(width), let hI = Int(height) {
let (w, h) = scaleSize(CGSize(width: CGFloat(wI), height: CGFloat(hI)),
toMax: CGSize(width: ScreenWidth * 0.65, height: 170))
singleImgV.flex.width(w).height(h)
// 缓存起来
item.picsSize[key] = CGSize(width: w, height: h)
}
}
singleImgV.kf.setImage(with: URL(string: url), options: [.transition(.fade(0.3)), .onlyLoadFirstFrame])
}

每次都要重新生成NSAttributedString,也好浪费资源……缓存一下

func configWith(_ item: TweetItem) {
...(略)...
// 先取一下是否有缓存
if let attrText = item.attributedText {
contentLabel.attributedText = attrText
} else {
// 太长就截断文本
let targetStr: String
if item.content.count <= 120 {
targetStr = item.content
} else {
let start = item.content.startIndex
let end = item.content.index(item.content.startIndex, offsetBy: 120)
targetStr = String(item.content[start..<end]) + "..."
}
// 替换内容里面的链接
let content = replaceLinkIn(text: targetStr,
font: UIFont.systemFont(ofSize: 16),
textColor: UIColor(white: 0.3, alpha: 1))
// 行距
let style = NSMutableParagraphStyle()
style.lineSpacing = 4
content.addAttributes([.paragraphStyle : style], range: NSRange(location: 0, length: content.length))
item.attributedText = content
contentLabel.attributedText = content
}

contentLabel.flex.markDirty()
setNeedsLayout()
}

呼……好像差不多了。上5s真机跑一下,跳帧😱,这个……得想办法优化了,比如简单地缓存行高什么的:

class TweetItem: Codable {
...(略)...
var cellHeight: CGFloat? // cell高度
...(略)...
}


class TweetListVC: UIViewController {
fileprivate let cellTemplate = TweetCell()
...(略)...

private func configUI() {
...(略)...
//tbv.rowHeight = UITableView.automaticDimension
tbv.estimatedRowHeight = 300
...(略)...
}
}


func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let item = TweetStore.shared.item(at: indexPath.section)
if let height = item.cellHeight { return height }
cellTemplate.configWith(item)
let size = cellTemplate.sizeThatFits(CGSize(width: tableView.bounds.width,
height: CGFloat.greatestFiniteMagnitude))
item.cellHeight = size.height
return size.height
}

大概意思就是,独立搞一个模板cell,需要计算行高的时候就用它套item去计算一次,得到的行高缓存起来。

各位爷请尽情施展您的奇技淫巧去优化,事实上对于这种组件动态位置比较多、滑动性能要求非常高的情况,用Texture(ASDK)才是正道吧?我demo也就凑合演示布局而已……求放过😅


完整代码在github


总结

FlexLayout好用吗?我觉得挺好用的,但是它也有自己的局限性,如:极度依赖于view的顺序与层级;大量的盒子嵌套会导致视图层级很深;不方便做动画;你的同事看不懂布局代码……

我一般是用在一些变化比较少的,规律整齐的界面上(或者说可以比较明显地用盒子来描述),其他的场景,我还是会使用AutolayoutPinLayout(又是另外一个坑)。

FlexLayout也可以和AutolayoutPinLayout混用的,这个要自己研究一下咯。

就写到这里了,入坑的话,后果自负啊……

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