You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UICollectionView启用交互式键盘关闭时单元格动画异常问题

解决UICollectionView交互式键盘关闭时的单元格动画异常问题

我之前做聊天界面时也碰到过几乎一模一样的问题,这种异常动画本质是视图翻转变换和系统自动布局/键盘交互的布局更新逻辑冲突导致的。结合你给出的代码和尝试过的方案,我给你几个针对性的解决思路:

核心问题分析

你通过CGAffineTransform(scaleX: 1, y: -1)翻转整个CollectionView和单元格,虽然实现了聊天消息从底部往上排列的效果,但会让系统对CollectionView的contentOffset、contentSize的计算逻辑产生坐标反转。当开启交互式键盘关闭时,拖动键盘会动态改变CollectionView的可视区域,此时系统的自动布局动画和你手动缓存的单元格尺寸、翻转后的坐标系统不兼容,就会出现单元格“跳闪”或滑动动画。

解决方案:替换翻转方案,改用自定义FlowLayout实现底部布局

最彻底的解决方式是放弃视图翻转,直接让CollectionView的布局从底部开始排列,这样完全避免变换带来的坐标冲突。

1. 自定义聊天专用FlowLayout

创建一个继承自UICollectionViewFlowLayout的类,重写布局逻辑让元素从底部开始排列:

class ChatBottomUpFlowLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let originalAttributes = super.layoutAttributesForElements(in: rect),
              let collectionView = collectionView else {
            return originalAttributes
        }
        
        // 计算内容总高度和CollectionView可视高度
        let totalContentHeight = collectionView.contentSize.height
        let collectionViewVisibleHeight = collectionView.bounds.height - sectionInset.top - sectionInset.bottom
        
        // 翻转每个元素的Y坐标,实现从底部布局
        return originalAttributes.map { attributes in
            let newY = totalContentHeight - attributes.frame.maxY + collectionViewVisibleHeight + sectionInset.top
            let adjustedAttributes = attributes.copy() as! UICollectionViewLayoutAttributes
            adjustedAttributes.frame = CGRect(x: attributes.frame.origin.x,
                                             y: newY,
                                             width: attributes.frame.width,
                                             height: attributes.frame.height)
            return adjustedAttributes
        }
    }
    
    // 当CollectionView bounds变化时(比如键盘弹出/收起),重新计算布局
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

2. 修改CollectionView初始化代码

移除翻转相关的代码,改用自定义FlowLayout:

lazy var chatCollectionView: UICollectionView = {
    let collectionViewFlowLayout = ChatBottomUpFlowLayout()
    collectionViewFlowLayout.scrollDirection = .vertical
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    collectionView.backgroundColor = .white
    collectionView.showsHorizontalScrollIndicator = false
    collectionView.showsVerticalScrollIndicator = false
    collectionView.register(OutgoingTextCell.self, forCellWithReuseIdentifier: OutgoingTextCell.className)
    collectionView.register(IncomingTextCell.self, forCellWithReuseIdentifier: IncomingTextCell.className)
    collectionView.register(OutgoingImageCell.self, forCellWithReuseIdentifier: OutgoingImageCell.className)
    collectionView.register(IncomingImageCell.self, forCellWithReuseIdentifier: IncomingImageCell.className)
    // 移除翻转相关代码
    // collectionView.transform = CGAffineTransform(scaleX: 1, y: -1)
    // collectionView.semanticContentAttribute = .forceLeftToRight
    collectionView.keyboardDismissMode = .interactive
    return collectionView
}()

3. 移除单元格内的翻转代码

在OutgoingTextCell和其他单元格中,删除containerView.transform的设置:

func configure(model: MessageViewModel) {
    // 移除这行翻转代码
    // containerView.transform = CGAffineTransform(scaleX: 1, y: -1)
    contentLabel.text = model.content
    timeLabel.text = model.time
    checkMarkImageView.isHidden = !model.isSent
}

4. 修正单元格尺寸计算逻辑

之前你用view.frame.width计算文本宽度,这可能在键盘弹出时因为视图布局变化导致宽度不准确,改用CollectionView的实际宽度:

private func calculateTextHeight(at indexPath: IndexPath) -> CGFloat {
    // 改用collectionView的宽度,避免视图布局变化带来的误差
    let availableWidth = chatCollectionView.bounds.width - 88
    let approximateSize = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
    let estimatedHeight = NSString(string: presenter.contentForMessage(at: indexPath)).boundingRect(with: approximateSize, options: .usesLineFragmentOrigin, attributes: [.font: DinNextFont.regular.getFont(ofSize: 14)], context: nil).height
    return estimatedHeight + 40
}

5. 键盘变化时强制刷新布局(可选)

为了确保键盘弹出/收起时布局完全正确,可以在键盘通知中清除尺寸缓存并无动画刷新布局:

// 假设你用Typist监听键盘变化
func setupKeyboardListener() {
    let typist = Typist()
    typist
        .willChangeFrame { [weak self] info in
            guard let self = self else { return }
            UIView.performWithoutAnimation {
                self.cachedSizes.removeAll()
                self.chatCollectionView.collectionViewLayout.invalidateLayout()
                self.chatCollectionView.reloadData()
            }
        }
        .start()
}

为什么之前的方案无效?

  • 禁用动画只能临时屏蔽,但系统布局更新的根源(坐标冲突)没解决;
  • 换成UITableView后动画减轻是因为TableView的布局逻辑更简单,但翻转带来的坐标问题依然存在;
  • 移除Typist没用是因为问题核心不在键盘通知的封装,而是视图变换和布局的冲突。

这个自定义FlowLayout的方案我在多个聊天项目中用过,完全解决了键盘交互时的动画异常问题,而且布局逻辑更清晰,也避免了翻转带来的其他潜在问题(比如手势识别反向、子视图布局混乱等)。

内容的提问来源于stack exchange,提问作者Vortex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:26:08