草庐IT

ios - 具有动态高度的单元格 Accordion 式动画的 UITableView

coder 2024-01-13 原文

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

第一次尝试使用 reloadRows

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

第二次尝试使用 beginUpdates/endUpdates

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

目标

我尝试实现的效果与尝试 2 中展开时的效果完全相同,但在折叠时也是相反的( Accordion /显示样式)。

注意:如果可能的话,我还想继续使用 StackView 来设置 UITextView 上的 hidden 属性,因为我正在处理的实际项目涉及其他几个 subview ,它们的动画应该类似于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
    }
}

Storyboard

尝试 2 使用 beginUpdates/endUpdates 的代码

与尝试 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()
}

最佳答案

我终于在 beginUpdates/endUpdates 的基础上解决了这个问题。我对问题和解决方案的进一步研究如下。

问题

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

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

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

  1. 将 UITextView 的 isHidden 属性设置为 true 时,包含的堆栈 View 会隐藏 View 并将单元格调整为新的固有内容大小。
  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 上创建一个高度约束,并在 cell 收缩时使其优先

2.1:在 Interface builder 中对 UIStackView 添加了高度限制

2.2:在 MyTableViewCell 中添加 heightConstraint 作为 strong(不是弱,这很重要)

2.3 在 MyTableViewCell 的 awakeFromNib 中,设置 heightConstraint.isActive = false 以保持默认行为

2.4 在界面生成器中:确保 UIStackView 底部约束的优先级设置低于高度约束的优先级。 IE。 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()
    }
}

关于ios - 具有动态高度的单元格 Accordion 式动画的 UITableView,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57845446/

有关ios - 具有动态高度的单元格 Accordion 式动画的 UITableView的更多相关文章

  1. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  2. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  3. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  4. ruby-on-rails - Rails 3.1 中具有相同形式的多个模型? - 2

    我正在使用Rails3.1并在一个论坛上工作。我有一个名为Topic的模型,每个模型都有许多Post。当用户创建新主题时,他们也应该创建第一个Post。但是,我不确定如何以相同的形式执行此操作。这是我的代码:classTopic:destroyaccepts_nested_attributes_for:postsvalidates_presence_of:titleendclassPost...但这似乎不起作用。有什么想法吗?谢谢! 最佳答案 @Pablo的回答似乎有你需要的一切。但更具体地说...首先改变你View中的这一行对此#

  5. ruby-on-rails - Prawn - 表格单元格内的链接 - 2

    我正在尝试用Prawn生成PDF。在我的PDF模板中,我有带单元格的表格。在其中一个单元格中,我有一个电子邮件地址:cell_email=pdf.make_cell(:content=>booking.user_email,:border_width=>0)我想让电子邮件链接到“mailto”链接。我知道我可以这样链接:pdf.formatted_text([{:text=>booking.user_email,:link=>"mailto:#{booking.user_email}"}])但是将这两行组合起来(将格式化文本作为内容)不起作用:cell_email=pdf.make_c

  6. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  7. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  8. ruby - 具有两个参数的 block - 2

    我从用户Hirolau那里找到了这段代码:defsum_to_n?(a,n)a.combination(2).find{|x,y|x+y==n}enda=[1,2,3,4,5]sum_to_n?(a,9)#=>[4,5]sum_to_n?(a,11)#=>nil我如何知道何时可以将两个参数发送到预定义方法(如find)?我不清楚,因为有时它不起作用。这是重新定义的东西吗? 最佳答案 如果您查看Enumerable#find的文档,您会发现它只接受一个block参数。您可以将它发送两次的原因是因为Ruby可以方便地让您根据它的“并行赋

  9. ruby-on-rails - 在 RSpec 中,如何以任意顺序期望具有不同参数的多条消息? - 2

    RSpec似乎按顺序匹配方法接收的消息。我不确定如何使以下代码工作:allow(a).toreceive(:f)expect(a).toreceive(:f).with(2)a.f(1)a.f(2)a.f(3)我问的原因是a.f的一些调用是由我的代码的上层控制的,所以我不能对这些方法调用添加期望。 最佳答案 RSpecspy是测试这种情况的一种方式。要监视一个方法,用allowstub,除了方法名称之外没有任何约束,调用该方法,然后expect确切的方法调用。例如:allow(a).toreceive(:f)a.f(2)a.f(1)

  10. ruby - 在 Ruby 中动态创建数组 - 2

    有没有办法在Ruby中动态创建数组?例如,假设我想遍历用户输入的书籍数组:books=gets.chomp用户输入:"TheGreatGatsby,CrimeandPunishment,Dracula,Fahrenheit451,PrideandPrejudice,SenseandSensibility,Slaughterhouse-Five,TheAdventuresofHuckleberryFinn"我把它变成一个数组:books_array=books.split(",")现在,对于用户输入的每一本书,我想用Ruby创建一个数组。伪代码来做到这一点:x=0books_array.

随机推荐