首页 > 解决方案 > 具有动态高度的单元格的手风琴式动画的 UITableView

问题描述

我正在尝试在具有动态高度的单元格的 UITableView 中创建类似手风琴的动画。

第一次尝试使用 reloadRows

在此处输入图像描述

问题似乎是 reloadRows 会导致即时单元格高度更新,因此虽然 tableview 动画,但单元格本身不会动画其高度。这会导致其他单元格有时会重叠并干扰扩展的单元格内容。

第二次尝试使用 beginUpdates/endUpdates

在此处输入图像描述

这是我得到的最接近单元格根据需要扩展的位置。然而,当折叠时,TextView 会立即消失,将标题挂在中间直到折叠。

目标

我尝试实现的效果与尝试 2 中展开时的效果完全相同,但在折叠时也相反(手风琴/显示风格)。

注意:如果可能的话,我还想继续使用 StackView 来设置hiddenUITextView 上的属性,因为我正在处理的实际项目涉及其他几个应该类似于 UITextView 动画的子视图。

我正在使用 XCode 11 beta 7。

首次尝试使用 reloadRows 的代码:

struct Post {
    var id: String
    var title: String
    var content: String
}

let posts = [
    Post(id: "1", title: "Post 1", content: "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est"),
    Post(id: "2", title: "Post 2", content: "Lorem ipsum dolor"),
    Post(id: "3", title: "Post 3", content: "Lorem ipsum dolor"),
    Post(id: "4", title: "Post 4", content: "Lorem ipsum dolor"),
    Post(id: "5", title: "Post 5", content: "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."),
    Post(id: "6", title: "Post 6", content: "Lorem ipsum dolor")
]

UITableViewController

class MyTableViewController: UITableViewController {
    var selectedRow: IndexPath? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.backgroundColor = .darkGray
        self.tableView.rowHeight = UITableView.automaticDimension
        self.tableView.estimatedRowHeight = 200
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath)
        -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(
            withIdentifier: "Cell", for: indexPath) as? MyTableViewCell
            else { fatalError() }

        let post = posts[indexPath.row]
        let isExpanded = selectedRow == indexPath
        cell.configure(expanded: isExpanded, post: post)

        return cell
    }

    override func tableView(_ tableView: UITableView,
                            didSelectRowAt indexPath: IndexPath) {
        var reloadPaths: [IndexPath] = []

        if selectedRow != nil {
            reloadPaths.append(selectedRow!)
        }

        if selectedRow == indexPath {
            selectedRow = nil
        } else {
            reloadPaths.append(indexPath)
            selectedRow = indexPath
        }

        tableView.reloadRows(at: reloadPaths, with: .automatic)
    }
}

细胞

class MyTableViewCell: UITableViewCell {
    @IBOutlet weak var title: UILabel!
    @IBOutlet weak var textView: UITextView!

    func configure(expanded: Bool, post: Post) {
        title.text = post.title
        textView.text = post.content
        configureExpansion(expanded)
    }

    func configureExpansion(_ expanded: Bool) {
        self.textView.isHidden = !expanded
        self.contentView.backgroundColor = expanded ?
            .red : .systemBackground
    }
}

故事板

在此处输入图像描述

使用 beginUpdates/endUpdates 进行尝试 2 的代码

与尝试 1 完全相同的代码,但...didSelectRowAt...替换为:

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
    tableView.beginUpdates()

    if let selectedRow = selectedRow,
        let prevCell = tableView.cellForRow(at: selectedRow) as? MyTableViewCell {
        prevCell.configureExpansion(false)
    }

    let selectedCell = tableView.cellForRow(at: indexPath) as? MyTableViewCell
    if selectedRow == indexPath {
        selectedCell?.configureExpansion(false)
        selectedRow = nil
    } else {
        selectedCell?.configureExpansion(true)
        selectedRow = indexPath
    }

    tableView.endUpdates()
}

标签: iosswiftuitableviewuistackview

解决方案


我终于在 beginUpdates/endUpdates 的基础上解决了它。接下来是我对问题和解决方案的进一步研究。

问题

如原始问题中所述,扩展阶段可以正常工作。这样做的原因是:

  1. 将 UITextView 的isHidden属性设置为 时false,包含堆栈视图会调整大小并为单元格提供新的内在内容大小
  2. 使用 beginUpdates/endUpdates 将为行高的变化设置动画,而无需重新加载单元格。起始状态是 UITextView 的内容是可见的,但当前被当前单元格高度裁剪,因为单元格尚未调整大小。
  3. endUpdates调用时,单元格将自动扩展,因为固有大小已更改并且tableView.rowHeight = .automaticDimension. 动画将根据需要逐渐显示新内容。

但是,折叠阶段不会反过来产生相同的动画效果。没有的原因是:

  1. 将 UITextView 的isHidden属性设置为 时true,包含堆栈视图会隐藏视图并将单元格的大小调整为新的内在内容大小。
  2. endUpdates被调用时,动画将通过立即删除 UITextView 开始。想想看,这是意料之中的,因为它与扩张期间发生的事情相反。但是,通过将可见元素“悬挂”在中间而不是在行缩小时逐渐隐藏 UITextView 也破坏了所需的动画效果。

一个解法

为了获得所需的隐藏效果,UITextView 应该在整个动画期间保持可见,同时单元格缩小。这包括几个步骤:

第 1 步:覆盖heightForRow

override func tableView(_ tableView: UITableView,
                        heightForRowAt indexPath: IndexPath) -> CGFloat {
    // If a cell is collapsing, force it to its original height stored in collapsingRow?
    // instead of using the intrinsic size and .automaticDimension
    if indexPath == collapsingRow?.indexPath {
        return collapsingRow!.height
    } else {
        return UITableView.automaticDimension
    }
}

UITextView 现在将在单元格缩小时保持可见,但由于自动布局,UITextView 会立即被剪裁,因为它已锚定到单元格高度。

第 2 步:在 UIStackView 上创建高度约束,并在单元格缩小时优先处理

2.1:在Interface builder中对UIStackView添加高度约束

2.2 :在 MyTableViewCell 中添加heightConstraint强(不是弱,这很重要)出口

2.3awakeFromNibMyTableViewCell中,设置heightConstraint.isActive = false保持默认行为

2.4在界面生成器中:确保UIStackView底部约束的优先级设置低于高度约束的优先级。即999用于底部约束和1000高度约束。不这样做会导致在崩溃阶段出现冲突的约束。

第 3 步:折叠时,激活heightConstraint并将其设置为 UIStackView 的当前固有大小。这在单元格高度减小的同时保持 UITextView 的内容可见,但也可以根据需要剪辑内容,从而产生“隐藏”效果。

if let expandedRow = expandedRow,
    let prevCell = tableView.cellForRow(at: expandedRow.indexPath) as? MyTableViewCell {
    prevCell.heightConstraint.constant = prevCell.stackView.frame.height
    prevCell.heightConstraint.isActive = true

    collapsingRow = expandedRow
}

第 4 步:使用动画完成时重置状态CATransaction.setCompletionBlock

合并的步骤

class MyTableViewController: UITableViewController {
    var expandedRow: (indexPath: IndexPath, height: CGFloat)? = nil
    var collapsingRow: (indexPath: IndexPath, height: CGFloat)? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.backgroundColor = .darkGray
        self.tableView.rowHeight = UITableView.automaticDimension
        self.tableView.estimatedRowHeight = 200
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(
            withIdentifier: "Cell", for: indexPath) as? MyTableViewCell
            else { fatalError() }

        let post = posts[indexPath.row]
        let isExpanded = expandedRow?.indexPath == indexPath
        cell.configure(expanded: isExpanded, post: post)

        return cell
    }

    override func tableView(_ tableView: UITableView,
                            heightForRowAt indexPath: IndexPath) -> CGFloat {
        if indexPath == collapsingRow?.indexPath {
            return collapsingRow!.height
        } else {
            return UITableView.automaticDimension
        }
    }

    override func tableView(_ tableView: UITableView,
                            didSelectRowAt indexPath: IndexPath) {
        guard let tappedCell = tableView.cellForRow(at: indexPath) as? MyTableViewCell
            else { return }

        CATransaction.begin()
        tableView.beginUpdates()

        if let expandedRow = expandedRow,
            let prevCell = tableView.cellForRow(at: expandedRow.indexPath)
                as? MyTableViewCell {
            prevCell.heightConstraint.constant = prevCell.stackView.frame.height
            prevCell.heightConstraint.isActive = true

            CATransaction.setCompletionBlock {
                if let cell = tableView.cellForRow(at: expandedRow.indexPath)
                    as? MyTableViewCell {
                    cell.configureExpansion(false)
                    cell.heightConstraint.isActive = false
                }
                self.collapsingRow = nil
            }

            collapsingRow = expandedRow
        }


        if expandedRow?.indexPath == indexPath {
            collapsingRow = expandedRow
            expandedRow = nil
        } else {
            tappedCell.configureExpansion(true)
            expandedRow = (indexPath: indexPath, height: tappedCell.frame.height)
        }

        tableView.endUpdates()
        CATransaction.commit()
    }
}

在此处输入图像描述


推荐阅读