Utilisation de UIViewPropertyAnimator pour créer des animations personnalisées

Créer des animations est super. Ils sont une partie importante des directives d'interface humaine iOS . Les animations aident à attirer l'attention de l'utilisateur sur des choses importantes ou rendent simplement l'application moins ennuyeuse.

Il existe plusieurs façons d'implémenter une animation dans iOS. La façon la plus populaire est probablement d' utiliser UIView.animate (withDuration: animations :) . Vous pouvez animer un calque d'image à l'aide de CABasicAnimation . De plus, UIKit vous permet de configurer des animations personnalisées pour afficher le contrôleur à l'aide de UIViewControllerTransitioningDelegate .

Dans cet article, je veux discuter d'une autre façon passionnante d'animer des vues - UIViewPropertyAnimator . Cette classe fournit beaucoup plus de fonctions de gestion que son prédécesseur, UIView.animat e. Utilisez-le pour créer des animations temporaires, interactives et interrompues. De plus, il est possible de changer rapidement d'animateur.

Présentation de UIViewPropertyAnimator


UIViewPropertyAnimator a été introduit dans iOS 10 . Il vous permet de créer des animations de manière orientée objet. Regardons un exemple d'animation créée à l'aide de UIViewPropertyAnimator .

image

Voici comment c'était lors de l'utilisation d'UIView.

UIView.animate(withDuration: 0.3) { view.frame = view.frame.offsetBy(dx: 100, dy: 0) } 

Et voici comment le faire avec UIViewPropertyAnimator :

 let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { view.frame = view.frame.offsetBy(dx:100, dy:0) } animator.startAnimation() 

Si vous devez vérifier l'animation, créez simplement un Playground et exécutez le code comme indiqué ci-dessous. Les deux fragments de code conduiront au même résultat.

image

Vous pourriez penser que dans cet exemple, il n'y a pas beaucoup de différence. Alors, quel est l'intérêt d'ajouter une nouvelle façon de créer des animations? UIViewPropertyAnimator devient plus utile lorsque vous devez créer des animations interactives.

Animation interactive et interrompue


Vous souvenez-vous du geste classique «Glissement de doigt pour déverrouiller l'appareil»? Ou le geste "Déplacez votre doigt sur l'écran de bas en haut" pour ouvrir le Control Center? Ce sont d'excellents exemples d'animation interactive. Vous pouvez commencer à déplacer l'image avec votre doigt, puis la relâcher et l'image reviendra à sa position d'origine. De plus, vous pouvez capturer l'image pendant l'animation et continuer à la déplacer avec votre doigt.

Les animations UIView ne permettent pas de contrôler facilement le pourcentage d'achèvement de l'animation. Vous ne pouvez pas suspendre l'animation au milieu d'une boucle et continuer son exécution après une interruption.

Dans ce cas, nous parlerons d' UIViewPropertyAnimator . Ensuite, nous verrons comment vous pouvez facilement créer une animation entièrement interactive et interrompue et une animation inverse en quelques étapes.

Préparation du projet de lancement


Vous devez d'abord télécharger le projet de démarrage . Une fois que vous avez ouvert l'archive, vous trouverez l'application CityGuide qui aide les utilisateurs à planifier leurs vacances. L'utilisateur peut faire défiler la liste des villes, puis ouvrir une description détaillée avec des informations détaillées sur la ville qu'il a aimé.

Considérez le code source du projet avant de commencer à créer de belles animations. Voici ce que vous pouvez trouver dans un projet en l'ouvrant dans Xcode :

  1. ViewController.swift : contrĂ´leur d'application principal avec UICollectionView qui affiche un tableau d'objets City .
  2. CityCollectionViewCell.swift: cellule pour afficher la ville . En fait, dans cet article, la plupart des modifications s'appliqueront à cette classe. Vous pouvez remarquer que descriptionLabel et closeButton sont déjà définis dans la classe. Cependant, après le démarrage de l'application, ces objets seront masqués. Ne vous inquiétez pas, ils seront visibles un peu plus tard. Cette classe a également des propriétés collectionView et index . Plus tard, ils seront utilisés pour l'animation.
  3. CityCollectionViewFlowLayout.swift: Cette classe est responsable du défilement horizontal. Nous ne le changerons pas encore.
  4. City.swift : Le modèle d'application principal possède une méthode utilisée dans ViewController.
  5. Main.storyboard: Vous y trouverez l'interface utilisateur pour ViewController et CityCollectionViewCell .

Essayons de créer et d'exécuter l'exemple d'application. À la suite de cela, nous obtenons ce qui suit.

cityguideapp-iphone8

Implémentation d'une animation de déploiement et de repli


Après avoir démarré l'application, une liste de villes s'affiche. Mais l'utilisateur ne peut pas interagir avec des objets sous forme de cellules. Vous devez maintenant afficher les informations de chaque ville lorsque l'utilisateur clique sur l'une des cellules. Jetez un œil à la version finale de l'application. Voici ce qui devait réellement être développé:

image

L'animation est bonne, non? Mais il n'y a rien de spécial ici, c'est juste la logique de base de UIViewPropertyAnimator . Voyons comment implémenter ce type d'animation. Créez une méthode collectionView (_: didSelectItemAt) , ajoutez le fragment de code suivant à la fin du fichier ViewController :

 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedCell = collectionView.cellForItem(at: indexPath)! as! CityCollectionViewCell selectedCell.toggle() } 

Maintenant, nous devons implémenter la méthode bascule . Passons à CityCollectionViewCell.swift et implémentons cette méthode.

Tout d'abord, ajoutez l'énumération State en haut du fichier, juste avant la déclaration de la classe CityCollectionViewCell . Cette liste vous permet de suivre l'état d'une cellule:

 private enum State { case expanded case collapsed var change: State { switch self { case .expanded: return .collapsed case .collapsed: return .expanded } } } 

Ajoutez des propriétés pour contrôler l'animation dans la classe CityCollectionViewCell :

 private var initialFrame: CGRect? private var state: State = .collapsed private lazy var animator: UIViewPropertyAnimator = { return UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) }() 

La variable initialFrame est utilisée pour stocker l'image de cellule jusqu'à l' exécution de l'animation. état est utilisé pour suivre si la cellule est développée ou réduite. Et la variable animateur est utilisée pour contrôler l'animation.

Ajoutez maintenant la méthode bascule et appelez-la à partir de la méthode close , par exemple:

 @IBAction func close(_ sender: Any) { toggle() } func toggle() { switch state { case .expanded: collapse() case .collapsed: expand() } } 

Ensuite, nous ajoutons deux autres méthodes: expand () et collapse () . Nous poursuivrons leur mise en œuvre. Tout d'abord, nous commençons avec la méthode expansiond () :

 private func expand() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.initialFrame = self.frame self.descriptionLabel.alpha = 1 self.closeButton.alpha = 1 self.layer.cornerRadius = 0 self.frame = CGRect(x: collectionView.contentOffset.x, y:0, width: collectionView.frame.width, height: collectionView.frame.height) if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x -= 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x += 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = false collectionView.allowsSelection = false default: () } } animator.startAnimation() } 

Combien de code. Permettez-moi d'expliquer ce qui se passe étape par étape:

  1. Tout d'abord, vérifiez si collectionView et index ne sont pas égaux à zéro. Sinon, nous ne pourrons pas démarrer l'animation.
  2. Ensuite, commencez à créer l'animation en appelant animator.addAnimations .
  3. Ensuite, enregistrez l'image actuelle, qui est utilisée pour la restaurer dans l'animation de convolution.
  4. Ensuite, nous définissons la valeur alpha pour descriptionLabel et closeButton pour les rendre visibles.
  5. Ensuite, supprimez le coin arrondi et définissez un nouveau cadre pour la cellule. La cellule sera affichée en plein écran.
  6. Ensuite, nous déplaçons les cellules voisines.
  7. Appelez maintenant la méthode animator.addComplete () pour désactiver l'interaction de l'image de la collection. Cela empêche les utilisateurs de le faire défiler pendant l'expansion des cellules. Modifiez également l'état actuel de la cellule. Il est important de changer l'état de la cellule, et ce n'est qu'après que l'animation se termine.

Ajoutez maintenant une animation de convolution. En bref, c'est juste que nous restaurons la cellule à son état précédent:

 private func collapse() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.descriptionLabel.alpha = 0 self.closeButton.alpha = 0 self.layer.cornerRadius = self.cornerRadius self.frame = self.initialFrame! if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x += 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x -= 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = true collectionView.allowsSelection = true default: () } } animator.startAnimation() } 

Il est maintenant temps de compiler et d'exécuter l'application. Essayez de cliquer sur la cellule et vous verrez l'animation. Pour fermer l'image, cliquez sur l'icône en forme de croix dans le coin supérieur droit.

Ajout du traitement gestuel


Vous pouvez prétendre obtenir le même résultat en utilisant UIView.animate . Quel est l'intérêt d'utiliser un UIViewPropertyAnimator ?

Eh bien, il est temps de rendre l'animation interactive. Ajoutez un UIPanGestureRecognizer et une nouvelle propriété appelée popupOffset pour suivre combien la cellule peut être déplacée. Déclarons ces variables dans la classe CityCollectionViewCell :

 private let popupOffset: CGFloat = (UIScreen.main.bounds.height - cellSize.height)/2.0 private lazy var panRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(popupViewPanned(recognizer:))) return recognizer }() 

Ajoutez ensuite la méthode suivante pour enregistrer la définition de balayage:

 override func awakeFromNib() { self.addGestureRecognizer(panRecognizer) } 

Vous devez maintenant ajouter la méthode popupViewPanned pour suivre le mouvement de balayage. Collez le code suivant dans CityCollectionViewCell :

 @objc func popupViewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: toggle() animator.pauseAnimation() case .changed: let translation = recognizer.translation(in: collectionView) var fraction = -translation.y / popupOffset if state == .expanded { fraction *= -1 } animator.fractionComplete = fraction case .ended: animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) default: () } } 

Il y a trois états. Au début du geste, nous initialisons l'animateur à l'aide de la méthode bascule et le mettons immédiatement en pause. Pendant que l'utilisateur fait glisser la cellule, nous mettons à jour l'animation en définissant les propriétés du multiplicateur fractionComplete . C'est la principale magie de l'animateur, qui leur permet de contrôler. Enfin, lorsque l'utilisateur relâche son doigt, la méthode d'animation continueAnimation est appelée pour continuer l'animation. Ensuite, la cellule se déplacera vers la position cible.

Après avoir démarré l'application, vous pouvez faire glisser la cellule vers le haut pour la développer. Faites ensuite glisser la cellule développée vers le bas pour la réduire.

L'animation semble maintenant assez bonne, mais l'interruption de l'animation au milieu n'est pas possible. Par conséquent, pour rendre l'animation entièrement interactive, vous devez ajouter une autre fonction - l'interruption. L'utilisateur peut démarrer l'animation de développement / réduction comme d'habitude, mais l'animation doit être interrompue immédiatement après que l'utilisateur a cliqué sur la cellule pendant le cycle d'animation.

Pour ce faire, enregistrez la progression de l'animation puis tenez compte de cette valeur afin de calculer le pourcentage d'achèvement de l'animation.

Tout d'abord, déclarez une nouvelle propriété dans CityCollectionViewCell :

 private var animationProgress: CGFloat = 0 

Mettez ensuite à jour le bloc .began de la méthode popupViewPanned avec la ligne de code suivante pour vous souvenir de la progression:

 animationProgress = animator.fractionComplete 

Dans le bloc .changed , vous devez mettre à jour la ligne de code suivante afin de calculer correctement le pourcentage d'achèvement:

 animator.fractionComplete = fraction + animationProgress 

Maintenant, l'application est prête pour les tests. Exécutez le projet et voyez ce qui se passe. Si toutes les actions sont effectuées correctement en suivant mes instructions, l'animation devrait ressembler à ceci:

image

Animation inverse


Vous pouvez trouver une faille pour l'implémentation actuelle. Si vous faites glisser un peu la cellule, puis la ramenez à sa position d'origine, la cellule continuera de s'agrandir lorsque vous relâcherez votre doigt. Corrigeons ce problème pour améliorer encore l'animation interactive.
Mettons à jour le bloc .end de la méthode popupViewPanned , comme décrit ci-dessous:

 let velocity = recognizer.velocity(in: self) let shouldComplete = velocity.y > 0 if velocity.y == 0 { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) break } switch state { case .expanded: if !shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } case .collapsed: if shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if !shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } } animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) 

Maintenant, nous prenons en compte la vitesse du geste pour déterminer si l'animation doit être inversée.

Et enfin, insérez une autre ligne de code dans le bloc .changed . Mettez ce code à droite du calcul animator.fractionComplete .

 if animator.isReversed { fraction *= -1 } 

Exécutons à nouveau l'application. Maintenant, tout devrait fonctionner sans faute.

image

Correction du geste de panoramique


Nous avons donc terminé la mise en œuvre de l'animation à l'aide de UIViewPropertyAnimator . Cependant, il y a une erreur désagréable. Vous l'avez peut-être rencontrée lors du test de l'application. Le problème est que le défilement horizontal de la cellule n'est pas possible. Essayons de glisser vers la gauche / la droite à travers les cellules, et nous sommes confrontés au problème.

La raison principale est liée à l' UIPanGestureRecognizer que nous avons créé . Il détecte également un geste de balayage et entre en conflit avec le reconnaisseur de gestes intégré UICollectionView .

Bien que l'utilisateur puisse toujours faire défiler le haut / bas des cellules ou l'espace entre les cellules pour faire défiler les villes, je n'aime toujours pas une interface utilisateur aussi mauvaise. Corrigeons-le.

Pour résoudre les conflits, nous devons implémenter une méthode déléguée appelée gestRecognizerShouldBegin (_ :) . Cette méthode contrôle si le reconnaisseur de gestes doit continuer à interpréter les touches. Si vous retournez false dans la méthode, la reconnaissance des gestes ignorera les touches. Nous allons donc donner à notre propre outil de reconnaissance de panorama la possibilité d'ignorer les mouvements horizontaux.

Pour ce faire, définissons le délégué de notre reconnaissance de panoramique. Insérez la ligne de code suivante dans l'initialisation panRecognizer (vous pouvez placer le code juste avant la reconnaissance de retour :

 recognizer.delegate = self 

Ensuite, nous implémentons la méthode gestRecognizerShouldBegin (_ :) comme suit:

 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return abs((panRecognizer.velocity(in: panRecognizer.view)).y) > abs((panRecognizer.velocity(in: panRecognizer.view)).x) } 

Nous allons ouvrir / fermer si sa vitesse verticale est supérieure à la vitesse horizontale.

Ouah! Testons à nouveau l'application. Vous pouvez maintenant vous déplacer dans la liste des villes en balayant vers la gauche / droite les cellules.

image

Bonus: fonctionnalités de synchronisation personnalisées


Avant de terminer ce didacticiel, parlons des fonctions de synchronisation. Vous souvenez-vous toujours du cas où le développeur vous a demandé d'implémenter une fonction de synchronisation personnalisée pour l'animation que vous créez?

Habituellement, vous devez remplacer UIView.animation par CABasicAnimation ou envelopper dans CATransaction . Avec UIViewPropertyAnimator, vous pouvez facilement implémenter des fonctions de synchronisation personnalisées.

Les fonctions de synchronisation (ou fonctions d'accélération) sont comprises comme des fonctions de vitesse d'animation qui affectent le taux de changement de l'une ou l'autre propriété animée. Quatre types sont actuellement pris en charge: EASYINOUT, EASYIN, EASYOUT, linéaire

Remplacez l'initialisation de l'animateur par ces fonctions de synchronisation (essayez de dessiner votre propre courbe cubique de Bézier) comme suit:

 private lazy var animator: UIViewPropertyAnimator = { let cubicTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.17, y: 0.67), controlPoint2: CGPoint(x: 0.76, y: 1.0)) return UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTiming) }() 

Alternativement, au lieu d'utiliser des paramètres de synchronisation cubiques, vous pouvez également utiliser la synchronisation à ressort, par exemple:

  let springTiming = UISpringTimingParameters(mass: 1.0, stiffness: 2.0, damping: 0.2, initialVelocity: .zero) 

Essayez de redémarrer le projet et voyez ce qui se passe.

Conclusion


Grâce à UIViewPropertyAnimator, vous pouvez améliorer les écrans statiques et l'interaction de l'utilisateur grâce à des animations interactives.

Je sais que vous ne pouvez pas attendre pour réaliser ce que vous avez appris dans votre propre projet. Si vous appliquez cette approche dans votre projet, ce sera très cool, faites le moi savoir en laissant un commentaire ci-dessous.

À titre de référence, vous pouvez télécharger ici le projet final .

Liens supplémentaires


Animations professionnelles utilisant UIKit - https://developer.apple.com/videos/play/wwdc2017/230/

Documentation UIViewPropertyAnimator pour les développeurs Apple - https://developer.apple.com/documentation/uikit/uiviewpropertyanimator

Source: https://habr.com/ru/post/fr458970/


All Articles