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