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

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

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

UIKit Dynamics — 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

За кліком на кожен елемент відбувається конкретна анімація. Почнемо з методу 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 або прототип застосунку? Ознайомтеся з нашим портфоліо и зробіть замовлення вже сьогодні!