【问题标题】:How can I force animations in UITableViewCells to stop while scrolling through the table view?如何在滚动表格视图时强制 UITableViewCells 中的动画停止?
【发布时间】:2021-12-31 09:37:50
【问题描述】:

我面临的问题本身很容易描述。

我的应用程序中有一个表格视图,其中动画是异步启动的。对于一次索引路径,动画只发生一次。如果我将单元格滚动到可见区域之外,我假设该索引路径的动画已完成。即下次索引路径回到可见区域时,我将其设置为最终值,没有动画。如果我不滚动,动画会按预期工作。

但是,当我滚动表格视图时,因为单元格被重复使用,并且打算在当前不可见的单元格上执行的动画仍在现在可见的单元格上执行。 我整理了一个描述我的问题的示例代码。

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
}


extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath)
        cell.textLabel?.layer.opacity = 1
        UIView.animate(withDuration: 2, delay: 0, options: .curveEaseIn) { [weak cell] in
            cell?.textLabel?.layer.opacity = 0
        } completion: { [weak cell] _ in
            cell?.textLabel?.layer.opacity = 0
            cell?.textLabel?.text = "\(indexPath.row)"
            UIView.animate(withDuration: 2, delay: 0, options: .curveEaseIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 1
            } completion: { [weak cell] _ in
                cell?.textLabel?.layer.opacity = 1
                cell?.textLabel?.text = "\(indexPath.row)"
            }
        }
        animated[indexPath.row].toggle()
        return cell
    }
}

为了在单元格超出表格视图范围时停止动画,我尝试将动画的引用保存在单元格中,并在单元格出列后立即删除。但这对我的情况没有帮助。

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
    
    var animated: [Bool] = Array(repeating: false, count: 100)
}

class TestCell: UITableViewCell {
    
    var animations: [UIViewPropertyAnimator] = []
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        
        if !animated[indexPath.row] {
            
            //Stop animations if any
            cell.animations.forEach { animator in
                animator.stopAnimation(true)
            }
            cell.animations.removeAll()

            //
            let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 0
            }
            
            animator.addCompletion { [weak cell] completed in
                cell?.textLabel?.layer.opacity = 0
                cell?.textLabel?.text = "\(indexPath.row)"
                
                if completed == .end {
                    
                    let animator1 = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                        cell?.textLabel?.layer.opacity = 1
                    }
                    
                    animator1.addCompletion { [weak cell] _ in
                        cell?.textLabel?.layer.opacity = 1
                        cell?.textLabel?.text = "\(indexPath.row)"
                    }
                    
                    animator1.startAnimation(afterDelay: 0)
                    cell?.animations.append(animator1)
                }
            }
            
            animator.startAnimation(afterDelay: 0)
            cell.animations.append(animator)
            
            animated[indexPath.row].toggle()
        } else {
            cell.textLabel?.layer.opacity = 1
            cell.textLabel?.text = "\(indexPath.row)"
        }
        return cell
    }
}

我在 SO 上找不到类似的东西。任何帮助表示赞赏。

【问题讨论】:

  • 在单元格类中添加动画师并在单元格中使用 prepareForReuse 来停止它
  • 您是否尝试在单元格滚动出视图时“暂停”动画,然后在单元格滚动回视图时“恢复”动画?
  • @DonMag 不,如果单元格滚动到可见区域之外,我假设该索引路径的动画已完成。对于单个索引路径,动画只需要发生一次。

标签: ios swift uitableview uikit


【解决方案1】:

在单元类中设置/停止动画:

class TestCell: UITableViewCell {

    var animation : UIViewPropertyAnimator?

    // call this in cellForRow in tableView controller 
    func startAnimation() {
        … create animation and save it in self.animation
    }

    func stopAnimation () {
        … stop animation 
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        StopAnimation();
    }
}

【讨论】:

  • 我认为这与我所做的相似? .但我有一些嵌套动画。就像我在第二段代码中描述的那样。当我这样做时,第二个目标无论如何都会运行。
  • 您可以在 cell 类的完成处理程序中添加嵌套动画。您只需要保存最后一个动画,因为当您创建第二个动画时,您的第一个动画位于完成处理程序中(即终止)。您也不要在制作动画之前重置不透明度。
【解决方案2】:

这是一个奇怪的动画序列,但我猜这更多是为了测试/弄清楚它以供您实际使用。

这可能会有所帮助...

在您的单元格中实现prepareForReuse() 以停止和删除任何正在执行的动画,并将标签的不透明度设置为1.0

cellForRowAt 中,将 animated 数组中的插槽设置为 false 而不是切换它,因为您只希望动画在第一次出现一行时运行。

同样在cellForRowAt 中,确保将单元格的标签“重置”为其初始值。

试试这个,看看它是否能让你更接近你的目标:

class TestCell: UITableViewCell {
    
    var animations: [UIViewPropertyAnimator] = []
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        animations.forEach { animator in
            animator.stopAnimation(true)
        }
        animations.removeAll()

        textLabel?.layer.opacity = 1.0
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
    
    var animated: [Bool] = Array(repeating: false, count: 100)
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        
        if !animated[indexPath.row] {
            
            animated[indexPath.row] = true
            
            // cells are reused, so "reset" the initial label text
            cell.textLabel?.text = "Needs to animate..."

            //
            let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 0
            }
            
            animator.addCompletion { [weak cell] completed in
                cell?.textLabel?.layer.opacity = 0
                cell?.textLabel?.text = "\(indexPath.row)"
                
                if completed == .end {
                    
                    let animator1 = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                        cell?.textLabel?.layer.opacity = 1
                    }
                    
                    animator1.addCompletion { [weak cell] _ in
                        cell?.textLabel?.layer.opacity = 1
                        cell?.textLabel?.text = "\(indexPath.row)"
                    }
                    
                    animator1.startAnimation(afterDelay: 0)
                    cell?.animations.append(animator1)
                }
            }
            
            animator.startAnimation(afterDelay: 0)
            cell.animations.append(animator)
            
        } else {
            cell.textLabel?.layer.opacity = 1
            cell.textLabel?.text = "\(indexPath.row)"
        }
        return cell
    }
    
}

编辑

关于动画完成块的注意事项...

首先,您使用 completed 作为完成块参数——您可能会误导自己,因为它不是 Bool 值...它是 UIViewAnimatingPosition 值。

来自苹果的docs

动画的结束位置。使用此值来确定动画是在开头、结尾还是中间某处停止。

完成块在动画正常结束后执行。如果您调用 stopAnimation(_:) 方法,如果您为方法的参数指定 true,则不会调用完成块。如果为参数指定 false,则动画器在调用其 finishAnimation(at:) 方法后正常执行完成块。

所以,如果你的

animator.stopAnimation(true)

在第一个动画结束之前调用,它的完成块不会被调用。

如果在第一个动画结束之后调用,但在第二个动画结束之前,则不会调用第二个动画的完成块。

【讨论】:

  • 理论上应该可行,但是在 if completed == .end { 行上,如果我们滚动表格视图,条件应该会失败,但是 completed这里的变量是.end,比较混乱。这是一个错误吗?还是我误解了变量的目的?基本上第一个动画 animator 按预期终止,但第二个动画 animator1 开始,这是不希望的
  • 对不起,我的错,这行得通。但是知道为什么 completion 仍然是 .end 吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-15
相关资源
最近更新 更多