Анимация в iOS: нативные решения и сторонние фреймворки

Анимация в iOS: нативные решения и сторонние фреймворки

Всем привет! Сегодня мы будем рассматривать анимацию в iOS. Взглянем на несколько довольно-таки легких примеров, пройдемся по преимуществам и недостаткам того, что предлагает нам Apple. Также рассмотрим несколько, на мой взгляд, достойных фреймворков сторонних разработчиков. Кроме того, я покажу примеры использования UIKit Dynamics и Motion Effects, которые доступны с версии iOS 7.

UIKit Dynamics — это физический движок, позволяющий воздействовать на элементы путем добавления поведений (behaviors), таких как gravity, springs, forces, и т.д. А движок при этом уже сам заботится обо всем остальном.

Motion Effects — позволяет вам создавать parallax effects, которые мы, например, видим на рабочем столе, когда немного поворачиваем телефон. Home screen при этом будто бы немного сдвигается в одну сторону, а иконки — в противоположную. За счет этого создается эффект глубины, который вы без особого труда можете использовать в своих приложениях.

UIKit Dynamics

Добавим в storyboard UIView, дадим ему серый цвет и привяжем его с помощью constrains. Тут нужно быть внимательными, так как иногда физический движок плохо воспринимает такую привязку, поэтому нужно просто добавить элемент где-нибудь в viewDidLoad() и задать размер с помощью frame.

UIKit Dynamics storyboard

Далее добавляем несколько свойств в класс ViewController:

var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collision: UICollisionBehavior!

В viewDidLoad() инициализируем эти свойства:

animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [box])
animator.addBehavior(gravity)

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

Добавляем UICollisionBehavior и ставим его свойство translatesReferenceBoundsIntoBoundary в true. Это свойство отвечает за границы системы, если его значение true — границами будут view нашего ViewController’а. Не забываем добавить наше новое поведение к объекту animator — он проводит все расчеты:

//add collision
collision = UICollisionBehavior(items: [box])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)

Теперь серый квадрат ударяется о нижнюю часть экрана — отлично!

Идем дальше и добавим барьер. Как я писал выше, при добавлении барьера через storyboard движок не смог его точно просчитать, так что добавим его вручную:

//add barrier
let barrier = UIView(frame: CGRect(x: 0.0, y: 400.0, width: 200.0, height: 20.0))
barrier.backgroundColor = UIColor.blackColor()
view.addSubview(barrier)

Барьер добавлен, сейчас нужно добавить collision:

let rightEdge = CGPoint(x: barrier.frame.origin.x + barrier.frame.size.width, y: barrier.frame.origin.y)
collision.addBoundaryWithIdentifier("barrier", fromPoint: barrier.frame.origin, toPoint: rightEdge)

UIKit Dynamics падающий куб

Теперь все отлично, система работает, кубик падает и происходит столкновение. Идем дальше и добавим кубику немного «эластичности»:

let itemBehavior = UIDynamicItemBehavior(items: [box])
itemBehavior.elasticity = 0.6
animator.addBehavior(itemBehavior)

Кроме параметра elasticity, в нашем арсенале есть еще пару свойств с которыми можно поиграться:

  • elasticity
  • friction
  • density
  • resistance
  • angularResistance

У нас также есть возможность отслеживать момент, когда кубик сталкивается с препятствиями. Подписываем контроллер как делегат:

collision.collisionDelegate = self

И реализуем следующий метод:

extension ViewController: UICollisionBehaviorDelegate {
 
    func collisionBehavior(behavior: UICollisionBehavior, beganContactForItem item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, atPoint p: CGPoint) {
 
        //change box color, when it collide
        if let itemView = item as? UIView {
              itemView.backgroundColor = UIColor.redColor()
        }
    }
}

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

UIKit Dynamics меняем цвет

Теперь поговорим о том, что происходит за сценой. Каждое поведение (behavior) имеет свойство action. Мы можем добавить блок кода, который наша анимация будет выполнять во время каждого шага (около 400 миллисекунд). Добавим:

        //add trails
        collision.action = {
            self.box.backgroundColor = UIColor.clearColor()
            let newBox = UIView(frame: self.box.frame)
            newBox.layer.borderWidth = 1.0
            newBox.layer.borderColor = UIColor.lightGrayColor().CGColor
            newBox.transform = self.box.transform
            newBox.center = self.box.center
            self.view.addSubview(newBox)
        }

На каждый шаг анимации будет добавляться новый кубик, который использует координаты нашего серого кубика, в то время, как наш «главный герой» будет спрятан, чтобы не мешал любоваться созданным эффектом :)

UIKit Dynamics принцип работы

Так будет выглядеть финальный результат:

UIKit Dynamics финальный результат

Motion Effects

Для следующего примера желательно иметь реальный девайс. На симуляторе повторить этот эффект не удастся, можете не стараться наклонять голову или монитор — я пробовал, ничего не вышло :)

Так выглядит наш storyboard:

Motion Effects storyboard

Также обратите внимание на constraints для backImView:

Motion Effects constraints для storyboard

Сделано это для того, чтобы при сдвиге backImView (картинка с небом) не показывала белый цвет view контроллера. А теперь код:

override func viewDidLoad() {
        super.viewDidLoad()
 
        addMotionEffectToView(backImView, magnitude: 50.0)
        addMotionEffectToView(frontImView, magnitude: -20.0)
    }
 
//MARK: Helpers
    func addMotionEffectToView(view: UIView, magnitude: CGFloat) {
 
        let xMotion = UIInterpolatingMotionEffect(keyPath: "center.x", type: .TiltAlongHorizontalAxis)
        xMotion.minimumRelativeValue = -magnitude
        xMotion.maximumRelativeValue = magnitude
 
        let yMotion = UIInterpolatingMotionEffect(keyPath: "center.y", type: .TiltAlongVerticalAxis)
        yMotion.minimumRelativeValue = -magnitude
        yMotion.maximumRelativeValue = magnitude
 
        let group = UIMotionEffectGroup()
        group.motionEffects = [xMotion, yMotion]
 
        view.addMotionEffect(group)
    }

Мы добавим motion effect к заднему виду (backImView) и к фронтальному (frontImView). midImView нам не интересен, он остается статичным.

Создаем вспомогательный метод addMotionEffectToView и инициализируем его нашими UIImageView с дополнительными свойствами — и все! Хорошего вам parallax-эффекта )

Pop

Начнем с рассмотрения сторонних фреймворков. Первой «жертвой» нашего обзора будет фреймворк Pop от Facebook. Вдобавок к стандартной анимации (POPBasicAnimation) фреймворк поддерживает POPSpringAnimation и POPDecayAnimation, что позволяет добиться реалистичной и физически корректной анимации.

Для примера будем использовать cocoapods. Добавляем в Podfile:

target 'Animation_frameworks' do
use_frameworks!
 
pod 'pop', '~> 1.0'
 
end

В storyboard добавлено несколько элементов и segue для перехода на экран со скотч-терьером, смотрим и умиляемся :)

Реализуем анимацию с помощью Pop в iOS

По клику на каждый элемент происходит конкретная анимация. Начнем c метода animateLabel():

func animateLabel() {
        let anim = POPSpringAnimation(propertyNamed: kPOPLayerScaleX)
        anim.fromValue = 0
        anim.toValue = 1
        anim.dynamicsFriction = 7
 
        anim.completionBlock = { anim, finished in
            print("Label animation completed: \(anim)")
        }
 
        label.layer.pop_removeAllAnimations()
        label.layer.pop_addAnimation(anim, forKey: "scaleX_animation")
    }

Вначале создаем объект POPSpringAnimation, инициализировав его именем того свойства, с которым будем работать. По клику на kPOPLayerScaleX перейдите к файлу, в котором увидите список всех свойств, которые можете анимировать.

Обратите внимание, что можно анимировать как CALayer property names, так и UIView property names — от этого зависит, будем ли мы применять анимацию к объекту или его слою. completionBlock я добавил просто для наглядности, чтобы знать когда закончится анимация.

Перед добавлением анимации желательно удалить уже существующую, даже если она и не применялась — так, на всякий случай:

label.layer.pop_removeAllAnimations

Добавляем анимацию:

label.layer.pop_addAnimation(anim, forKey: “scaleX_animation")

Ключ может быть произвольным. В итоге наш UILabel-объект начинает растягиваться по оси Х.

Следующий метод:

func animateSegCtrl() {
        //UIView animation
        let anim = POPSpringAnimation(propertyNamed: kPOPViewAlpha)
        anim.fromValue = 0.0
        anim.toValue = 1.0
        anim.dynamicsFriction = 3.0
 
        segCtrl.pop_removeAllAnimations()
        segCtrl.pop_addAnimation(anim, forKey: "alpha_animation")
    }

По клику на UISegmentedControl запускаем мигающую анимацию свойства alpha.

Продвигаемся к более сложному примеру:

func animateOrangeBox() {
        orangeBox.pop_removeAllAnimations()
 
        let anim = POPSpringAnimation(propertyNamed: kPOPLayerBounds)
        anim.toValue = NSValue(CGRect: CGRect(x: orangeBox.bounds.origin.x, y: orangeBox.bounds.origin.y, width: 200.0, height: 20.0))
        anim.springBounciness = 8
        orangeBox.pop_addAnimation(anim, forKey: "stretchRound")
 
        let cornerRadius = POPBasicAnimation(propertyNamed: kPOPLayerCornerRadius)
        cornerRadius.duration = 0.3
        cornerRadius.toValue = 10.0 //half of height
        cornerRadius.setValue("animProp", forKey: "cornerRadKey")
        cornerRadius.delegate = self
        orangeBox.layer.pop_addAnimation(cornerRadius, forKey: "cornerRadius")
    }

Опять будем использовать POPSpringAnimation и, на этот раз, анимировать kPOPLayerBounds. Эта анимация отвечает за изменения размеров (frame) UIView. Вначале мы придаем UIView вид, похожий на status bar.

Далее анимируем cornerRadius — тут используем просто POPBasicAnimation. И подписываемся делегатом на pop_animationDidStop. Реализовываем сам метод:

extension ViewController: POPAnimationDelegate {
    func pop_animationDidStop(anim: POPAnimation!, finished: Bool) {
        if finished {
            if anim.valueForKey("cornerRadKey")!.isEqual("animProp") {
                print("corner radius did finished")
                self.addProgressBarAnimation()
            }
        }
    }
}

Далее в теле метода вызываем еще одну анимацию:

 func addProgressBarAnimation() {
        bar.frame = CGRectInset(orangeBox.frame, 4.0, 2.0)
        bar.layer.cornerRadius = 5.0
 
        let alphaAnim = POPBasicAnimation(propertyNamed: kPOPViewAlpha)
        alphaAnim.duration = 0.5
        alphaAnim.toValue = 0.8
        bar.pop_addAnimation(alphaAnim, forKey: "anim")
 
        let anim = POPBasicAnimation(propertyNamed: kPOPLayerBounds)
        anim.fromValue = NSValue(CGRect: CGRect(x: bar.frame.origin.x, y: bar.frame.origin.y, width: 0.0, height: bar.frame.size.height))
        anim.toValue = NSValue(CGRect: bar.frame)
        anim.duration = 3.0
        anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        bar.pop_addAnimation(anim, forKey: "bar")
    }

В итоге у нас выходит вот такая вот метаморфоза простого оранжевого кубика в status bar :)

Анимация в iOS с помощью фреймворка Pop

И последний вид анимации, который я хотел бы рассмотреть, это custom segue animation.

Вот как создание подобного segue выглядит в storyboard:

Создание seague в storyboard

CustomSegue — это класс, унаследованный от UIStoryboardSegue, который отвечает за анимацию при переходе между контроллерами.

Чтобы сделать простую анимацию, мы перепишем всего один метод:

override func perform() {
        let sourceViewController = self.sourceViewController
        let destinationController = self.destinationViewController
 
        let layer = destinationController.view.layer
        layer.pop_removeAllAnimations()
 
        let sizeAnim = POPSpringAnimation(propertyNamed: kPOPLayerSize)
        sizeAnim.fromValue = NSValue(CGSize: CGSize(width: 10.0, height: 20.0))
 
        let rotAnim = POPSpringAnimation(propertyNamed: kPOPLayerRotationY)
        rotAnim.springSpeed = 3
        rotAnim.dynamicsFriction = 3
        rotAnim.fromValue = M_PI_4
        rotAnim.toValue = 0
 
        layer.pop_addAnimation(rotAnim, forKey: "rotation")
        layer.pop_addAnimation(sizeAnim, forKey: "size")
 
        rotAnim.completionBlock = { _, _ in
            print("Rotation animation completed!")
        }
        sizeAnim.completionBlock = { _, _ in
            print("Size animation completed!")
        }
 
        //animated should be 'false'!
        sourceViewController.navigationController?.pushViewController(destinationController, animated: false)
    }

В итоге нам удалось немного оживить анимацию. Конечно, в реальном приложении нам пришлось бы еще подчиняться протоколу UIViewControllerTransitioningDelegate и реализовывать два его метода: animationControllerForPresentedController и animationControllerForDismissedController. Тут я лишь хотел показать, как относительно легко можно добиться желаемого результата с использованием фреймворка Pop.

Cheetah

Лично мне очень понравился фреймворк Cheetah. Он удивительно компактный в плане кода и дает вполне приличные результаты.

Откройте проект Cheetah_project. В storyboard у нас содержится всего одна кнопка, по которой мы и будем кликать все время :) Код самой кнопки показан ниже:

@IBAction func buttonAction(sender: AnyObject) {
        box.cheetah
            .scale(1.5)
            .duration(1.0)
            .spring()
            .cornerRadius(20.0)
            .wait()
 
	//reset
            .scale(1.0)
            .cornerRadius(0.0)
            .duration(0.5)
 
            .run()
    }

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

Анимация в iOS  при помощи фреймворка Cheetah

Также советую обратить внимание на framework Spring. Разработчики подготовили очень приятное приложение, с помощью которого вы можете исследовать возможности их продукта и даже смотреть на код, заинтересовавшего вас эффекта. А также на DCAnimationKit — очень простой в использовании framework с кучей уже готовых решений.

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

Или, по крайней мере, вы должны знать методы уже более «высокого» уровня, такие как:

  • UIView.animateWithDuration
  • UIView.animateKeyframesWithDuration
  • UIView.addKeyframeWithRelativeStartTime

А также понимать, что такое CABasicAnimation(), CAPropertyAnimation() и т.д.

Но если вам все-таки нужно быстро применить какую-то функцию, то ваш путь — одно из более «ленивых решений», о которых я писал выше.

Нужен MVP, разработка под iOS, Android или прототип приложения? Ознакомьтесь с нашим портфолио и сделайте заказ уже сегодня!