Comprendre l'UICollectionViewLayout avec l'application Photos

Bonjour, Habr! Je m'appelle Nikita, je travaille sur les SDK mobiles chez ABBYY et je m'occupe également du composant d'interface utilisateur pour numériser et visualiser facilement des documents de plusieurs pages sur un smartphone. Ce composant réduit le temps de développement d'applications basées sur la technologie ABBYY Mobile Capture et se compose de plusieurs parties. Tout d'abord, un appareil photo pour numériser des documents; deuxièmement, un écran d'éditeur avec les résultats de la capture (c'est-à-dire des photos prises automatiquement) et un écran pour corriger les bordures du document.

Il suffit au développeur d'appeler quelques méthodes - et maintenant, dans son application, une caméra est déjà disponible qui numérise automatiquement les documents. Mais, en plus des caméras configurées, vous devez fournir aux clients un accès pratique aux résultats de l'analyse, c'est-à-dire prises automatiquement des photos. Et si le client scanne le contrat ou la charte, il peut y avoir beaucoup de telles photos.

Dans cet article, je parlerai des difficultés survenues lors de la mise en œuvre de l'écran de l'éditeur avec les résultats de la capture de documents. L'écran lui-même est un deux UICollectionView , je les appellerai grands et petits. Je vais omettre les possibilités de régler manuellement les bordures du document et d'autres travaux avec le document, et je me concentrerai sur les animations et les fonctionnalités de mise en page pendant le défilement. Ci-dessous sur GIF, vous pouvez voir ce qui s'est passé à la fin. Un lien vers le référentiel sera à la fin de l'article.



Comme références, je fais souvent attention aux applications du système Apple. Lorsque vous regardez attentivement les animations et autres solutions d'interface de leurs applications, vous commencez à admirer leur attitude attentive aux diverses bagatelles. Nous allons maintenant regarder l'application Photos (iOS 12) comme référence. J'attirerai votre attention sur les fonctionnalités spécifiques de cette application, puis nous essaierons de les implémenter.

Nous allons couvrir la plupart des UICollectionViewFlowLayout personnalisation UICollectionViewFlowLayout , voir comment les techniques courantes telles que la parallaxe et le carrousel sont implémentées, et discuter des problèmes associés aux animations personnalisées lors de l'insertion et de la suppression de cellules.

Examen des fonctionnalités


Pour ajouter des détails, je décrirai quelles petites choses spécifiques m'ont plu dans l'application Photos , puis je les implémenterai dans l'ordre approprié.

  1. Effet de parallaxe dans une grande collection
  2. Les éléments d'une petite collection sont centrés.
  3. Taille dynamique des articles dans une petite collection
  4. La logique de placement des éléments d'une petite cellule dépend non seulement du contentOffset, mais aussi des interactions de l'utilisateur
  5. Animations personnalisées pour déplacer et supprimer
  6. L'index de la cellule "active" n'est pas perdu lors du changement d'orientation

1. Parallaxe


Qu'est-ce que la parallaxe?
Le défilement de parallaxe est une technique en infographie où les images d'arrière-plan passent devant la caméra plus lentement que les images de premier plan, créant une illusion de profondeur dans une scène 2D et ajoutant au sentiment d'immersion dans l'expérience virtuelle.
Vous pouvez remarquer que lors du défilement, le cadre de la cellule se déplace plus rapidement que l'image qui s'y trouve.


Commençons! Créez une sous-classe de la cellule, placez-y l'UIImageView.

 class PreviewCollectionViewCell: UICollectionViewCell { private(set) var imageView = UIImageView()​ override init(frame: CGRect) { super.init(frame: frame) addSubview(imageView) clipsToBounds = true imageView.snp.makeConstraints { $0.edges.equalToSuperview() } } } 

Vous devez maintenant comprendre comment déplacer l' imageView , créant un effet de parallaxe. Pour ce faire, vous devez redéfinir le comportement des cellules lors du défilement. Apple:
Évitez de sous- UICollectionView . La vue de collection a peu ou pas d’aspect propre. Au lieu de cela, il extrait toutes ses vues de votre objet de source de données et toutes les informations liées à la disposition de l'objet de disposition. Si vous essayez de disposer des éléments en trois dimensions, la bonne façon de le faire est d'implémenter une disposition personnalisée qui définit la transformation 3D de chaque cellule et de la visualiser de manière appropriée.
Ok, créons notre objet de mise en page . UICollectionView a une propriété collectionViewLayout , à partir de laquelle il apprend des informations sur le positionnement des cellules. UICollectionViewFlowLayout est une implémentation de l'abrégé UICollectionViewLayout , qui est la propriété collectionViewLayout .
UICollectionViewLayout attend que quelqu'un la sous-classe et fournisse le contenu approprié. UICollectionViewFlowLayout est une classe concrète de UICollectionViewLayout qui a tous ses quatre membres implémentés, de la manière dont les cellules seront organisées de manière en grille.
Créez une sous-classe de UICollectionViewFlowLayout et remplacez sa layoutAttributesForElements(in:) . La méthode renvoie un tableau de UICollectionViewLayoutAttributes , qui fournit des informations sur la façon d'afficher une cellule particulière.

La collection demande des attributs à chaque fois que contentOffset change, ainsi que lorsque la mise en page n'est pas valide. De plus, nous allons créer des attributs personnalisés en ajoutant la propriété parallaxValue , qui détermine combien le cadre de l'image est retardé par rapport au cadre de la cellule. Pour les sous-classes d'attributs, vous devez remplacer NSCopiyng pour elles. Apple:
Si vous sous-classe et implémentez des attributs de disposition personnalisés, vous devez également remplacer la méthode héritée isEqual: pour comparer les valeurs de vos propriétés. Dans iOS 7 et versions ultérieures, la vue de collection n'applique pas d'attributs de disposition si ces attributs n'ont pas changé. Il détermine si les attributs ont changé en comparant les anciens et les nouveaux objets d'attribut à l'aide de la méthode isEqual :. Étant donné que l'implémentation par défaut de cette méthode vérifie uniquement les propriétés existantes de cette classe, vous devez implémenter votre propre version de la méthode pour comparer les propriétés supplémentaires. Si vos propriétés personnalisées sont toutes égales, appelez super et renvoyez la valeur résultante à la fin de votre implémentation.
Comment trouver parallaxValue ? Calculons de combien vous avez besoin pour déplacer le centre de la cellule afin qu'elle se trouve au centre. Si cette distance est supérieure à la largeur de la cellule, martelez-la. Sinon, divisez cette distance par la largeur de la cellule . Plus cette distance est proche de zéro, plus l'effet de parallaxe est faible.

 class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }​ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44​ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }​ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }​ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }​ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }​ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / width​ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } } 


image

Lorsque la collection reçoit les attributs nécessaires, les cellules les appliquent . Ce comportement peut être remplacé dans la sous-classe de la cellule. imageView sur la valeur en fonction de parallaxValue . Cependant, pour que le décalage des images avec contentMode == .aspectFit fonctionne correctement, cela ne suffit pas, car le cadre d'image ne coïncide pas avec le cadre imageView , par lequel le contenu est rogné lorsque clipsToBounds == true . Mettez un masque qui correspond à la taille de l'image avec le contentMode approprié et nous le mettrons à jour si nécessaire. Maintenant, tout fonctionne!

 extension PreviewCollectionViewCell {​ override func layoutSubviews() {​ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)​ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {​ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } } 


2. Les éléments d'une petite collection sont centrés




Ici, tout est très simple. Cet effet peut être obtenu en plaçant de grands inset à gauche et à droite. Lors du défilement vers la droite / gauche, il est nécessaire de commencer à bouncing uniquement lorsque la dernière cellule a quitté le contenu visible. Autrement dit, le contenu visible doit être égal à la taille de la cellule.

 extension ThumbnailFlowLayout {​ var farInset: CGFloat { guard let collection = collectionView else { return .zero } return (collection.bounds.width - itemSize.width) / 2 } var insets: UIEdgeInsets { UIEdgeInsets(top: .zero, left: farInset, bottom: .zero, right: farInset) }​ override func prepare() { collectionView?.contentInset = insets super.prepare() } } 


image


En savoir plus sur le centrage: lorsque la collection a fini de défiler, la mise en page demande un contentOffset pour s'arrêter. Pour ce faire, remplacez targetContentOffset(forProposedContentOffset:withScrollingVelocity:) . Apple:
Si vous souhaitez que le comportement de défilement s'aligne sur des limites spécifiques, vous pouvez remplacer cette méthode et l'utiliser pour modifier le point auquel s'arrêter. Par exemple, vous pouvez utiliser cette méthode pour toujours arrêter le défilement sur une limite entre les éléments, par opposition à l'arrêt au milieu d'un élément.
Pour tout rendre beau, nous nous arrêterons toujours au centre de la cellule la plus proche. Le calcul du centre de la cellule la plus proche est une tâche plutôt triviale, mais vous devez être prudent et considérer le contentInset .

 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collection = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } let cellWithSpacing = itemSize.width + config.distanceBetween let relative = (proposedContentOffset.x + collection.contentInset.left) / cellWithSpacing let leftIndex = max(0, floor(relative)) let rightIndex = min(ceil(relative), CGFloat(itemsCount)) let leftCenter = leftIndex * cellWithSpacing - collection.contentInset.left let rightCenter = rightIndex * cellWithSpacing - collection.contentInset.left​ if abs(leftCenter - proposedContentOffset.x) < abs(rightCenter - proposedContentOffset.x) { return CGPoint(x: leftCenter, y: proposedContentOffset.y) } else { return CGPoint(x: rightCenter, y: proposedContentOffset.y) } } 




3. La taille dynamique des éléments d'une petite collection


Si vous faites défiler une grande collection, le contentOffset change pour une petite contentOffset . De plus, la cellule centrale d'une petite collection n'est pas aussi grande que les autres. Les cellules latérales ont une taille fixe et la cellule centrale coïncide avec le rapport d'aspect de l'image qu'elle contient.


Vous pouvez utiliser la même technique que dans le cas de la parallaxe. Créons un UICollectionViewFlowLayout personnalisé pour une petite collection et redéfinissons prepareAttributes(attributes: donné que la logique de mise en page de la petite collection sera compliquée, nous créerons une entité distincte pour stocker et calculer la géométrie des cellules.

 struct Cell { let indexPath: IndexPath​ let dims: Dimensions let state: State​ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }​ extension Cell { struct Dimensions {​ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }​ struct State {​ let expanding: CGFloat​ static var `default`: State { State(expanding: .zero) } } } 

UICollectionViewFlowLayout a une propriété collectionViewContentSize qui détermine la taille de la zone qui peut défiler. Afin de ne pas compliquer notre vie, laissons-la constante, quelle que soit la taille de la cellule centrale. Pour la géométrie correcte pour chaque cellule, vous devez connaître l' aspectRatio image et l'éloignement du centre de la cellule de contentOffset . Plus la cellule est proche, plus sa size.width / size.height de l' aspectRatio . Lorsque vous redimensionnez une cellule spécifique, déplacez les cellules restantes (à droite et à gauche) à l'aide de affineTransform . Il s'avère que pour calculer la géométrie d'une cellule particulière, vous devez connaître les attributs des voisins (visibles).

 extension Cell {​ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {​ let attributes = layout.layoutAttributesForItem(at: indexPath)​ attributes?.size = size attributes?.center = center​ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)​ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } } 

state.expanding est considéré comme la même chose que parallaxValue .

 func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {​ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)​ guard let attribute = cell.attributes(from: self, with: []) else { return cell }​ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }​ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } } 


4. La logique de placement des éléments d'une petite cellule dépend non seulement du contentOffset, mais aussi des interactions des utilisateurs


Lorsqu'un utilisateur fait défiler une petite collection, toutes les cellules ont la même taille. Lorsque vous faites défiler une grande collection, ce n'est pas le cas. ( voir les gifs 3 et 5 ). Écrivons un animateur qui mettra à jour les propriétés de la disposition ThumbnailLayout . L'animateur stockera DisplayLink en lui-même et appellera le bloc 60 fois par seconde, donnant accès à la progression actuelle. Il est facile de easing functions diverses easing functions à l'animateur. La mise en œuvre peut être consultée sur le github au lien à la fin du post.

Entrons la propriété ThumbnailLayout dans ThumbnailLayout , par laquelle l' expanding toutes les Cell sera multipliée. Il s'avère que expandingRate indique dans quelle mesure l' aspectRatio image particulière affectera sa taille si elle devient centrée. Avec expandingRate == 0 toutes les cellules auront la même taille. Au début du défilement d'une petite collection, nous exécuterons un animateur qui définit l' expandingRate à 0, et à la fin du défilement, vice versa, à 1. En fait, lors de la mise à jour de la disposition, la taille de la cellule centrale et des cellules latérales changera. Aucun contentOffset avec contentOffset et secousses!

 class ScrollAnimation: NSObject {​ enum `Type` { case begin case end }​ let type: Type​ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))​ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)​ animator.animate(duration: duration) { _ in completion() } } } 


 func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling) // call ScrollAnimation.run(type: .begin) } }​ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if scrollView == thumbnails.collectionView && !decelerate { thumbnailEndScrolling() } }​ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { thumbnailEndScrolling() } }​ func thumbnailEndScrolling() { handle(event: .endScrolling) // call ScrollAnimation.run(type: .end) } 


5. Animations personnalisées pour déplacer et supprimer


Il existe de nombreux articles expliquant comment créer des animations personnalisées pour mettre à jour les cellules, mais dans notre cas, ils ne nous aideront pas. Les articles et les didacticiels décrivent comment remplacer les attributs d'une cellule mise à jour. Dans notre cas, la modification de la disposition de la cellule supprimée provoque des effets secondaires - l' expanding cellule voisine, qui a tendance à remplacer celle supprimée lors de l'animation, change.

La mise à jour du contenu dans un UICollectionViewFlowLayout fonctionne comme suit. Après avoir supprimé / ajouté une cellule, la méthode prepare(forCollectionViewUpdates:) démarre, donnant un tableau de UICollectionViewUpdateItem , qui nous indique à quelles cellules les index ont été mis à jour / supprimés / ajoutés. Ensuite, la mise en page appellera un groupe de méthodes

 finalLayoutAttributesForDisappearingItem(at:) initialLayoutAttributesForAppearingDecorationElement(ofKind:at:) 

et leurs amis pour la décoration / vues supplémentaires. Lorsque les attributs des données mises à jour sont reçus, finalizeCollectionViewUpdates est appelé. Apple:
La vue de collection appelle cette méthode comme dernière étape avant de procéder à l'animation des modifications. Cette méthode est appelée dans le bloc d'animation utilisé pour effectuer toutes les animations d'insertion, de suppression et de déplacement afin que vous puissiez créer des animations supplémentaires à l'aide de cette méthode si nécessaire. Sinon, vous pouvez l'utiliser pour effectuer des tâches de dernière minute associées à la gestion des informations d'état de votre objet de présentation.
Le problème est que nous ne pouvons spécialiser les attributs que pour la cellule mise à jour , et nous devons les changer pour toutes les cellules, et de différentes manières. La nouvelle cellule centrale doit changer d' aspectRatio et les cellules latérales doivent se transform .


Après avoir examiné le fonctionnement de l'animation par défaut des cellules de collection lors de la suppression / insertion, il est devenu connu que les cellules de la couche dans CABasicAnimation contiennent CABasicAnimation , qui peut y être modifiée si vous souhaitez personnaliser l'animation pour les cellules restantes. Les choses ont empiré lorsque les journaux ont montré qu'entre performBatchUpdates et prepare(forCollectionViewUpdates:) prepareAttributes(attributes:) prepare(forCollectionViewUpdates:) est appelé, et il peut déjà y avoir le mauvais nombre de cellules, bien que collectionViewUpdates n'ait pas encore commencé, il est très difficile de maintenir et de comprendre cela. Que peut-on faire à ce sujet? Vous pouvez désactiver ces animations intégrées!

 final override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { super.prepare(forCollectionViewUpdates: updateItems) CATransaction.begin() CATransaction.setDisableActions(true) }​ final override func finalizeCollectionViewUpdates() { CATransaction.commit() } 

Armés des animateurs déjà écrits, nous ferons toutes les animations nécessaires sur demande de suppression, et nous lancerons la mise à jour dataSource à la fin de l'animation. Ainsi, nous simplifierons l'animation de la collection lors de la mise à jour, puisque nous contrôlons nous-mêmes quand le nombre de cellules va changer.

 func delete( at indexPath: IndexPath, dataSourceUpdate: @escaping () -> Void, completion: (() -> Void)?) {​ DeleteAnimation(thumbnails: thumbnails, preview: preview, index: indexPath).run { let previousCount = self.thumbnails.itemsCount if previousCount == indexPath.row + 1 { self.activeIndex = previousCount - 1 } dataSourceUpdate() self.thumbnails.collectionView?.deleteItems(at: [indexPath]) self.preview.collectionView?.deleteItems(at: [indexPath]) completion?() } } 

Comment ces animations fonctionneront-elles? Dans ThumbnailLayout stockons des brochures facultatives qui mettent à jour la géométrie de cellules spécifiques.

 class ThumbnailLayout {​ typealias CellUpdate = (Cell) -> Cell var updates: [IndexPath: CellUpdate] = [:] // ... override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {​ let cells = (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .map { cell -> Cell in if let update = self.config.updates[cell.indexPath] { return update(cell) } return cell } return cells.compactMap { $0.attributes(from: self, with: cells) } } 

Avec un tel outil, vous pouvez tout faire avec la géométrie des cellules, lancer des mises à jour pendant le travail de l'animateur et les supprimer dans le compliment. Il y a aussi la possibilité de combiner les mises à jour.

 updates[index] = newUpdate(updates[index]) 

Le code d'animation de suppression est plutôt lourd; il se trouve dans le fichier DeleteAnimation.swift du référentiel. L'animation du changement de focus entre les cellules est implémentée de la même manière.



6. L'index de la cellule "active" n'est pas perdu lors du changement d'orientation


scrollViewDidScroll(_ scrollView:) est appelé même si vous scrollViewDidScroll(_ scrollView:) simplement une valeur dans contentOffset , ainsi que lorsque vous changez l'orientation. Lorsque le défilement de deux collections est synchronisé, certains problèmes peuvent survenir lors des mises à jour de la mise en page. L'astuce suivante aide: lors des mises à jour de la mise en page, vous pouvez définir scrollView.delegate sur nil .

 extension ScrollSynchronizer {​ private func bind() { preview.collectionView?.delegate = self thumbnails.collectionView?.delegate = self }​ private func unbind() { preview.collectionView?.delegate = nil thumbnails.collectionView?.delegate = nil } } 

Lors de la mise Ă  jour de la taille des cellules au moment du changement d'orientation, cela ressemblera Ă  ceci:

 extension PhotosViewController {​ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator)​ contentView.synchronizer.unbind() coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.contentView.synchronizer.bind() } } } 

Afin de ne pas perdre le contentOffset souhaité lors de la modification de l'orientation, vous pouvez mettre à jour targetIndexPath dans scrollView.delegate . Lorsque vous modifiez l'orientation, la mise en page sera désactivée si vous remplacez shouldInvalidateLayout(forBoundsChange:) . Lors du changement des bounds mise en page demandera de clarifier contentOffset , pour le clarifier, vous devez redéfinir targetContentOffset(forProposedContentOffset:) . Apple:
Pendant les mises à jour de mise en page ou lors de la transition entre les mises en page, la vue de collection appelle cette méthode pour vous donner la possibilité de modifier le décalage de contenu proposé à utiliser à la fin de l'animation. Vous pouvez remplacer cette méthode si les animations ou la transition peuvent entraîner le positionnement des éléments d'une manière qui n'est pas optimale pour votre conception.

La vue de collection appelle cette méthode après avoir appelé les méthodes prepare() et collectionViewContentSize .


 override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { let targetOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset) guard let layoutHandler = layoutHandler else { return targetOffset } let offset = CGFloat(layoutHandler.targetIndex) / CGFloat(itemsCount) return CGPoint( x: collectionViewContentSize.width * offset - farInset, y: targetOffset.y) } 





, !

github.com/YetAnotherRzmn/PhotosApp

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


All Articles