UIViewPropertyAnimator отказов эффект - PullRequest
5 голосов
/ 10 января 2020

Допустим, у меня есть аниматор, который перемещает вид с (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, так что я могу непрерывно изменять размер представления вместе с движения пальцев.

Проблема возникает, когда я хочу добавить какой-то эффект отскакивания в начале или в конце анимации. НЕ просто коэффициент демпфирования, а эффект отскока. Самым простым способом представить это является функция «Swipe-To-Delete» UITableViewCell, где вы можете перетащить кнопку «Удалить» за пределы ее фактической ширины, а затем она возвращается назад.

Эффективно то, чего я хочу достичь, это способ установить свойство fractionComplete вне сегмента [0, 1], поэтому, когда дробь равна 1.2, смещение становится 144 вместо его максимального значения 120.

И сейчас максимальное значение для fractionComplete в точности равно 1.

Ниже приведены несколько примеров для визуализации этой проблемы:

То, что у меня сейчас есть: enter image description here

Чего я хочу достичь: enter image description here


РЕДАКТИРОВАТЬ (19 января):

Извините за задержку с ответом. Вот некоторые пояснения:

Я не использую UIView.animate(...), и вместо этого использую UIViewPropertyAnimator по очень конкретной причине: c: он обрабатывает все тайминги, кривые и скорости.

Например, вы перетащили вид на полпути. Это означает, что продолжительность оставшейся части должна быть в два раза меньше общей продолжительности. Или, если вы перетащили через 99% расстояния, оставшаяся часть должна пройти почти мгновенно.

Кроме того, UIViewPropertyAnimator имеет такие функции, как пауза (когда пользователь начинает перетаскивание еще раз) или обратный ход. (когда пользователь начал перетаскивать влево, но после этого он передумал и переместил палец вправо), от чего я тоже выиграю.

Все это недоступно для простых анимаций UIView или требует TONS усилия в лучшем случае. Он допускает только простые переходы, и это не так.

Вот почему я должен использовать какой-то аниматор.

И, как я упоминал в поток комментариев в ответе, который был удален его издателем, самая сложная часть для меня здесь состоит в том, чтобы симулировать эффект трения: чем дальше вы перетаскиваете, тем меньше фактически перемещается представление. Точно так же, как когда вы пытаетесь перетащить любой UIScrollView за пределы его содержимого.

Спасибо за ваши усилия, ребята, но я не думаю, что какой-либо из этих 2 ответов уместен. Я постараюсь реализовать это поведение, используя UIDynamicAnimator всякий раз, когда у меня есть время. Вероятно, в ближайшие неделю или две. Я опубликую sh мой подход на случай, если у меня будут хорошие результаты.


РЕДАКТИРОВАТЬ (20 января):

Я только что загрузил демонстрационный проект в GitHub, который включает в себя все переходы, которые у меня есть в моем проекте. Так что теперь у вас есть идея, зачем мне нужно для использования аниматоров и как я их использую: https://github.com/demon9733/bouncingview-prototype

Единственный файл, который вас действительно интересует это MainViewController+Modes.swift. Здесь содержится все, что связано с переходами и анимацией.

Что мне нужно сделать, так это дать пользователю возможность перетаскивать область ручки за пределы ширины кнопки «Скрыть» с эффектом демпфирования. Кнопка «Скрыть» появится при перемещении области маркера влево.

PS Я не тестировал эту демонстрацию, поэтому в ней могут быть ошибки, которых нет в моем основном проекте. Так что вы можете спокойно их игнорировать.

Ответы [ 2 ]

1 голос
/ 16 января 2020

вам нужно разрешить жесту панорамирования добраться до нужной 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
            })
        }
    }
}

Редактировать 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()
        }
    }
}
0 голосов
/ 18 января 2020

Здесь возможен подход (упрощенный и немного неуклюжий - только отскок, без кнопки справа, потому что это будет намного больше кода и фактически только вопрос управления кадрами)

Из-за большой задержки UIPanGestureRecognizer в конце я предпочитаю использовать UILongPressGestureRecognizer, так как он дает более быструю обратную связь.

Вот демонстрационный результат

enter image description here

Раскадровка используется ниже ViewController имеет только представление gray-background-rect-container, все остальное выполняется в коде, представленном ниже.

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)
    }
}
...