UICollectionViewLayout pour pizza de différentes moitiés

Pour faire de la pizza à partir de moitiés, nous avons utilisé deux UICollectionViewLayout . Je parle de la façon dont nous avons écrit une telle mise en page pour iOS, de ce que nous avons rencontré et refusé.



Prototype


Lorsque nous avons eu la tâche de créer une interface pour la pizza à partir de moitiés, nous étions un peu confus. Je le veux magnifiquement, clairement et commodément, grand, interactif et bien plus encore. Je veux faire cool.


Les concepteurs ont essayé différentes approches: une grille de pizzas, des cartes horizontales et verticales, mais se sont installés sur le demi-coup. Nous ne savions pas comment obtenir un tel résultat, nous avons donc commencé par une expérience et mis deux semaines à réaliser un prototype. Même la disposition grossière a pu plaire à tout le monde. La réaction a été enregistrée sur vidéo:



Fonctionnement d'UICollectionView


UICollectionView est une sous-classe de UIScrollView , et c'est une UIView régulière, avec des bounds changeant à partir d'un balayage. En le déplaçant .origin , nous .origin la zone visible et le changement de .size affecte l'échelle.


Lorsque l'écran UICollectionView crée (ou réutilise) les cellules et les règles pour les afficher sont décrites dans l' UICollectionViewLayout . Nous travaillerons avec lui.


Les possibilités de UICollectionViewLayout grandes, vous pouvez spécifier n'importe quelle relation entre les cellules. Par exemple, vous pouvez faire très similaire à ce que peut faire iCarousel :



Première approche


Changer l'apparence du déplacement de l'écran m'a permis de mieux comprendre la disposition de la disposition.
Nous sommes habitués au fait que les cellules se déplacent autour de l'écran (le rectangle vert est l'écran du téléphone):



Mais vice versa, cet écran se déplace par rapport aux cellules. Les arbres sont immobiles, ce train voyage:



Dans l'exemple, les cadres de cellules ne changent pas, mais les bounds la collection elle-même changent. Origin ces bounds est le contentOffset nous connaissons.


Pour créer une mise en page, vous devez passer par deux étapes:


  • calculer la taille de toutes les cellules
  • afficher uniquement visible sur l'écran.

Disposition simple comme dans UITableView


La disposition ne fonctionne pas directement avec les cellules. Au lieu de cela, ils utilisent UICollectionViewLayoutAttributes - c'est l'ensemble de paramètres qui sera appliqué à la cellule. Frame - le principal, est responsable de la position et de la taille de la cellule. Autres paramètres: transparence, décalage, position dans la profondeur de l'écran, etc.



Pour commencer, écrivons un simple UICollectionViewLayout , qui répète le comportement d'un UITableView : les cellules occupent toute la largeur, vont les unes après les autres.


4 pas en avant:


  • Calculez le frame pour toutes les cellules de la méthode de prepare .
  • Renvoie les cellules visibles dans la layoutAttributesForElements(in:) .
  • Renvoie les paramètres de cellule par son index dans la layoutAttributesForItem(at:) . Par exemple, cette méthode est utilisée lors de l'appel de la méthode de collection scrollToItem (at :).
  • Renvoie les dimensions du contenu résultant dans collectionViewContentSize . Ainsi, le collectionneur saura où la bordure à laquelle vous pouvez faire défiler.

Sur la plupart des appareils, la taille de la pizza sera de 300 points, puis les coordonnées et les tailles de toutes les cellules:



J'ai fait les calculs dans une classe séparée. Il se compose de deux parties: il calcule toutes les images du constructeur, puis ne donne accès qu'aux résultats finis:


 class TableLayoutCache { // MARK: - Calculation func recalculateDefaultFrames(numberOfItems: Int) { defaultFrames = (0..<numberOfItems).map { defaultCellFrame(atRow: $0) } } func defaultCellFrame(atRow row: Int) -> CGRect { let y = itemSize.height * CGFloat(row) let defaultFrame = CGRect(x: 0, y: y, width: collectionWidth, height: itemSize.height) return defaultFrame } // MARK: - Access func visibleRows(in frame: CGRect) -> [Int] { return defaultFrames .enumerated() // Index to frame relation .filter { $0.element.intersects(frame)} // Filter by frame .map { $0.offset } // Return indexes } var contentSize: CGSize { return CGSize(width: collectionWidth, height: defaultFrames.last?.maxY ?? 0) } static var zero: TableLayoutCache { return TableLayoutCache(itemSize: .zero, collectionWidth: 0) } init(itemSize: CGSize, collectionWidth: CGFloat) { self.itemSize = itemSize self.collectionWidth = collectionWidth } private let itemSize: CGSize private let collectionWidth: CGFloat private var defaultFrames = [CGRect]() } 

Ensuite, dans la classe de disposition, il vous suffit de transmettre les paramètres du cache.


  1. La méthode prepare appelle le calcul de toutes les images.
  2. layoutAttributesForElements (in :) filtrera les cadres. Si le cadre coupe la zone visible, alors la cellule doit être affichée: calculez tous les attributs et renvoyez-la au tableau de cellules visibles.
  3. layoutAttributesForItem (at :) - Calcule les attributs d'une seule cellule.

 class TableLayout: UICollectionViewLayout { override var collectionViewContentSize: CGSize { return cache.contentSize } override func prepare() { super.prepare() let numberOfItems = collectionView!.numberOfItems(inSection: section) cache = TableLayoutCache(itemSize: itemSize, collectionWidth: collectionView!.bounds.width) cache.recalculateDefaultFrames(numberOfItems: numberOfItems) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let indexes = cache.visibleRows(in: rect) let cells = indexes.map { (row) -> UICollectionViewLayoutAttributes? in let path = IndexPath(row: row, section: section) let attributes = layoutAttributesForItem(at: path) return attributes }.compactMap { $0 } return cells } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = cache.defaultCellFrame(atRow: indexPath.row) return attributes } var itemSize: CGSize = .zero { didSet { invalidateLayout() } } private let section = 0 var cache = TableLayoutCache.zero } 

Nous changeons selon vos besoins


Nous avons trié la vue du tableau, mais nous devons maintenant créer une disposition dynamique. À chaque déplacement de doigt, nous recalculerons les attributs des cellules: prendre des images qui ont déjà été comptées et les changer en utilisant .transform . Toutes les modifications seront apportées dans une sous-classe de PizzaHalfSelectorLayout .


Nous lisons l'indice actuel des pizzas


Pour plus de commodité, vous pouvez oublier contentOffset et le remplacer par le numéro de la pizza actuelle. Ensuite, vous n'aurez plus besoin de penser aux coordonnées, toutes les décisions seront autour du numéro de la pizza et de son degré de déplacement par rapport au centre de l'écran.


Deux méthodes sont nécessaires: l'une convertit contentOffset en un numéro de pizza, l'autre vice versa:


 extension PizzaHalfSelectorLayout { func contentOffset(for pizzaIndex: Int) -> CGPoint { let cellHeight = itemSize.height let screenHalf = collectionView!.bounds.height / 2 let midY = cellHeight * CGFloat(pizzaIndex) + cellHeight / 2 let newY = midY - screenHalf return CGPoint(x: 0, y: newY) } func pizzaIndex(offset: CGPoint) -> CGFloat { let cellHeight = itemSize.height let proposedCenterY = collectionView!.screenCenterYOffset(for: offset) let pizzaIndex = proposedCenterY / cellHeight return pizzaIndex } } 

Le calcul contentOffset pour le centre de l'écran est rendu en extension :


 extension UIScrollView { func screenCenterYOffset(for offset: CGPoint? = nil) -> CGFloat { let offsetY = offset?.y ?? contentOffset.y let contentOffsetY = offsetY + bounds.height / 2 return contentOffsetY } } 

On s'arrête à la pizza au centre


La première chose que nous devons faire est d'arrêter la pizza au centre de l'écran. La targetContentOffset(forProposedContentOffset:) demande où s'arrêter si, à la vitesse actuelle, elle allait s'arrêter à proposedContentOffset .


Le calcul est simple: regardez dans quelle pizza le proposedContentOffset tombera et faites défiler pour qu'il se trouve au centre:


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) return projectedOffset } 

UIScrollView a deux .normal défilement: .normal et .fast . .fast plus approprié pour .fast :


 collectionView!.decelerationRate = .fast 

Mais il y a un problème: si nous avons fait défiler un peu, nous devons rester sur la pizza et ne pas passer au suivant. Il n'y a pas de méthode pour changer la vitesse, donc le rebond inverse, quoique à petite distance, mais avec une vitesse très élevée:



Attention, hack!


Si la cellule ne change pas, nous contentOffset le contentOffset actuel, donc le défilement s'arrête. Ensuite, nous allons nous-mêmes faire défiler à l'emplacement précédent en utilisant le scrollToItem standard. Hélas, vous devez également faire défiler de manière asynchrone, afin que le code soit appelé après le return , puis il y aura peu de décoloration pendant l'animation.


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) let sameCell = pizzaIndex == currentPizzaIndexInt if sameCell { animateBackwardScroll(to: pizzaIndex) return collectionView!.contentOffset // Stop scroll, we've animated manually } return projectedOffset } /// A bit of magic. Without that, high velocity moves cells backward very fast. /// We slow down the animation private func animateBackwardScroll(to pizzaIndex: Int) { let path = IndexPath(row: pizzaIndex, section: 0) collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) // More magic here. Fix double-step animation. // You can't do it smoothly without that. DispatchQueue.main.async { self.collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) } } 

Le problème a disparu, maintenant la pizza revient en douceur:



Augmenter la pizza centrale


Nous racontons la mise en page lors du déplacement


Il faut augmenter progressivement la pizza centrale à l'approche du centre. Pour ce faire, vous devez calculer les paramètres non pas une fois au début, mais à chaque fois au décalage. S'allume simplement:


  override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } 

Désormais, à chaque décalage, les méthodes prepare et layoutAttributesForElements(in:) seront appelées. Nous pouvons donc mettre à jour UICollectionViewLayoutAttributes plusieurs fois de suite, en changeant en douceur la position et la transparence.


Transformer les cellules


Dans la disposition du tableau, les cellules se trouvaient les unes sous les autres et leurs coordonnées étaient comptées une fois. Nous allons maintenant les modifier en fonction de la position par rapport au centre de l'écran. Ajoutez une méthode qui les changera à la volée.


Dans la méthode layoutAttributesForElements , layoutAttributesForElements devez obtenir les attributs de la superclasse, filtrer les attributs des cellules et les transmettre à la méthode updateCells :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let elements = super.layoutAttributesForElements(in: rect) else { return nil } let cells = elements.filter { $0.representedElementCategory == .cell } self.updateCells(cells) } 

Maintenant, nous allons changer les attributs de la cellule dans une fonction:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) 

Pendant le mouvement, nous devons changer la transparence, la taille et garder la pizza au centre.


La position de la cellule par rapport au centre de l'écran est avantageusement présentée sous une forme normalisée. Si la cellule est au centre, le paramètre est 0, s'il est décalé, le paramètre passe de -1 lors du déplacement à 1 lors du déplacement. Si les valeurs sont plus éloignées de zéro que 1 / -1, cela signifie que la cellule n'est plus centrale et a cessé de changer. J'ai appelé ce paramètre d'échelle:



Vous devez calculer la différence entre le centre du cadre et le centre de l'écran. En divisant la différence par une constante, nous normalisons la valeur, et min et max mèneront à une plage de -1 à +1:


 extension PizzaHalfSelectorLayout { func scale(for row: Int) -> CGFloat { let frame = cache.defaultCellFrame(atRow: row) let scale = self.scale(for: frame) return scale.normalized } func scale(for frame: CGRect) -> CGFloat { let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter // 200 pt let centerOffset = offsetFromScreenCenter(frame) let relativeOffset = centerOffset / criticalOffset return relativeOffset } func offsetFromScreenCenter(_ frame: CGRect) -> CGFloat { return frame.midY - collectionView!.screenCenterYOffset() } } extension CGFloat { var normalized: CGFloat { return CGFloat.minimum(1, CGFloat.maximum(-1, self)) } } 

La taille


Ayant une scale normalisée, vous pouvez tout faire. Les changements de -1 à +1 sont trop forts, ils doivent être convertis pour la taille. Par exemple, nous voulons que la taille diminue jusqu'à un maximum de 0,6 de la taille de la pizza centrale:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) } } 

.transform redimensionne par rapport au centre des cellules. La cellule centrale a normScale = 0, sa taille ne change pas:



La transparence


La transparence peut être modifiée via le paramètre alpha . La valeur d' scale que nous avons utilisée dans la transform convient également.


  cell.alpha = scale 

Maintenant, la pizza change de taille et de transparence. Déjà pas aussi ennuyeux que dans les tables ordinaires.



Bissection


Avant cela, nous avons travaillé avec une pizza: nous avons défini le système de référence à partir du centre, changé la taille et la transparence. Maintenant, vous devez diviser en deux.


Utiliser une collection pour cela est trop difficile: vous devrez écrire votre propre gestionnaire de gestes pour chaque moitié. Il est plus facile de créer deux collections, chacune avec sa propre mise en page. Seulement maintenant, au lieu d'une pizza entière, il y aura des moitiés.


Deux contrôleurs, un conteneur


Presque toujours, je casse un écran en plusieurs UIViewController , chacun avec sa propre tâche. Cette fois, cela s'est avéré comme ceci:



  1. Le contrôleur principal: toutes les pièces y sont assemblées et le bouton «mix».
  2. Contrôleur avec deux conteneurs pour les moitiés, une signature centrale et des indicateurs de défilement.
  3. Le contrôleur avec un collecteur (blanc droit).
  4. Panneau inférieur avec prix.

Pour faire la distinction entre la moitié gauche et la moitié droite, j'ai commencé l' enum , elle est stockée dans la disposition dans la .orientation .orientation:


 enum PizzaHalfOrientation { case left case right func opposite() -> PizzaHalfOrientation { switch self { case .left: return .right case .right: return .left } } } 

Nous déplaçons les moitiés vers le centre


La disposition précédente a cessé de faire ce que nous attendions: les moitiés ont commencé à se déplacer vers le centre de leurs collections, pas vers le centre de l'écran:



La solution est simple: vous devez déplacer horizontalement les cellules à mi-chemin vers le centre de l'écran:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: 0) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: 0) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = collectionView!.bounds.width let scaledElementWidth = element.frame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } 

La distance entre les moitiés est immédiatement contrôlée.



Décalage de cellule


Il était facile d'insérer une pizza ronde dans un carré, et pour la moitié, vous avez besoin d'un demi-carré:



Vous pouvez réécrire le calcul des images: divisez par deux la largeur, alignez les images au centre différemment pour chaque moitié. Pour plus de simplicité, il suffit de changer le contentMode image à l'intérieur de la cellule:


 class PizzaHalfCell: UICollectionViewCell { var halfOrientation: PizzaHalfOrientation = .left { didSet { imageView?.contentMode = halfOrientation == .left ? .topRight : .topLeft self.setNeedsLayout() } } } 


Appuyez la pizza verticalement


Les pizzas ont diminué, mais la distance entre leurs centres n'a pas changé, de grands écarts sont apparus. Vous pouvez les compenser de la même manière que nous avons aligné les moitiés au centre.


  private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let offsetFromCenter = offsetFromScreenCenter(element.frame) let vOffset: CGFloat = PizzaHalfSelectorLayout.verticalOffset( offsetFromCenter: offsetFromCenter, scale: scale) return vOffset } static func verticalOffset(offsetFromCenter: CGFloat, scale: CGFloat) -> CGFloat { return -offsetFromCenter / 4 * scale } 

Par conséquent, toutes les compensations ressemblent à ceci:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) let vOffset = verticalOffset (for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } 

Et le paramètre de cellule est comme ceci:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = scale cell.frame = centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = cellZLevel } } 

Ne confondez pas: le réglage du cadre doit être avant la transformation. Si vous changez l'ordre, le résultat des calculs sera complètement différent.


C'est fait! Nous avons coupé les moitiés et les avons alignées au centre:



Ajouter des légendes


Les en-têtes sont créés de la même manière que les cellules, uniquement à la place de UICollectionViewLayoutAttributes(forCellWith:) vous devez utiliser le constructeur UICollectionViewLayoutAttributes(forSupplementaryViewOfKind:)
et les renvoyer avec les paramètres de cellule dans layoutAttributesForElements(in:)


Tout d'abord, nous décrivons une méthode pour obtenir un en-tête par IndexPath :


  override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes( forSupplementaryViewOfKind: elementKind, with: indexPath) attributes.frame = defaultFrameForHeader(at: indexPath) attributes.zIndex = headerZLevel return attributes } 

Le calcul du cadre est masqué dans la méthode defaultFrameForHeaderdefaultFrameForHeader ).


Vous pouvez maintenant obtenir l' IndexPath cellules visibles et leur montrer les IndexPath :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visiblePaths = cells.map { $0.indexPath } let headers = self.headers(for: visiblePaths) updateHeaders(headers) return cells + headers } 

Un appel de fonction terriblement long est caché dans les en- headers(for:) méthode:


  func headers(for paths: [IndexPath]) -> [UICollectionViewLayoutAttributes] { let headers: [UICollectionViewLayoutAttributes] = paths.map { layoutAttributesForSupplementaryView( ofKind: UICollectionView.elementKindSectionHeader, at: $0) }.compactMap { $0 } return headers } 

zIndex


Maintenant, les cellules et les signatures sont au même niveau de "hauteur", de sorte qu'elles peuvent se chevaucher. Pour garder les en-têtes toujours plus élevés, donnez-leur un zIndex supérieur à zéro. Par exemple, 100.


Nous fixons une position (en fait pas)


Les signatures fixées à l'écran sont un peu déroutantes. Vous voulez corriger, mais vice versa, bougez constamment avec des bounds :



Tout est simple dans le code: on obtient la position de la signature à l'écran et on la contentOffset vers contentOffset :


  func defaultFrameForHeader(at indexPath: IndexPath) -> CGRect { let inset = max(collectionView!.layoutMargins.left, collectionView!.layoutMargins.right) let y = collectionView!.bounds.minY let height = collectionView!.bounds.height let width = collectionView!.bounds.width let headerWidth = width - inset * 2 let headerHeight: CGFloat = 60 let vOffset: CGFloat = 30 let screenY = (height - itemSize.height) / 2 - headerHeight / 2 - vOffset return CGRect(x: inset, y: y + screenY, width: headerWidth, height: headerHeight) } 

La hauteur des signatures peut être différente, il vaut mieux la considérer dans le délégué (et y mettre en cache).


Animation des signatures


Tout est très similaire aux cellules. Sur la base de l' scale actuelle, vous pouvez calculer la transparence de la cellule. Le décalage peut être défini via .transform , de sorte que l'inscription sera décalée par rapport à son cadre:


  func updateHeaders(_ headers: [UICollectionViewLayoutAttributes]) { for header in headers { let scale = self.scale(for: header.indexPath.row) let alpha = 1 - abs(scale) header.alpha = alpha let translation = 20 * scale header.transform = CGAffineTransform(translationX: 0, y: translation) } } 


Optimiser


Après avoir ajouté des en-têtes, les performances ont considérablement baissé. Cela s'est produit parce que nous avons caché les signatures, mais les UICollectionViewLayoutAttributes toujours à UICollectionViewLayoutAttributes . À partir de cela, les en-têtes sont ajoutés à la hiérarchie, participent à la mise en page, mais ne sont pas affichés. Nous avons montré uniquement les cellules qui se croisent avec les bounds actuelles, et les en-têtes doivent être filtrés par alpha :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visibleHeaders = headers.filter { $0.alpha > 0 } return cells + visibleHeaders } 

Nous sommes d'accord avec la signature centrale (recette originale)


Nous avons fait un excellent travail, mais il y avait une contradiction dans l'interface - si vous choisissez deux moitiés identiques, elles se transforment en une pizza régulière.


Nous avons décidé de laisser les choses de cette façon, mais traiter correctement la condition, montrant qu'il s'agissait d'une pizza régulière. Notre nouvelle tâche consiste à afficher une étiquette au centre pour les pizzas identiques et à se cacher le long des bords.



La mise en page seule est trop difficile à résoudre, car l'inscription est à la jonction de deux collectionneurs. Ce sera plus facile si le contrôleur, qui contient les deux collections, coordonne le mouvement de toutes les signatures.


Lors du défilement, nous passons l'index actuel au contrôleur, il envoie l'index à la moitié opposée. Si les indices correspondent, alors il indique le titre de la pizza originale, et s'il est différent, alors les signatures pour chaque moitié sont visibles.


Comment inventer vos mises en page


Le plus difficile a été de comprendre comment mettre votre idée en informatique. Par exemple, je veux que les pizzas roulent comme un tambour. Pour comprendre le problème, j'ai suivi 4 étapes habituelles:


  1. J'ai dessiné quelques états.
  2. J'ai compris comment les éléments sont liés à la position de l'écran (les éléments se déplacent par rapport au centre de l'écran).
  3. Variables créées et faciles à utiliser (centre de l'écran, cadre central de pizza, échelle).
  4. J'ai proposé des étapes simples, chacune pouvant être vérifiée.

Les états et les animations sont faciles à dessiner dans Keynote. J'ai pris la disposition standard et dessiné les deux premières étapes:



La vidéo se présente comme ceci:



Il a fallu trois changements:


  1. Au lieu des images du cache, nous prendrons centerPizzaFrame .
  2. En utilisant l' scale lisez le décalage de ce cadre.
  3. Recalculez zIndex .


     func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = self.horizontalOffset(for: element, scale: scale) let vOffset = self.verticalOffset (for: element, scale: scale) switch self.pizzaHalf { case .left: // Align to right return centerPizzaFrame.offsetBy(dx: hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return centerPizzaFrame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = self.collectionView!.bounds.width let scaledElementWidth = centerPizzaFrame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let totalProgress = self.scale(for: element.frame).normalized(by: 1) let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter * 1.1 return totalProgress * criticalOffset } 


, zIndex . : , , zIndex .


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = self.scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = 1//scale cell.frame = self.centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = self.zIndex(row: cell.indexPath.row) } } private func zIndex(row: Int) -> Int { let numberOfCells = self.cache.defaultFrames.count if row == self.currentPizzaIndexInt { return numberOfCells } else if row < self.currentPizzaIndexInt { return row } else { return numberOfCells - row - 1 } } 

, , :


 row: zIndex` 0: 0 1: 1 2: 2 3: 10 —   4: 5 5: 4 6: 3 7: 2 8: 1 9: 0 

, .



, : , . : , .


, :


  • ,
  • , : , ,
  • ,
  • ,
  • -,
  • «»,
  • Voice Over.

:



github , .


UICollectionViewLayout , A Tour of UICollectionView


, .

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


All Articles