动画部分 UITableViewCell

Animate Parts Of UITableViewCell

我想创建一个可展开的 table 视图单元格,点击单元格的特定部分我想设置动画并展开单元格。

我可以使用默认动画在点击时展开和折叠,这基本上会淡化视图更改。但我想在指示器图像上设置旋转动画并展开单元格。

这是当前的动画效果。

这是我的 table 视图代码

class ExpandableTableView: UITableView {
    
    var status:[Bool] = [Bool].init(repeating: false, count: 10)
 
    override init(frame: CGRect, style: UITableView.Style) {
        super.init(frame: frame, style: style)
        self.registerCell()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.registerCell()
    }
    
    func registerCell() {
        self.register(UINib(nibName: "DemoTableViewCell", bundle: .main), forCellReuseIdentifier: "DemoTableViewCell")
        self.rowHeight = UITableView.automaticDimension
        self.delegate = self
        self.dataSource = self
    }
}

extension ExpandableTableView: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return status.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "DemoTableViewCell") as? DemoTableViewCell else {
            return UITableViewCell()
        }
        cell.setHeader("Header \(indexPath.row) : Status \(self.status[indexPath.row])")
        cell.setStatus(isExpanded: self.status[indexPath.row])
        return cell
    }
}

extension ExpandableTableView: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.status[indexPath.row] = !self.status[indexPath.row]
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
}

这是 UITableViewCell 的代码

class DemoTableViewCell: UITableViewCell {
    
    @IBOutlet private var lblHeader:UILabel!
    @IBOutlet weak var imgIndicator: UIImageView!
    @IBOutlet private var containerStackView:UIStackView!
  
    func setHeader(_ header:String) {
        self.lblHeader.text = header
    }
    
    func setStatus(isExpanded:Bool) {
        self.containerStackView.isHidden = !isExpanded
        if (isExpanded){
            self.imgIndicator.transform = CGAffineTransform(rotationAngle: -.pi/2)
        }else {
            self.imgIndicator.transform = CGAffineTransform(rotationAngle: 0)
        }
        self.containerStackView.layoutIfNeeded()
    } 
}

任何帮助将不胜感激。

谢谢


编辑

代码:Here is the code 用于快速结帐。

我不确定您是否尝试将转换代码放入动画块中。

这通常适合我。

例如:

// Set the duration you wish
UIView.animate(withDuration: 2, animations: { [weak self] in
    self?.imgIndicator.transform =
        CGAffineTransform(rotationAngle: .pi/2)
})

给出:


更新 1 使用自动布局的 UITableView 单元示例

自定义table查看单元格

fileprivate class CustomCell: UITableViewCell
{
    var imgIndicator: UIImageView!
    
    static let tableViewCellIdentifier = "cell"
    
    override init(style: UITableViewCell.CellStyle,
                  reuseIdentifier: String?)
    {
        super.init(style: style,
                   reuseIdentifier: reuseIdentifier)
        
        configureIndicator()
    }
    
    required init?(coder: NSCoder)
    {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func configureIndicator()
    {
        let config = UIImage.SymbolConfiguration(textStyle: .largeTitle)
        
        let image = UIImage(systemName: "chevron.right.circle",
                            withConfiguration: config)
        
        imgIndicator = UIImageView(image: image)
        
        imgIndicator.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(imgIndicator)
        
        // auto-layout
        imgIndicator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
            .isActive = true
        imgIndicator.topAnchor.constraint(equalTo: contentView.topAnchor)
            .isActive = true
        imgIndicator.widthAnchor.constraint(equalTo: imgIndicator.heightAnchor,
                                            multiplier: 1).isActive = true
        imgIndicator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            .isActive = true
    }
    
    // Simplified just to do the animation
    // Customize with your logic
    func setStatus(isExpanded: Bool)
    {
        if isExpanded
        {
            imgIndicator.transform = CGAffineTransform(rotationAngle: .pi/2)
        }
    }
}

基本视图控制器配置

class TableViewAnimationVC: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        // This is just view set up, you can ignore this
        title = "Animation rotation"
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.backgroundColor = .white
        navigationController?.navigationBar.tintColor = .white
        view.backgroundColor = .white
        
        configureTableView()
    }
    
    private func configureTableView()
    {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(CustomCell.self,
                           forCellReuseIdentifier: CustomCell.tableViewCellIdentifier)
        
        tableView.dataSource = self
        tableView.delegate = self
        
        // remove additional rows
        tableView.tableFooterView = UIView()
        
        view.addSubview(tableView)
        
        // Auto layout
        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
            .isActive = true
        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
            .isActive = true
        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
            .isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
            .isActive = true
    }
}

表格视图数据源

extension TableViewAnimationVC: UITableViewDataSource
{
    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int
    {
        // random number
        return 10
    }
    
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell =
            tableView.dequeueReusableCell(withIdentifier: CustomCell.tableViewCellIdentifier)
            as! CustomCell
        
        cell.textLabel?.text = "Tap to rotate"
        
        return cell
    }
}

TableView 委托

extension TableViewAnimationVC: UITableViewDelegate
{
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
    {
        // Retrieved the actual cell that was tapped
        let cell = tableView.cellForRow(at: indexPath) as! CustomCell
        
        // Perform animation in block
        UIView.animate(withDuration: 2.0) {
            cell.setStatus(isExpanded: true)
        } completion: { (success) in
            if success
            {
                // do your processing after the animation has completed
            }
        }
    }
}

最终结果:

使用 OP 代码示例更新 2

问题似乎与使用用于重新加载 table 视图和执行其他 UIView 动画的相同动画块有关。

我看到的选项是将这两个动画分开。

一种选择是完成旋转动画,然后重新加载您的 table视图行以进行展开 - 您可以对此进行试验。

我会告诉你第二个选项,它似乎是一个更好的用户体验。高层次:

  1. 找出当前应为哪个单元格设置动画
  2. 使用动画重新加载 table 视图行
  3. 在单独的 UIView 动画块中,执行您的其他动画

以下是我为实现上述目标所做的更改:

首先,在您的单元格中 class

class DemoTableViewCell: UITableViewCell {
    
    @IBOutlet private var lblHeader:UILabel!
    @IBOutlet weak var imgIndicator: UIImageView!
    @IBOutlet private var containerStackView:UIStackView!
  
    // No change
    func setHeader(_ header:String) {
        self.lblHeader.text = header
    }
    
    // Contract / expand the table view cell only
    func setStatus(isExpanded:Bool) {
        self.containerStackView.isHidden = !isExpanded
        self.containerStackView.layoutIfNeeded()
    }
    
    // Animate the indicator separately
    func animateIndicator()
    {
        var endAngle: CGFloat = .pi/2.0
        var startAngle = CGFloat.zero
        
        if self.containerStackView.isHidden
        {
            startAngle = .pi/2
            endAngle = .zero
        }
        
        imgIndicator.transform
            = CGAffineTransform(rotationAngle: startAngle)
        
        UIView.animate(withDuration: 0.5) { [weak self] in
            self?.imgIndicator.transform
                = CGAffineTransform(rotationAngle: endAngle)
        }
        completion: { (success) in
            // do anything
        }
    }
}

ExpandableTableView中我做了一些修改:

// Unchanged, done by you
var status:[Bool] = [Bool].init(repeating: false, count: 10)

// Stores the current cell's index path which should be animated
// This is for my convenience as I couldn't grasp your complete logic
// you can edit based on your logic
var shouldAnimate: [IndexPath] = []

UITableViewDelegate 的变化

extension ExpandableTableView: UITableViewDelegate {
    func tableView(_ tableView: UITableView,
                   didSelectRowAt indexPath: IndexPath) {
        self.status[indexPath.row] = !self.status[indexPath.row]
        
        // Added by me to keep track of which cell to animate
        shouldAnimate.append(indexPath)
        
        tableView.reloadRows(at: [indexPath], with: .fade)
        
    }
    
    // Perform your UIView animations in here
    func tableView(_ tableView: UITableView,
                   willDisplay cell: UITableViewCell,
                   forRowAt indexPath: IndexPath)
    {
        if let cell = cell as? DemoTableViewCell,
           shouldAnimate.firstIndex(of: indexPath) != nil
        {
            cell.animateIndicator()
        }
    }
    
    // Unchanged
    func tableView(_ tableView: UITableView,
                   heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
}

结果接近我相信你希望达到的结果:

调整时间以尝试使展开和旋转动画更好地同步。