Всем привет! Сегодня мы будем рассматривать анимацию в 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
.
Далее добавляем несколько свойств в класс 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)
Теперь все отлично, система работает, кубик падает и происходит столкновение. Идем дальше и добавим кубику немного «эластичности»:
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 collideif let itemView = item as? UIView { itemView.backgroundColor = UIColor.redColor()}}}
Так при каждом столкновении мы будем менять цвет кубика на красный:
Теперь поговорим о том, что происходит за сценой. Каждое поведение (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)}
На каждый шаг анимации будет добавляться новый кубик, который использует координаты нашего серого кубика, в то время, как наш «главный герой» будет спрятан, чтобы не мешал любоваться созданным эффектом :)
Так будет выглядеть финальный результат:
Motion Effects
Для следующего примера желательно иметь реальный девайс. На симуляторе повторить этот эффект не удастся, можете не стараться наклонять голову или монитор — я пробовал, ничего не вышло :)
Так выглядит наш storyboard:
Также обратите внимание на constraints для backImView
:
Сделано это для того, чтобы при сдвиге 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 для перехода на экран со скотч-терьером, смотрим и умиляемся :)
По клику на каждый элемент происходит конкретная анимация. Начнем 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 :)
И последний вид анимации, который я хотел бы рассмотреть, это custom segue animation.
Вот как создание подобного segue выглядит в 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()}
И, прописав всего несколько строчек кода, мы получаем анимацию, на которую стандартными методами потратили бы намного больше времени. А главное, чем больше у вас написано кода, тем сложнее его править.
Также советую обратить внимание на framework Spring. Разработчики подготовили очень приятное приложение, с помощью которого вы можете исследовать возможности их продукта и даже смотреть на код, заинтересовавшего вас эффекта. А также на DCAnimationKit — очень простой в использовании framework с кучей уже готовых решений.
Не стоит забывать, что все эти уже готовые решения написаны с использованием низкоуровневых С-функций, таких как CGAffineTransformMakeRotation()
, и если вы хотите всерьез заниматься анимацией, то вам никуда от них не деться.
Или, по крайней мере, вы должны знать методы уже более «высокого» уровня, такие как:
UIView.animateWithDuration
UIView.animateKeyframesWithDuration
UIView.addKeyframeWithRelativeStartTime
А также понимать, что такое CABasicAnimation()
, CAPropertyAnimation()
и т.д.
Но если вам все-таки нужно быстро применить какую-то функцию, то ваш путь — одно из более «ленивых решений», о которых я писал выше.
Нужен MVP, разработка под iOS, Android или прототип приложения? Ознакомьтесь с нашим портфолио и сделайте заказ уже сегодня!