FlexLayout入门系列(四)

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

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

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

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

嗯,就是一个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
复制代码

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

肯定是复用导致的问题啦……其实是由于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 {
	...(略)...
    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混用的,这个要自己研究一下咯。

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

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

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值