Ошибка анимации при перелистывании UIView - PullRequest
0 голосов
/ 20 сентября 2018

Я занимаюсь разработкой приложения, которое имеет 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

Ответы [ 2 ]

0 голосов
/ 20 сентября 2018

Я обнаружил, что вы забыли отключить динамический аниматор после анимации.По крайней мере, вам нужно отключить аниматор по поводу карт [0].В противном случае это становится непредсказуемым.Вы можете использовать свою функцию removeOldFrontCard () следующим образом.Надеюсь, что это ответ.

  func removeOldFrontCard() {
    dynamicAnimator.removeAllBehaviors()
    cards.append( cards.remove(at: 0))
    layoutCards()
}
0 голосов
/ 20 сентября 2018

Вы начинаете с индекса 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]
...
}
...