代码之家  ›  专栏  ›  技术社区  ›  Emre Önder

滑动UIView时出现动画错误

  •  0
  • Emre Önder  · 技术社区  · 6 年前

    我正在开发一个应用程序,它有一个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()
            })
        }
    }
    }
    

    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

    2 回复  |  直到 6 年前
        1
  •  1
  •   E.Coms    6 年前

    我发现你在拍完动画后忘了关掉你的动态发射器。至少,您需要关闭关于卡片的动画[0]。否则,它将变得不可预测。您可以像这样使用removeOldFrontCard()。希望这就是答案。

      func removeOldFrontCard() {
        dynamicAnimator.removeAllBehaviors()
        cards.append( cards.remove(at: 0))
        layoutCards()
    }
    
        2
  •  0
  •   ValW    6 年前

    // 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]
    ...
    }