de Trevor Phillips

Cum să implementați animații interactive cu UIViewPropertyAnimator de la Swift

Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift

Să-i abandonăm urâtul UIView.animate(…) cod și să-i actualizăm, nu-i așa?

Aici ne vom scufunda într-un exemplu practic folosind Apple UIViewPropertyAnimator pentru a crea animații fluide combinate cu interacțiunea utilizatorului.

Puteți verifica rezultatul final al acestor animații în aplicația gratuită Bloq, care folosește tehnicile descrise mai jos ca bază pentru joc.

Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift
Animații folosind UIViewPropertyAnimator

fundal

Este o clasă introdusă în iOS 10 care oferă mai multe capacități decât tradițională UIView.animate(...) funcții:

  • Porniți, opriți, întrerupeți sau reluați animația în orice moment
  • Adăugați blocuri de animație și blocuri de finalizare la animator în timpul liber
  • Inversați animația în orice moment
  • „Frecați” animația, adică setați programatic cât de departe ar trebui să fie chiar acum

Noțiuni de bază

În primul rând, vom defini un controler de vizualizare personalizat BlockViewController care va reprezenta fiecare pătrat colorat din cadrul jocului. Notă: Nu includ cod pentru culori, colțuri rotunjite sau alte aspecte care nu sunt relevante pentru acest tutorial.

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}

Proprietatile topConstraint și leftConstraint definiți decalajul vizualizării controlerului de vizualizare din partea superioară și din stânga supravegherii sale (respectiv).

offset proprietățile sunt utilizate de UIViewPropertyAnimator pentru a determina unde ar trebui să înceapă animația și unde ar trebui să se termine. Deoarece blocurile din joc se pot deplasa atât la stânga / dreapta, cât și sus / jos, le definim pe ambele X și Y compensări.

De asemenea, avem o enumere simplă AnimationDirection pentru a ajuta cu logica necesară animațiilor.

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

Acum, în controlerul de vizualizare viewDidLoad() funcție, putem configura constrângeri ceva de genul acesta:

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)
1611299407 762 Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift

Funcții de ajutor

Să configurăm, de asemenea, câteva funcții de „ajutor” care vor fi utile mai târziu. Următoarele funcții vor schimba valorile de compensare:

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

Acesta va fi util pentru resetarea animației:

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

Și aici avem o funcție de inversare a animatorului:

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

Dacă animația este deja alergând și isReversed este true, atunci știm că animația rulează în direcția inversată. Dacă animația este nu alergând și isReversed este true, animația va rula în direcția inversată la pornire.

În cele din urmă, această mică funcție necesită velocity reprezentat ca un CGPoint, și determină ce direcție, dacă există, ar trebui să meargă animația în funcție de dacă componenta x sau componenta y a vitezei este mai mare ca mărime:

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}

Interacțiunea cu utilizatorul

Să trecem la lucrurile esențiale: animațiile și interacțiunea cu utilizatorul!

În viewDidLoad() am atașat un dispozitiv de recunoaștere a gestului pan la BlockViewControllerpunctul de vedere. Acest recunoscător de gesturi apelează funcția viewPanned(recognizer: UIPanGestureRecognizer) când starea sa se schimbă.

@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  }}

Amintiți-vă cum am menționat capacitatea de „spălare” a UIViewPropertyAnimator? fractionComplete proprietatea ne permite să ajungem și să stabilim cât de departe ar trebui să fie animatorul cu animațiile sale. Această valoare variază de la 0,0 la 1,0.

1611299408 322 Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift
„Prinderea” animației la jumătatea drumului

animationProgress este capturat în recognizer.state = .began pentru că este posibil să avem o situație arătată mai sus, în care un gest pan este inițiat la mijlocul animației. În acest caz, vrem să „prindem” animația în starea sa actuală. animationProgress proprietatea este utilizată pentru a activa acest comportament de „capturare”.

Functia viewPanned(recognizer: UIPanGestureRecognizer)descarcă cea mai mare parte a logicii sale în două funcții, descrise mai jos. Codul nostru va deveni puțin mai complex, așa că pentru o mai bună lizibilitate și evidențierea sintaxei, voi trece la Github Gists acum.

Comentariile descriu ce se întâmplă. Rețineți că de fapt începem animația (dacă nu există) când state în viewPanned(recognizer: UIPanGestureRecognizer) este changed Decat began. Acest lucru se datorează faptului că viteza când state = .began este întotdeauna zero. Nu putem determina direcția animației până când viteza nu este zero, așadar așteptăm până state = .changed pentru a începe animația.

Când sunăm transitionAnimator.continueAnimation(...) practic spunem: „Bine animator, utilizatorul a terminat de interacționat, așa că du-te și termină afacerea ta acum!” Trecere nil pentru parametrul de sincronizare și 0 pentru factorul de durată va nu face ca animația să se termine instantaneu. În continuare va anima fără probleme până la final.

Logica, explicată

La sfârșitul acestei funcții, vedeți isOpposite variabilă și o oarecare logică confuză în ceea ce privește animator.isReversed? Să înțelegem ce se întâmplă aici.

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    }}

Variabila isOpposite folosește funcția de ajutor de mai sus. Pur și simplu ia o viteză ca intrare și se întoarce true dacă această viteză merge opus direcției curente de animație.

Apoi avem o declarație if-else cu două scenarii:

Cazul 1: Gestul panoramic sa încheiat în sens invers direcției sale inițiale, dar animatorul nu a fost inversat. Aceasta înseamnă că trebuie să inversăm animatorul înainte de a apela transitionAnimator.continueAnimation(...).

Cazul 2: Gestul panului s-a încheiat iniţială direcția, dar animatorul a fost inversat la un moment dat. Aceasta înseamnă că, din nou, trebuie să inversăm animatorul înainte de a apela transitionAnimator.continueAnimation(...).

1611299408 385 Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift
Aproape acolo!

Animaţie

În didChangePan(...) am sunat beginAnimation() dacă animatorul de tranziție a fost nil. Iată implementarea pentru această funcție:

Lucrurile importante care se întâmplă sunt:

  • Notificăm un delegat că ar trebui setat startingXOffset, endingXOffset, startingYOffset, și endingYOffset
  • Inițializăm transitionAnimator cu un bloc de animație care actualizează constrângerile vizualizării, apoi apelați layoutIfNeeded()
  • Configurăm blocul de finalizare al animatorului (descris mai jos)
  • Dacă animația a fost inițiată programatic (nu este implicat niciun gest pan), apelăm transitionAnimator.continueAnimation(...) pentru a permite animației să se termine singură
  • Dacă animația a fost inițiată dintr-un gest panoramic, noi imediat pauză animația, mai degrabă decât să-i permită finalizarea. Acest lucru se datorează faptului că progresul animației va fi eliminat didChangePan(...)

Finalizare animație

Ultima funcție care trebuie abordată este configureAnimationCompletionBlock(), descris mai jos:

Dacă animatorul a terminat de unde a început, vom reseta constrângerile la modul în care erau înainte de animație.

Dacă animatorul a terminat așa cum era de așteptat, într-o poziție diferită, schimbăm constrângerile. Acest lucru permite vizualizarea să fie animată înainte și înapoi, iar și iar.

1611299409 808 Cum sa implementati animatii interactive cu UIViewPropertyAnimator de la Swift
Glisați înainte și înapoi după schimbarea constrângerilor în blocul de finalizare

În cele din urmă, facem o verificare rapidă a sănătății pentru a ne asigura că dacă position starea este .end, vizualizarea a schimbat de fapt poziția. Când am dezvoltat aplicația, am avut un comportament de tip buggy, dar acest lucru a rezolvat problema.

rezumat

Eșantionul BlockViewController cod pot fi găsite aici, dar vă rugăm să rețineți că este scos din context dintr-o aplicație mai mare. Nu va funcționa din cutie.

Pentru proiecte mai interesante, de la Node.js la Raspberry Pi, vă rugăm să nu ezitați vezi site-ul meu web. Sau descărcați Bloq gratuit pe App Store.