Как реализовать интерактивную анимацию с помощью UIViewPropertyAnimator Swift

1656553574 kak realizovat interaktivnuyu animacziyu s pomoshhyu uiviewpropertyanimator swift

Тревор Филипс

1*3Xnoeplkg9w7hqZxuFIuLw

Давайте отбросим безобразное UIView.animate(…) код и обновить его, не правда ли?

Здесь мы погрузимся в практический пример использования Apple UIViewPropertyAnimator для создания плавной анимации в сочетании с взаимодействием с пользователем.

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

1*MUioYOxvdIlmQCUt8Wuuzw
Анимация с помощью UIViewPropertyAnimator

Фон

Это класс, представленный в iOS 10, который предлагает больше возможностей, чем традиционный UIView.animate(...) функции:

  • Программно запускайте, останавливайте, приостанавливайте или восстанавливайте анимацию в любое время
  • Добавьте анимационные блоки и блоки завершения к аниматору на свободное время
  • Поверните анимацию в любое время
  • «Очищайте» анимацию, то есть программно устанавливайте, как далеко она должна быть сейчас

Начинаем

Сначала мы определим пользовательский контроллер представления BlockViewController который будет представлять каждый цветной квадрат в игре. Примечание: Я не включаю код для цветов, закругленных углов или других аспектов, не имеющих отношения к этому учебнику.

class BlockViewController: UIViewController {    var startingXOffset: CGFloat = 0    var endingXOffset: CGFloat = 0    var startingYOffset: CGFloat = 0    var endingYOffset: CGFloat = 0
    var topConstraint = NSLayoutConstraint()    var leadingConstraint = NSLayoutConstraint()
    var animationDirection: AnimationDirection = .undefined    var isVerticalAnimation: Bool {        return animationDirection == .up            || animationDirection == .down    }    var transitionAnimator: UIViewPropertyAnimator?    var animationProgress: CGFloat = 0}

Свойства topConstraint и leftConstraint определите смещение представления контроллера представления от верхней и левой сторон его супервзгляда (соответственно).

The offset свойства используются UIViewPropertyAnimator чтобы определить, где должна начинаться анимация, а где кончаться. Поскольку блоки в игре могут двигаться как влево/вправо, так и вверх/вниз, мы определяем оба X и Y смещение.

У нас также есть простой список AnimationDirection чтобы помочь с логикой, необходимой для анимации.

enum AnimationDirection: Int {    case up, down, left, right, undefined}

Теперь в контроллере представления viewDidLoad() функции, мы можем установить ограничения примерно так:

topConstraint = view.topAnchor.constraint(equalTo: superview.topAnchor, constant: startingYOffset)
leadingConstraint = view.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: startingXOffset)
topConstraint.isActive = true
leadingConstraint.isActive = true
let recognizer = UIPanGestureRecognizer()
recognizer.addTarget(self, action: #selector(viewPanned(recognizer:))) // will be defined later!
view.addGestureRecognizer(recognizer)
1*JdV9LVgcCFp9BJynQf-EwA

Вспомогательные функции

Давайте также настроим несколько «вспомогательных» функций, которые пригодятся позже. Следующие функции изменят местами значение смещения:

private func swapXConstraints() {    let tmp = endingXOffset    endingXOffset = startingXOffset    startingXOffset = tmp}
private func swapYConstraints() {    let tmp = endingYOffset    endingYOffset = startingYOffset    startingYOffset = tmp}

Это будет полезно для сброса анимации:

private func nullifyAnimations() {    transitionAnimator = nil    animationDirection = .undefined}

И здесь у нас есть функция для реверса аниматора:

private func reverseAnimation() {    guard let animator = transitionAnimator else { return }    animator.isReversed = !animator.isReversed}

Если анимация есть уже бег и isReversed есть true, то мы знаем, что анимация выполняется в обратном направлении. Если анимация есть нет бег и isReversed есть trueпосле запуска анимация будет производиться в обратном направлении.

Наконец, эта маленькая функция занимает velocity представлен как а CGPointи определяет, в каком направлении анимация должна двигаться, если таковая имеется, в зависимости от того, является ли x-компонент или y-компонент скорости больше по величине:

private func directionFromVelocity(_ velocity: CGPoint) -> AnimationDirection {    guard velocity != .zero else { return .undefined }    let isVertical = abs(velocity.y) > abs(velocity.x)    var derivedDirection: AnimationDirection = .undefined    if isVertical {        derivedDirection = velocity.y < 0 ? .up : .down    } else {        derivedDirection = velocity.x < 0 ? .left : .right    }    return derivedDirection}

Взаимодействие с пользователем

Давайте перейдем к самому важному: анимации и взаимодействию с пользователем!

в viewDidLoad() мы добавили распознаватель жестов панорамирования BlockViewController‘s view. Этот опознаватель жестов вызывает функцию viewPanned(recognizer: UIPanGestureRecognizer) когда его состояние меняется.

@objcfunc viewPanned(recognizer: UIPanGestureRecognizer) {  switch recognizer.state {  case .began:    animationProgress = transitionAnimator?.fractionComplete ?? 0  case .changed:    didChangePan(recognizer: recognizer) // described below  case .ended:    didEndPan(recognizer: recognizer) // described below default:    break  }}

Помните, как я упоминал о способности чистить. UIViewPropertyAnimator? The fractionComplete свойство позволяет получить и установить, насколько далеко должен быть аниматор со своими анимациями. Это значение колеблется от 0,0 до 1,0.

1*GwhivLJau6FrlhsRvhDtxg
«Поймать» анимацию на полпути

The animationProgress увлекается в recognizer.state = .began потому что у нас может возникнуть ситуация, показанная выше, когда на середине анимации инициируется жест панорамирования. В этом случае мы хотим «поймать» анимацию в ее текущем состоянии. The animationProgress свойство используется, чтобы включить это поведение.

Функция viewPanned(recognizer: UIPanGestureRecognizer)разгружает большую часть своей логики на две функции, описанные ниже. Наш код станет несколько сложнее, поэтому для улучшения читабельности и подсветки синтаксиса я сейчас перейду на Github Gists.

Комментарии описывают, что происходит. Заметьте, что мы действительно начинаем анимацию (если она не существует), когда state в viewPanned(recognizer: UIPanGestureRecognizer) есть changed а не began. Это потому, что скорость когда state = .began всегда равна нулю. Мы не можем определить направление анимации, пока скорость не будет отличной от нуля, следовательно, ждем пока state = .changed чтобы начать анимацию.

Когда мы звоним transitionAnimator.continueAnimation(...) мы в основном говорим: «Ладно, аниматор, пользователь закончил взаимодействие, так что идите и заканчивайте свое дело сейчас!» Прохождение nil для параметра времени и 0 для коэффициента продолжительности будет нет мгновенно завершить анимацию. Анимация все равно будет плавной до конца.

Логика, объяснение

В конце этой функции вы видите isOpposite переменной и некоторой запутанной логики относительно animator.isReversed? Давайте разберемся, что здесь происходит.

private func oppositeOfInitialAnimation(velocity: CGPoint) -> Bool {    switch animationDirection {    case .up:        return velocity.y > 0    case .down:        return velocity.y < 0    case .left:        return velocity.x > 0    case .right:        return velocity.x < 0    case .undefined:        return false    }}

Переменная isOpposite использует приведенную выше вспомогательную функцию. Он просто принимает скорость как входящую информацию и возвращается true если эта скорость движется обратно направления текущей анимации.

Тогда у нас есть оператор if-else с двумя сценариями:

Случай 1: Жест панорамирования закончился в направлении, обратном его начальному, но аниматор не был изменен. Это значит, что перед вызовом нам нужно перевернуть аниматора. transitionAnimator.continueAnimation(...).

Случай 2: Жест панорамирования завершился начальный режиссуру, но аниматор был перевернутый в некоторой точке. Это значит, что мы снова должны вернуть аниматор перед вызовом transitionAnimator.continueAnimation(...).

1*7drca2ayKwLj7k8FHtvKuA
Почти там!

Анимация

в didChangePan(...) мы звонили beginAnimation() если бы аниматор перехода был nil. Вот реализация этой функции:

Важные вещи, которые происходят:

  • Мы сообщаем делегату о том, что он должен быть установлен startingXOffset, endingXOffset, startingYOffsetи endingYOffset
  • Инициализируем transitionAnimator с блоком анимации, который обновляет ограничение представления, а затем вызов layoutIfNeeded()
  • Настраиваем блок завершения аниматора (описан ниже)
  • Если анимация была инициирована программно (без жесткого панорамирования), мы вызываем transitionAnimator.continueAnimation(...) чтобы анимация завершилась самостоятельно
  • Если анимация была инициирована из жеста панорамирования, мы немедленно пауза анимации, а не позволить ей завершиться. Это потому, что прогресс анимации будет вычищен didChangePan(...)

Завершение анимации

Последняя функция, которую необходимо рассмотреть configureAnimationCompletionBlock()описано ниже:

Если аниматор закончил там, где он начал, мы сбрасываем ограничения до того, как они были до анимации.

Если аниматор кончил, как ожидалось, в другой позиции, мы меняем ограничения местами. Это позволяет анимировать вид вперед и назад, снова и снова.

1*XnrqYkX8KdP0UBce4l4Jmw
Проведите пальцем вперед и назад после замены ограничений в блоке завершения

Наконец, мы делаем скорую проверку здоровья, чтобы убедиться, что position состояние есть .end, вид фактически изменил положение. При разработке программы я столкнулся с некоторыми ошибками, но это решило проблему.

Резюме

Образец BlockViewController код можно найти здесь, но имейте в виду, что он вырван из контекста из большей программы. Из коробки не выйдет.

Для более интересных проектов, начиная от Node.js и заканчивая Raspberry Pi, пожалуйста, посмотрите мой веб-сайт. Или загрузите Bloq бесплатно в App Store.

Добавить комментарий

Ваш адрес email не будет опубликован.