UIViewPropertyAnimator的弹跳效果

5
假设我有一个动画师,将视图从(0, 0)移动到(-120, 0)
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8)

animator.addAnimations { 
    switch state:
    case .normal: view.frame.origin.x = 0
    case .swiped: view.frame.origin.x = -120
    }
}

我将其与UIPanGestureRecognizer一起使用,以便可以随着手指的移动连续调整视图大小。当我想在动画的开头或结尾添加某种弹跳效果时出现问题。我不仅需要阻尼比,还需要弹跳效果。最容易想象这种情况的方法是UITableViewCell的滑动删除功能,在该功能中,您可以将“删除”按钮拖动到其实际宽度之外,然后它会反弹回来。
实际上,我想要实现的是设置fractionComplete属性在[0, 1]段之外的方法,因此当分数为1.2时,偏移量变为最大值120的144
而现在fractionComplete的最大值恰好是1
以下是一些示例,以可视化此问题:
我目前拥有的:enter image description here 我想要实现的:enter image description here 编辑(1月19日):
抱歉我的回复延迟了。以下是一些澄清:
我没有使用UIView.animate(...),而是使用UIViewPropertyAnimator,因为它可以为我处理所有时序、曲线和速度。
例如,您将视图拖动到一半。这意味着剩余部分的持续时间应为总持续时间的两倍。或者如果你拖动99%的距离,它应该几乎立即完成剩余部分。
此外,UIViewPropertyAnimator还具有暂停(当用户再次开始拖动时)或反转(当用户开始向左拖动,但之后改变主意并将手指移向右侧)等功能,我也从中受益。
所有这些简单的UIView动画都无法实现,或者最多需要大量努力。它仅适用于简单的过渡,而这不是这种情况。
这就是为什么我必须使用某种动画器的原因。
正如我在被其发布者删除的答案的评论线程中提到的那样,我在这里最复杂的部分是模拟摩擦效应:您拖得越远,视图实际移动的越少。就像当您尝试将任何UIScrollView拖出其内容之外时一样。
感谢你们的努力,但我不认为这两个答案都是相关的。我会尽可能在最近的一两周内使用UIDynamicAnimator来实现这种行为。如果我有什么好的结果,我会发布我的方法。

编辑(1月20日):

我刚刚在GitHub上上传了一个演示项目,其中包括我项目中所有的转换。现在你可以真正地理解为什么我需要使用动画器以及我如何使用它们:https://github.com/demon9733/bouncingview-prototype

你唯一感兴趣的文件是MainViewController+Modes.swift。所有与过渡和动画相关的内容都在那里。

我需要做的是让用户能够以阻尼效果拖动处理区域超出“隐藏”按钮宽度。在将处理区域向左滑动时,“隐藏”按钮将出现。

P.S. 我并没有真正测试这个演示,所以它可能有我主要项目中没有的错误。因此,你可以放心地忽略它们。


经过一些研究,我找到了这个框架:UIDynamicAnimator。但我仍然不确定它是否能够实现弹跳动画。 - demon9733
你能否通过打印语句确认,当你刷卡时,实际上是触发了“刷卡”这个情况? - Julian Silvestri
@julian-silvestri,我不明白你的问题。第二张图片(我想要实现的效果)是在滑动UITableViewCell时的默认行为。而第一张图片是我的普通UIView,我想模仿这种弹跳效果。问题是,我如何在UIView中获得这种弹跳效果,而不是在UITableViewCell中? - demon9733
@user3344236 你的评论与我的问题完全无关。当用户用手指拖动视图时,使用 UIPanGestureRecognizer 会触发抖动效果。这与动画完成块无关。 - demon9733
顺便问一下你得到了什么帧起点 x 呢?从这里开始。 - ares777
显示剩余10条评论
2个回答

3

您需要允许平移手势来到达所需的x位置,并在平移结束时触发动画。

一种实现方式是:

var initial = CGRect.zero

override func viewDidLayoutSubviews() {
    initial = animatedView.frame
}

@IBAction func pan(_ sender: UIPanGestureRecognizer) {

    let closed = initial
    let open = initial.offsetBy(dx: -120, dy: 0)

    // 1 manage panning along x direction
    sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x, y: (sender.view?.center.y)! )
    sender.setTranslation(CGPoint.zero, in: self.view)

    // 2 animate to needed position once pan ends
    if sender.state == .ended {
        if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
            UIView.animate(withDuration: 1 , animations: {
                sender.view?.frame = closed
            })
        } else {
            UIView.animate(withDuration: 1 , animations: {
                sender.view?.frame = open
            })
        }
    }
}

编辑于1月20日

为了模拟阻尼效果并专门利用UIViewPropertyAnimator

var initialOrigin = CGRect.zero

override func viewDidLayoutSubviews() {
    initialOrigin = animatedView.frame
}

@IBAction func pan(_ sender: UIPanGestureRecognizer) {

    let closed = initialOrigin
    let open = initialOrigin.offsetBy(dx: -120, dy: 0)

    // 1. to simulate dampening
    var multiplier: CGFloat = 1.0
    if animatedView?.frame.origin.x ?? CGFloat(0) > closed.origin.x || animatedView?.frame.origin.x ?? CGFloat(0) < open.origin.x {
        multiplier = 0.2
    } else {
        multiplier = 1
    }

    // 2. animate panning
    sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x * multiplier, y: (sender.view?.center.y)! )
      sender.setTranslation(CGPoint.zero, in: self.view)

    // 3. animate to needed position once pan ends
    if sender.state == .ended {
        if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
            let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
                self.animatedView.frame.origin.x = closed.origin.x
            })
            animate.startAnimation()

        } else {
            let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
                self.animatedView.frame.origin.x = open.origin.x
            })
            animate.startAnimation()
        }
    }
}

我同意这个答案,我提供了一个类似的答案,但没有这个写得好,但是OP不同意。在我看来,这是解决这个问题与OP当前设置的最佳方法。它可能需要OP进行更多的定制,但这是非常优秀的。 - Julian Silvestri
@DharayMistry 关键区别在于您仅在用户结束拖动后创建动画器。但我从一开始就使用它,并且仅在用户平移时更新其“fractionComplete”。在您的情况下,您完全没有使用动画器的好处,实际上可以用简单的UIView动画替换它。但这不是我想要实现的。 - demon9733

0

这里有一个可能的方法(简化且有些粗糙-只有反弹,没有右侧按钮,因为那会涉及更多代码,实际上只是帧管理问题)

由于 UIPanGestureRecognizer 在结束时存在长时间延迟,我更喜欢使用 UILongPressGestureRecognizer ,因为它能提供更快的反馈。

这是演示结果

enter image description here

下面使用的 ViewController 的故事板只有灰色背景的矩形容器视图,其他所有内容都在下面提供的代码中完成。
class ViewController: UIViewController {

    @IBOutlet weak var container: UIView!
    let imageView = UIImageView()

    var initial: CGFloat = .zero
    var dropped = false

    private func excedesLimit() -> Bool {
         // < set here desired bounce limits
        return imageView.frame.minX < -180 || imageView.frame.minX > 80
    }

    @IBAction func pressHandler(_ sender: UILongPressGestureRecognizer) {

        let location = sender.location(in: imageView.superview).x
        if sender.state == .began {
            dropped = false
            initial = location - imageView.center.x
        }
        else if !dropped {
            if (sender.state == .changed) {
                imageView.center = CGPoint(x: location - initial, y: imageView.center.y)
                dropped = excedesLimit()
            }

            if sender.state == .ended || dropped {
                initial = .zero

               // variant with animator
               let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
                  let stickTo: CGFloat = self.imageView.frame.minX < -100 ? -100 : 0 // place for button at right
                  self.imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: self.imageView.frame.origin.y), size: self.imageView.frame.size)
               }
               animator.isInterruptible = true
               animator.startAnimation()

// uncomment below - variant with UIViewAnimation
//                UIView.beginAnimations("bounce", context: nil)
//                UIView.setAnimationDuration(0.2)
//                UIView.setAnimationTransition(.none, for: imageView, cache: true)
//                UIView.setAnimationBeginsFromCurrentState(true)
//
//                let stickTo: CGFloat = imageView.frame.minX < -100 ? -100 : 0 // place for button at right
//                imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: imageView.frame.origin.y), size: imageView.frame.size)

//                UIView.setAnimationDelegate(self)
//                UIView.setAnimationDidStop(#selector(makeBounce))
//                UIView.commitAnimations()
            }
        }
    }

//    @objc func makeBounce() {
//        let bounceAnimation = CABasicAnimation(keyPath: "position.x")
//        bounceAnimation.duration = 0.1
//        bounceAnimation.repeatCount = 0
//        bounceAnimation.autoreverses = true
//        bounceAnimation.fillMode = kCAFillModeBackwards
//        bounceAnimation.isRemovedOnCompletion = true
//        bounceAnimation.isAdditive = false
//        bounceAnimation.timingFunction = CAMediaTimingFunction(name: "easeOut")
//        imageView.layer.add(bounceAnimation, forKey:"bounceAnimation");
//    }

    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.image = UIImage(named: "cat")
        imageView.contentMode = .scaleAspectFill
        imageView.layer.borderColor = UIColor.red.cgColor
        imageView.layer.borderWidth = 1.0
        imageView.clipsToBounds = true
        imageView.isUserInteractionEnabled = true

        container.addSubview(imageView)
        imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
        imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
        imageView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1).isActive = true
        imageView.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1).isActive = true

        let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler(_:)))
        pressGesture.minimumPressDuration = 0
        pressGesture.allowableMovement = .infinity

        imageView.addGestureRecognizer(pressGesture)
    }
}

添加了基于动画师的变体(对于那些喜欢使用UIView动画的人,仍然是注释掉的)。 - Asperi
关键的区别在于您只有在用户结束拖动后才创建动画器。但我从一开始就使用它,并且仅在用户平移时更新其“fractionComplete”。在您的情况下,您完全没有使用动画器的好处,实际上可以用简单的UIView动画替换它。但这不是我想要实现的。 - demon9733

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接