滑动 UIView 时出现动画错误

Posted

技术标签:

【中文标题】滑动 UIView 时出现动画错误【英文标题】:Animation bug when swiping UIView 【发布时间】:2018-09-20 13:45:36 【问题描述】:

我正在开发一个应用程序,它有 3 个视图,并且是像 Tinder 中的卡片视图。我正在 for 循环中创建视图。当我有超过 4 个视图时,一切正常。当它只有3张卡时,起初一切看起来都很好,当应用程序打开时,但刷一张卡后,它就坏了。最后一张卡移动有一些错误。我正在尝试编辑代码以使用 3 卡,但无法弄清楚。顺便说一句,ImageCard 只是一个UIView 类。

编辑:我的问题是,当它有 3 张卡片时,应用程序打开时屏幕上显示 3 张卡片,但刷卡后,最后一张卡片没有显示在屏幕上,仅显示 2 张卡片屏幕。在前面刷卡后应该去最后面,应该再次看到3张卡。当它有超过 5 张卡片时,一切正常,就像我解释的那样,屏幕上显示 3 张卡片(它需要是什么)

我确定showNextCard() 函数会出现问题,但请确保这里是完整代码:

class WelcomeViewController: UIViewController 

/// Data structure for custom cards
var cards = [ImageCard]()

override func viewDidLoad() 
    super.viewDidLoad()
    dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
    print(self.view.frame.height)
    print(self.view.frame.width)
    let screenWidth = self.view.frame.width
    let screenHeight = self.view.frame.height
    //When add new cards to self.cards and call layoutCards() again
    for i in 1...5 
        let card = ImageCard(frame: CGRect(x: 0, y: 0, width: screenWidth - screenWidth / 5, height: screenWidth))
        card.tag = i
        card.label.text = "Card Number: \(i)"
        cards.append(card)
    
    lastIndex = cards.count

    // 2. layout the first cards for the user
    layoutCards()



/// Scale and alpha of successive cards visible to the user
let cardAttributes: [(downscale: CGFloat, alpha: CGFloat)] = [(1, 1), (0.92, 0.8), (0.84, 0.6), (0.76, 0.4)]
let cardInteritemSpacing: CGFloat = 12


/// Set up the frames, alphas, and transforms of the first 4 cards on the screen
func layoutCards() 
    // frontmost card (first card of the deck)
    let firstCard = cards[0]
    self.view.addSubview(firstCard)
    firstCard.layer.zPosition = CGFloat(cards.count)
    firstCard.center = self.view.center
    firstCard.frame.origin.y += 23
    firstCard.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleCardPan)))

    // the next 3 cards in the deck
    for i in 1...3 
        if i > (cards.count - 1)  continue 

        let card = cards[i]

        card.layer.zPosition = CGFloat(cards.count - i)

        // here we're just getting some hand-picked vales from cardAttributes (an array of tuples)
        // which will tell us the attributes of each card in the 4 cards visible to the user
        let downscale = cardAttributes[i].downscale
        let alpha = cardAttributes[i].alpha
        card.transform = CGAffineTransform(scaleX: downscale, y: downscale)
        card.alpha = alpha
        // position each card so there's a set space (cardInteritemSpacing) between each card, to give it a fanned out look
        card.center.y = self.view.center.y + 23
        card.frame.origin.x = cards[0].frame.origin.x + (CGFloat(i) * cardInteritemSpacing * 3)
        // workaround: scale causes heights to skew so compensate for it with some tweaking
        if i == 3 
            card.frame.origin.x += 1.5
        

        self.view.addSubview(card)
    

    // make sure that the first card in the deck is at the front
    self.view.bringSubview(toFront: cards[0])


/// This is called whenever the front card is swiped off the screen or is animating away from its initial position.
/// showNextCard() just adds the next card to the 4 visible cards and animates each card to move forward.
func showNextCard() 
    let animationDuration: TimeInterval = 0.2
    // 1. animate each card to move forward one by one
    for i in 1...3
        if i > (cards.count - 1)  continue 
        let card = cards[i]
        let newDownscale = cardAttributes[i - 1].downscale
        let newAlpha = cardAttributes[i - 1].alpha
        UIView.animate(withDuration: animationDuration, delay: (TimeInterval(i - 1) * (animationDuration / 2)), usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: 
            card.transform = CGAffineTransform(scaleX: newDownscale, y: newDownscale)
            card.alpha = newAlpha
            if i == 1 
                card.center = self.view.center
                card.frame.origin.y += 23
             else 
                card.center.y = self.view.center.y + 23
                card.frame.origin.x = self.cards[1].frame.origin.x + (CGFloat(i - 1) * self.cardInteritemSpacing * 3)
            
        , completion:  (_) in
            if i == 1 
                card.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleCardPan)))
            
        )

    

    // 2. add a new card (now the 4th card in the deck) to the very back
    if 4 > (cards.count - 1) 
        if cards.count != 1 
            self.view.bringSubview(toFront: cards[1])
        else
            //self.view.bringSubview(toFront: cards.last!)
        
        return
    
    let newCard = cards[4]
    newCard.layer.zPosition = CGFloat(cards.count - 4)
    let downscale = cardAttributes[3].downscale
    let alpha = cardAttributes[3].alpha

    // initial state of new card
    newCard.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
    newCard.alpha = 0
    newCard.center.y = self.view.center.y + 23
    newCard.frame.origin.x = cards[1].frame.origin.x + (4 * cardInteritemSpacing * 3)
    self.view.addSubview(newCard)

    // animate to end state of new card
    UIView.animate(withDuration: animationDuration, delay: (3 * (animationDuration / 2)), usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [], animations: 
        newCard.transform = CGAffineTransform(scaleX: downscale, y: downscale)
        newCard.alpha = alpha
        newCard.center.y = self.view.center.y + 23
        newCard.frame.origin.x = self.cards[1].frame.origin.x + (3 * self.cardInteritemSpacing) + 1.5
    , completion:  (_) in

    )
    // first card needs to be in the front for proper interactivity
    self.view.bringSubview(toFront: self.cards[1])



/// Whenever the front card is off the screen, this method is called in order to remove the card from our data structure and from the view.
func removeOldFrontCard() 
    cards.append(cards[0])
    cards[0].removeFromSuperview()
    cards.remove(at: 0)
    layoutCards()


private func isVerticalGesture(_ recognizer: UIPanGestureRecognizer) -> Bool 
    let translation = recognizer.translation(in: self.view!)
    if fabs(translation.y) > fabs(translation.x) 
        return true
    
    return false


/// UIKit dynamics variables that we need references to.
var dynamicAnimator: UIDynamicAnimator!
var cardAttachmentBehavior: UIAttachmentBehavior!

/// This method handles the swiping gesture on each card and shows the appropriate emoji based on the card's center.
@objc func handleCardPan(sender: UIPanGestureRecognizer) 

    // Ensure it's a horizontal drag
    let velocity = sender.velocity(in: self.view)
    if abs(velocity.y) > abs(velocity.x) 
        return
    

    // if we're in the process of hiding a card, don't let the user interace with the cards yet
    if cardIsHiding  return 
    // change this to your discretion - it represents how far the user must pan up or down to change the option
    // distance user must pan right or left to trigger an option
    let requiredOffsetFromCenter: CGFloat = 80

    let panLocationInView = sender.location(in: view)
    let panLocationInCard = sender.location(in: cards[0])

    switch sender.state 
    case .began:
        dynamicAnimator.removeAllBehaviors()
        let offset = UIOffsetMake(cards[0].bounds.midX, panLocationInCard.y)
        // card is attached to center
        cardAttachmentBehavior = UIAttachmentBehavior(item: cards[0], offsetFromCenter: offset, attachedToAnchor: panLocationInView)
        //dynamicAnimator.addBehavior(cardAttachmentBehavior)
        let translation = sender.translation(in: self.view)
        print(sender.view!.center.x)

        if(sender.view!.center.x < 555) 

            sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)

        else 
            sender.view!.center = CGPoint(x:sender.view!.center.x, y:554)
        
        sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
    case .changed:
        //cardAttachmentBehavior.anchorPoint = panLocationInView
        let translation = sender.translation(in: self.view)
        print(sender.view!.center.y)

        if(sender.view!.center.x < 555) 

            sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)

        else 
            sender.view!.center = CGPoint(x:sender.view!.center.x, y:554)
        
        sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)

    case .ended:

        dynamicAnimator.removeAllBehaviors()

        if !(cards[0].center.x > (self.view.center.x + requiredOffsetFromCenter) || cards[0].center.x < (self.view.center.x - requiredOffsetFromCenter)) 
            // snap to center
            let snapBehavior = UISnapBehavior(item: cards[0], snapTo: CGPoint(x: self.view.frame.midX, y: self.view.frame.midY + 23))
            dynamicAnimator.addBehavior(snapBehavior)
         else 
            let velocity = sender.velocity(in: self.view)
            let pushBehavior = UIPushBehavior(items: [cards[0]], mode: .instantaneous)
            pushBehavior.pushDirection = CGVector(dx: velocity.x/10, dy: velocity.y/10)
            pushBehavior.magnitude = 175
            dynamicAnimator.addBehavior(pushBehavior)
            // spin after throwing
            var angular = CGFloat.pi / 2 // angular velocity of spin

            let currentAngle: Double = atan2(Double(cards[0].transform.b), Double(cards[0].transform.a))

            if currentAngle > 0 
                angular = angular * 1
             else 
                angular = angular * -1
            
            let itemBehavior = UIDynamicItemBehavior(items: [cards[0]])
            itemBehavior.friction = 0.2
            itemBehavior.allowsRotation = true
            itemBehavior.addAngularVelocity(CGFloat(angular), for: cards[0])
            dynamicAnimator.addBehavior(itemBehavior)

            showNextCard()
            hideFrontCard()

        
    default:
        break
    


/// This function continuously checks to see if the card's center is on the screen anymore. If it finds that the card's center is not on screen, then it triggers removeOldFrontCard() which removes the front card from the data structure and from the view.
var cardIsHiding = false
func hideFrontCard() 
    if #available(ios 10.0, *) 
        var cardRemoveTimer: Timer? = nil
        cardRemoveTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block:  [weak self] (_) in
            guard self != nil else  return 
            if !(self!.view.bounds.contains(self!.cards[0].center)) 
                cardRemoveTimer!.invalidate()
                self?.cardIsHiding = true
                UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn], animations: 
                    self?.cards[0].alpha = 0.0
                , completion:  (_) in
                    self?.removeOldFrontCard()
                    self?.cardIsHiding = false
                )
            
        )
     else 
        // fallback for earlier versions
        UIView.animate(withDuration: 0.2, delay: 1.5, options: [.curveEaseIn], animations: 
            self.cards[0].alpha = 0.0
        , completion:  (_) in
            self.removeOldFrontCard()
        )
    


ImageCard 类:

class ImageCard: UIView 
let label = UILabel()
override init(frame: CGRect) 
    super.init(frame: frame)

    // card style
    self.backgroundColor = UIColor.blue

    self.layer.cornerRadius = 26

    label.font = Font.gothamBold?.withSize(30)
    label.textColor = UIColor.white
    self.addSubview(label)
    label.anchor(self.topAnchor, left: self.leftAnchor, bottom: nil, right: nil, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)


required init?(coder aDecoder: NSCoder) 
    fatalError("init(coder:) has not been implemented")



【问题讨论】:

那么问题出在哪里?您发布了一堆代码和一个 Gif,但没有解释发生了什么问题。 @DuncanC 我编辑我的问题。 【参考方案1】:

我发现你在动画后忘记关闭你的 dynamicAnimator。至少,您需要关闭关于卡[0] 的动画师。否则,它变得不可预测。您可以像这样使用您的 removeOldFrontCard() 。希望这是答案。

  func removeOldFrontCard() 
    dynamicAnimator.removeAllBehaviors()
    cards.append( cards.remove(at: 0))
    layoutCards()

【讨论】:

哇,谢谢!它就像一个魅力!我不认为这是关于 dynamicAnimator。【参考方案2】:

您从索引 1 开始,但数组的索引从 0 开始

// the next 3 cards in the deck
for i in 1...3 
    if i > (cards.count - 1)  continue 
    let card = cards[i]
...

将其更改为:

// the next 3 cards in the deck
for i in 0...2 
    if i > (cards.count - 1)  break 
    let card = cards[i]
...

【讨论】:

if i &gt; (cards.count - 1) continue 也可以更改为 if i &gt; (cards.count - 1) break ,因为循环按升序进行迭代,因此如果条件计算为 true 对于 i 的一个值,它将计算为true 也适用于 i 的所有连续值。 它仍然有那个错误。你跑了吗? 你必须改变你的 if 语句,比如 if i == 3 car​​d.frame.origin.x += 1.5 并解释存在什么错误 -> 给出一些错误消息或类似的东西 Bug 是它需要在屏幕上显示 3 张卡片(就像在第一次打开时一样)但它在刷卡后只显示 2。

以上是关于滑动 UIView 时出现动画错误的主要内容,如果未能解决你的问题,请参考以下文章

滑动 ViewController 时出现导航栏错误

Flutter - AnimatedBuilder ,第一次加载时出现动画/小部件错误

UITableviewCell 中的渐变添加导致重新加载时出现动画问题

调用 Unity Animator.Play() 时出现错误消息:层 -1 无效?

当我尝试在 if-else 方法中使用动画骨架小部件时出现错误 '_debugLifecycleState != _ElementLifecycle.defunct': is not true。”

绘制 UIImage 时出现无效的上下文错误