隐形细胞集合查看动画

Invisible Cell CollectionView Animation

我对在 collectionView 中管理动画不可见单元感到困惑。

所以我有堆叠卡片集合视图,当用户捏出集合单元格时,x 坐标会发生变化。更改坐标后,一些单元格在屏幕外。当我想通过

获取单元格时
collectionView?.cellForItem(at: indexPath)

return零。

查看绿色单元格。

func animateExpand(cells: [UICollectionViewCell], coordinateY: [CGFloat]) {
    
    UIView.animate(
        withDuration: 0.5,
        delay: 0,
        options: .curveEaseOut,
        animations: {
            for (index, cell) in cells.enumerated() {
                cell.frame.origin.y = coordinateY[index]
            }
        },
        completion: { [weak self] (_: Bool) in
            self?.invalidateLayout()
            self?.isMoving = false
        }
    )
}

只有 return 4 个单元格,因为 1 个单元格为零。 我怎样才能实现这个动画?谢谢

您遇到的几个问题。

首先,您使用自定义集合视图布局来定位单元格,然后您还显式设置了单元格 y-origins。

其次,集合视图只会呈现可见单元格,因此当您尝试“un-expand”布局时,底部单元格不存在。

我将建议一种稍微不同的方法。

使用 protocol/delegate 模式,这样您的自定义 UICollectionViewLayout 可以告诉 控制器 在出现捏合手势时展开/折叠布局。然后控制器将创建自定义布局的新实例并调用 .setCollectionViewLayout(...) - 包裹在动画块中 - 展开或折叠。

此外,控制器将临时扩展集合视图的高度,以便呈现“off-screen”个单元格。

这是一些示例代码 - 我对您现有的自定义布局几乎没有做任何更改。我包含的评论应该足以让事情变得清楚。

不过请注意,这只是 示例代码 -- 它尚未经过全面测试,旨在作为一个开始点数:

protocol PinchProtocol: AnyObject {
    func toggleExpanded(_ expand: Bool)
}

class MyWalletVC: UIViewController, PinchProtocol {
    
    var data: [UIColor] = [
        .red, .green, .blue, .cyan, .magenta,
        //.yellow, .orange, .systemYellow,
    ]
    
    var collectionView: UICollectionView!
    
    var cvBottom: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let lay = WalletStackLayout()
        lay.isExpanded = false
        lay.pinchDelegate = self

        collectionView = UICollectionView(frame: .zero, collectionViewLayout: lay)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)

        let g = view.safeAreaLayoutGuide

        cvBottom = collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            cvBottom,
        ])
        
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
        collectionView.dataSource = self
        collectionView.delegate = self
    }
    
    func toggleExpanded(_ expand: Bool) {
        
        // increase collection view height
        //  so "off-screen" cells will be rendered
        //  I just picked a value of 800 to make sure it's enough
        self.cvBottom.constant = 800

        UIView.animate(
            withDuration: 0.5,
            delay: 0,
            options: .curveEaseOut,
            animations: { [weak self] in
                guard let self = self else { return }
                
                // create a NEW layout object
                let lay = WalletStackLayout()
                
                // set its isExpanded property
                lay.isExpanded = expand
                
                // set self as its pinchDelegate
                lay.pinchDelegate = self
                
                // set the new layout
                //  use "animated: false" because we're animating it with UIView.animate
                self.collectionView.setCollectionViewLayout(lay, animated: false)
            },
            completion: { [weak self] (_: Bool) in
                guard let self = self else { return }
                // reset collection view height
                self.cvBottom.constant = 0
            }
        )
        
    }
    
}

extension MyWalletVC: UICollectionViewDataSource, UICollectionViewDelegate {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
        c.contentView.backgroundColor = data[indexPath.item]
        c.contentView.layer.cornerRadius = 16
        return c
    }
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Selected item from Tap gesture:", indexPath)
    }
}

typealias CellAndLayoutAttributes = (cell: UICollectionViewCell, layoutAttributes: UICollectionViewLayoutAttributes)

class WalletStackLayout: UICollectionViewLayout {
    
    // so we can tell the controller we got pinched
    weak var pinchDelegate: PinchProtocol?
    
    // expanded / collapsed layout
    var isExpanded: Bool = false
    
    private let heightRatio: CGFloat = 196/343
    private let sidePadding: CGFloat = 16.0
    private let peekStack: CGFloat = 40
    
    private var cellWidth: CGFloat {
        return UIScreen.main.bounds.width - sidePadding * 2.0
        //return Device.screenWidth - sidePadding*2
    }
    
    private var cellHeight: CGFloat {
        return heightRatio * cellWidth
    }
    
    private var isMoving: Bool = false
    private var collectionLayoutAttributes: [UICollectionViewLayoutAttributes] = []
    private var tapGestureRecognizer: UITapGestureRecognizer?
    private var pinchGestureRecognizer: UIPinchGestureRecognizer?
    
    // this is needed to keep the Top cell at the Top of the collection view
    //  when changing the layout
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        return .zero
    }
    
    override var collectionViewContentSize: CGSize {
        
        guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
            return CGSize(width: 0, height: 0)
        }
        
        var contentHeight: CGFloat = 0
        for index in 0..<collectionView.numberOfSections {
            contentHeight += calculateSectionCardHeight(section: index)
        }
        
        return CGSize(
            width: collectionView.bounds.width,
            height: contentHeight
        )
    }
    
    override func prepare() {
        super.prepare()
        
        collectionLayoutAttributes.removeAll()
        guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
            return
        }
        
        initializeCardCollectionViewLayout()
        
        collectionLayoutAttributes = makeCardsLayoutAttributes()
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
            return nil
        }
        
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        
        var r = rect
        r.size.height += 500
        for attributes in collectionLayoutAttributes where attributes.frame.intersects(r) {
            visibleLayoutAttributes.append(attributes)
        }
        
        return visibleLayoutAttributes
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return collectionLayoutAttributes[indexPath.row]
    }
    
    private func getCell(at indexPath: IndexPath) -> UICollectionViewCell? {
        return collectionView?.cellForItem(at: indexPath)
    }
    
    private func getLayoutAttributes(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return collectionView?.layoutAttributesForItem(at: indexPath)
    }
    
    private func getCellAndLayoutAttributes(at indexPath: IndexPath) -> CellAndLayoutAttributes? {
        
        guard let cell = getCell(at: indexPath),
              let layoutAttributes = getLayoutAttributes(at: indexPath) else {
                  return nil
              }
        
        return (cell: cell, layoutAttributes: layoutAttributes)
    }
    
    // MARK: - BEGIN SET CARDS -
    private func makeCardsLayoutAttributes() -> [UICollectionViewLayoutAttributes] {
        
        guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
            return []
        }
        
        var collectionViewLayout: [UICollectionViewLayoutAttributes] = []
        
        for section in 0..<collectionView.numberOfSections {
            for row in 0..<collectionView.numberOfItems(inSection: section) {
                let indexPath = IndexPath(row: row, section: section)
                collectionViewLayout.append(makeCardLayoutAttributes(forCellWith: indexPath))
            }
        }
        
        return collectionViewLayout
    }
    
    private func makeInitialLayoutAttributes(forCellWith indexPath: IndexPath, height: CGFloat) -> UICollectionViewLayoutAttributes {
        
        let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        let preferredSize = CGSize(width: cellWidth, height: height)
        attributes.size = preferredSize
        
        return attributes
    }
    
    private func makeCardLayoutAttributes(forCellWith indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
        
        let attributes = makeInitialLayoutAttributes(forCellWith: indexPath, height: cellHeight)
        let coordinateY = calculateSectionYCoordinate(indexPath: indexPath)
        attributes.frame.origin.y = coordinateY
        attributes.frame.origin.x = sidePadding
        attributes.zIndex = indexPath.item
        
        return attributes
    }
    
    private func calculateSectionYCoordinate(indexPath: IndexPath) -> CGFloat {
        
        var sectionYCoordinate: CGFloat = 0
        
        for section in 0..<indexPath.section {
            sectionYCoordinate += calculateSectionCardHeight(section: section)
        }
        if isExpanded {
            return (cellHeight + sidePadding) * CGFloat(indexPath.row) + sectionYCoordinate
        } else {
            return peekStack * CGFloat(indexPath.row) + sectionYCoordinate
        }
    }
    
    private func calculateSectionCardHeight(section: Int) -> CGFloat {
        
        guard let numberOfItems = collectionView?.numberOfItems(inSection: section) else {
            return 0
        }
        
        if isExpanded {
            let totalExpandedCards: Int = numberOfItems
            return (cellHeight + sidePadding) * CGFloat(totalExpandedCards)
        } else {
            
            let visibleCardCount: Int = 1
            let totalStackedCards: Int = numberOfItems > 1 ? numberOfItems - visibleCardCount : 0
            
            return peekStack * CGFloat(totalStackedCards) + cellHeight + sidePadding

        }
        
    }
    
    // MARK: - TAP GESTURE -
    private func initializeCardCollectionViewLayout() {
        
        if tapGestureRecognizer == nil {
            tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGestureHandler))
            if let tapGesture = tapGestureRecognizer {
                collectionView?.addGestureRecognizer(tapGesture)
            }
        }

        if pinchGestureRecognizer == nil {
            pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinchGesture(_:)))
            pinchGestureRecognizer?.delegate = self
            if let pinchGesture = pinchGestureRecognizer {
                collectionView?.addGestureRecognizer(pinchGesture)
            }
        }
    }
    
}

extension WalletStackLayout: UIGestureRecognizerDelegate {
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    @objc private func handlePinchGesture(_ pinchGesture: UIPinchGestureRecognizer) {
        if pinchGesture.state == .began || pinchGesture.state == .changed {
            
            guard let collectionView = collectionView,
                  let tapLocation = pinchGestureRecognizer?.location(in: collectionView),
                  let indexPath = collectionView.indexPathForItem(at: tapLocation),
                  !isMoving else {
                      return
                  }
            
            if pinchGesture.scale > 1 {
                // tell the controller to switch to Expanded layout
                pinchDelegate?.toggleExpanded(true)
            } else if pinchGesture.scale < 1 {
                // tell the controller to switch to Collapsed layout
                pinchDelegate?.toggleExpanded(false)
            }
            
        }
    }
    
    @objc
    private func tapGestureHandler() {
        guard let collectionView = collectionView,
              let tapLocation = tapGestureRecognizer?.location(in: collectionView),
              let indexPath = collectionView.indexPathForItem(at: tapLocation) else {
                  return
              }
        print("TapGestureHandler Section: \(indexPath.section) Row: \(indexPath.row)")
        
        collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
    }
    
}

不需要 @IBOutlet@IBAction 连接。只需将标准视图控制器的自定义 class 分配给 MyWalletVC,它应该 运行 没有问题。