1

我正在构建一个卡片视图 - 选定的卡片在顶部,其余的在底部,彼此堆叠。他们都有相同的superview。

所选卡片的 zPosition = 0,堆叠中的卡片具有递增的 zPositions:1,2,3 等。 交换前 CardStack

当我从堆栈中挑选一张卡片时,我会为它与所选卡片(连同它们的 zPositions)的交换设置动画 - 类似于 Apple Wallet。 交换后 CardStack - 正确的 zPositions

动画后,zPositions 设置为正确的值,但视图层次结构无效。 查看层次结构 - Xcode 可视化调试器

是否可以使用 zPosition 实现这样的动画?

交换动画代码:

func didSelect(cardToBeSelected: CardView) {
    guard alreadySelectedCard !== cardToBeSelected else {
        return
    }
    
    guard let alreadySelectedCard = alreadySelectedCard else { return }
    
    let destinationOriginY = alreadySelectedCard.frame.origin.y
    let destinationZPosition = alreadySelectedCard.layer.zPosition

    alreadySelectedCard.layer.zPosition = cardToBeSelected.layer.zPosition
    
    let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
        self.alreadySelectedCard.frame.origin.y = cardToBeSelected.frame.origin.y
        cardToBeSelected.frame.origin.y = destinationOriginY
        
        self.view.layoutSubviews()
    }
    
    animator.addCompletion { (position) in
        switch position {
        case .end:
            cardToBeSelected.layer.zPosition = destinationZPosition
        default:
            break
        }
    }
    
    animator.startAnimation()
    
    self.alreadySelectedCard = cardToBeSelected
}
4

1 回答 1

0

我想你会遇到几个问题...

  1. 您正在设置约束明确设置框架-几乎总是自找麻烦

  2. 改变layer.zPosition不会改变对象在子视图集合中的顺序

  3. 尝试更改卡片的位置/顺序时,使用相对于“顶部卡片”底部的垂直约束可能会变得复杂

我认为更好的方法是:

  • 更新约束常量而不是帧
  • 使用交换子视图“z-order”顺序insertSubview(_ view: UIView, belowSubview siblingSubview: UIView)
  • 将“已选择”卡中的顶部约束常量值与“待选择”卡交换

我看到您正在使用 SnapKit(就我个人而言,我不喜欢它,但无论如何......)

从我的快速搜索中,似乎很难“即时”获得对 SnapKit 约束的引用以获取其.constant价值。为了解决这个问题,您可以向您的CardView类添加一个属性以保留对其“捕捉顶部约束”的引用。

这是您的 pastebin 链接中的代码,按照我上面的描述进行了修改。请考虑它的示例代码——但它可能会让你如愿以偿。大部分是相同的——我添加了注释,希望能澄清我添加/更改的代码:

class ViewController: UIViewController {
    private let contentInset: CGFloat = 20.0
    private var scrollView: UIScrollView!
    private var contentContainerView: UIView!
    private var mainCardView: CardView!
    
    private var alreadySelectedCard: CardView!
    private let colors: [UIColor] = [.black, .green, .blue, .red, .yellow, .orange, .brown, .cyan, .magenta, .purple]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        initializeScrollView()
        initializeContentContainerView()

        generateCards(count: colors.count)
        
        alreadySelectedCard = cards[0]
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // first card is at the top of the view, so we'll set its offset
        //  inside the forEach loop to contentInset
        
        // start top of 2nd card at bottom of first card + cardOffset
        //  since first card is not "at the top" yet, calculate it
        var topOffset = contentInset + alreadySelectedCard.frame.height + cardOffset
        
        // update the top offset for the rest of the cards
        cards.forEach { card in
            guard let thisTopConstraint = card.topConstraint else {
                fatalError("Cards were not initialized correctly!!!")
            }
            if card == alreadySelectedCard {
                thisTopConstraint.update(offset: contentInset)
            } else {
                thisTopConstraint.update(offset: topOffset)
                topOffset += cardOffset
            }
        }
        // animate them into view
        let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
            self.contentContainerView.layoutSubviews()
        }
        animator.startAnimation()

    }
    
    private let cardOffset: CGFloat = 100.0
    private var cards = [CardView]()
    
    private func add(_ card: CardView) {
        cards.append(card)
        contentContainerView.addSubview(card)
        
        // position all cards below the bottom of the screen
        //  animate them into view in viewDidAppear
        
        let topOffset = UIScreen.main.bounds.height + 10
        
        card.snp.makeConstraints { (make) in
            let t = make.top.equalToSuperview().offset(topOffset).constraint
            card.topConstraint = t
            make.left.equalToSuperview().offset(contentInset)
            make.right.equalToSuperview().offset(-contentInset)
            make.height.equalTo(card.snp.width).multipliedBy(0.5)
            make.bottom.lessThanOrEqualToSuperview()
        }
        
    }
    
    private func generateCards(count: Int) {
        for index in 0..<count {
            let card = CardView(delegate: self)
            card.backgroundColor = colors[index % colors.count]
            card.layer.cornerRadius = 10
            add(card)
        }
    }
}

extension ViewController: CardViewDelegate {
    func didSelect(cardToBeSelected: CardView) {

        guard alreadySelectedCard !== cardToBeSelected else {
            return
        }

        guard
            // get the top "snap constraint" from alreadySelectedCard
            let alreadySnapConstraint = alreadySelectedCard.topConstraint,
            // get its constraint reference so we can get its .constant
            let alreadyConstraint = alreadySnapConstraint.layoutConstraints.first,
            // get the top "snap constraint" from cardToBeSelected
            let toBeSnapConstraint = cardToBeSelected.topConstraint,
            // get its constraint reference so we can get its .constant
            let toBeConstraint = toBeSnapConstraint.layoutConstraints.first
            else { return }

        // save the constant (the Top Offset) from cardToBeSelected
        let tmpOffset = toBeConstraint.constant

        // update the Top Offset for cardToBeSelected with the
        //  constant from alreadySelectedCard (it will be contentInset unless something has changed)
        toBeSnapConstraint.update(offset: alreadyConstraint.constant)
        
        // update the Top Offset for alreadySelectedCard
        alreadySnapConstraint.update(offset: tmpOffset)

        // swap the "z-order" of the views, instead of the view layers
        contentContainerView.insertSubview(alreadySelectedCard, belowSubview: cardToBeSelected)
        
        // animate the change
        let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
            self.contentContainerView.layoutSubviews()
        }
        animator.startAnimation()

        // update alreadySelectedCard
        self.alreadySelectedCard = cardToBeSelected

    }
}

extension ViewController {
    private func initializeScrollView() {
        scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.backgroundColor = .lightGray
        scrollView.contentInsetAdjustmentBehavior = .never
        
        scrollView.snp.makeConstraints { (make) in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }
    
    private func initializeContentContainerView() {
        contentContainerView = UIView()
        scrollView.addSubview(contentContainerView)
        
        contentContainerView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
            make.width.equalToSuperview()
        }
    }
}

protocol CardViewDelegate {
    func didSelect(cardToBeSelected: CardView)
}

class CardView: UIView {
    var tapGestureRecognizer: UITapGestureRecognizer!
    var delegate: CardViewDelegate?
    
    // snap constraint reference so we can modify it later
    weak var topConstraint: Constraint?
    
    convenience init(delegate: CardViewDelegate) {
        self.init(frame: .zero)
        self.delegate = delegate
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapCard))
        tapGestureRecognizer.delegate = self
        addGestureRecognizer(tapGestureRecognizer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc private func didTapCard() {
        delegate?.didSelect(cardToBeSelected: self)
    }
}

extension CardView: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}
于 2020-09-04T18:10:47.397 回答