滑动 UIView 时的动画错误
Animation bug when swiping UIView
我正在开发一个有 3 个视图的应用程序,它是一个类似于 Tinder 的卡片视图。我在 for 循环中创建视图。当我有超过 4 个视图时,一切正常。当它只有 3 张卡时,一开始看起来一切正常,当应用程序打开时,但刷了一张卡后,它就坏了。最后一张牌移动有一些错误。我正在尝试编辑代码以使用 3 张卡,但无法弄清楚。顺便说一下,ImageCard
只是一个 UIView
class。
编辑: 我的问题是,当它有 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:
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")
}
}
您从索引 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]
...
}
我发现你忘记在动画后关闭你的 dynamicAnimator。至少,你需要关闭 cards[0] 的动画。否则,它变得不可预测。您可以像这样使用您的 removeOldFrontCard() 。希望这就是答案。
func removeOldFrontCard() {
dynamicAnimator.removeAllBehaviors()
cards.append( cards.remove(at: 0))
layoutCards()
}
我正在开发一个有 3 个视图的应用程序,它是一个类似于 Tinder 的卡片视图。我在 for 循环中创建视图。当我有超过 4 个视图时,一切正常。当它只有 3 张卡时,一开始看起来一切正常,当应用程序打开时,但刷了一张卡后,它就坏了。最后一张牌移动有一些错误。我正在尝试编辑代码以使用 3 张卡,但无法弄清楚。顺便说一下,ImageCard
只是一个 UIView
class。
编辑: 我的问题是,当它有 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:
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")
}
}
您从索引 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]
...
}
我发现你忘记在动画后关闭你的 dynamicAnimator。至少,你需要关闭 cards[0] 的动画。否则,它变得不可预测。您可以像这样使用您的 removeOldFrontCard() 。希望这就是答案。
func removeOldFrontCard() {
dynamicAnimator.removeAllBehaviors()
cards.append( cards.remove(at: 0))
layoutCards()
}