UICollectionViewLayout für Pizza aus verschiedenen Hälften

Um Pizza aus Hälften zu machen, haben wir zwei UICollectionViewLayout . Ich spreche darüber, wie wir ein solches Layout für iOS geschrieben haben, was uns begegnet ist und was wir abgelehnt haben.



Prototyp


Als wir die Aufgabe hatten, eine Schnittstelle für Pizza aus Hälften zu erstellen, waren wir etwas verwirrt. Ich möchte es schön und klar und bequem und groß und interaktiv und viel mehr. Ich möchte cool machen.


Designer versuchten verschiedene Ansätze: ein Raster aus Pizzen, horizontalen und vertikalen Karten, aber sie entschieden sich für den halben Schlag. Wir wussten nicht, wie wir ein solches Ergebnis erzielen sollten, also begannen wir mit einem Experiment und brauchten zwei Wochen, um den Prototyp zu erstellen. Sogar das grobe Layout konnte allen gefallen. Die Reaktion wurde auf Video aufgezeichnet:



So funktioniert UICollectionView


UICollectionView ist eine Unterklasse von UIScrollView und eine reguläre UIView, deren bounds sich durch Wischen ändern. Wenn Sie .origin , verschieben wir den sichtbaren Bereich, und das Ändern der Größe .size auf die Skalierung aus.


Wenn sich der Bildschirm UICollectionView erstellt die UICollectionView die Zellen (oder verwendet sie erneut), und die Regeln für deren Anzeige werden im UICollectionViewLayout . Wir werden mit ihm arbeiten.


Die Möglichkeiten von UICollectionViewLayout groß. Sie können eine beliebige Beziehung zwischen Zellen angeben. Sie können beispielsweise sehr ähnlich wie iCarousel Folgendes tun :



Erster Ansatz


Das Ändern des Aussehens beim Verschieben des Bildschirms erleichterte mir das Verständnis des Layouts des Layouts.
Wir sind daran gewöhnt, dass sich Zellen auf dem Bildschirm bewegen (das grüne Rechteck ist der Bildschirm des Telefons):



Umgekehrt bewegt sich dieser Bildschirm relativ zu den Zellen. Die Bäume stehen noch, dieser Zug fährt:



Im Beispiel ändern sich die Zellenrahmen nicht, aber die bounds der Sammlung selbst ändern sich. Origin dieser bounds ist das contentOffset bekannte contentOffset .


Um ein Layout zu erstellen, müssen Sie zwei Schritte durchlaufen:


  • Berechnen Sie die Größe aller Zellen
  • Nur auf dem Bildschirm sichtbar anzeigen.

Einfaches Layout wie in UITableView


Das Layout funktioniert nicht direkt mit Zellen. Stattdessen verwenden sie UICollectionViewLayoutAttributes - dies ist der Satz von Parametern, die auf die Zelle angewendet werden. Frame - der Hauptrahmen ist für die Position und Größe der Zelle verantwortlich. Andere Parameter: Transparenz, Versatz, Position in der Tiefe des Bildschirms usw.



Schreiben UICollectionViewLayout ein einfaches UICollectionViewLayout , das das Verhalten einer UITableView wiederholt: Die Zellen nehmen die gesamte Breite ein und gehen nacheinander.


4 Schritte voraus:


  • Berechnen Sie den frame für alle Zellen in der prepare .
  • layoutAttributesForElements(in:) sichtbare Zellen in der layoutAttributesForElements(in:) -Methode zurück.
  • layoutAttributesForItem(at:) die layoutAttributesForItem(at:) anhand ihres Index in der layoutAttributesForItem(at:) -Methode zurück. Diese Methode wird beispielsweise beim Aufrufen der Erfassungsmethode scrollToItem (at :) verwendet.
  • Geben Sie die Abmessungen des resultierenden Inhalts in collectionViewContentSize . So findet der Sammler heraus, wo sich der Rand befindet, zu dem Sie scrollen können.

Auf den meisten Geräten beträgt die Pizzagröße 300 Punkte, dann die Koordinaten und Größen aller Zellen:



Ich habe die Berechnungen in einer separaten Klasse durchgeführt. Es besteht aus zwei Teilen: Es berechnet alle Frames im Konstruktor und gibt dann nur Zugriff auf die fertigen Ergebnisse:


 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]() } 

In der Layoutklasse müssen Sie dann nur noch Parameter aus dem Cache übergeben.


  1. Die prepare Methode ruft die Berechnung aller Frames auf.
  2. layoutAttributesForElements (in :) filtert die Frames. Wenn sich der Rahmen mit dem sichtbaren Bereich schneidet, muss die Zelle angezeigt werden: Berechnen Sie alle Attribute und geben Sie sie an das Array der sichtbaren Zellen zurück.
  3. layoutAttributesForItem (at :) - Berechnet die Attribute für eine einzelne Zelle.

 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 } 

Wir ändern uns nach Ihren Bedürfnissen


Wir haben die Tabellenansicht sortiert, aber jetzt müssen wir ein dynamisches Layout erstellen. Bei jeder Fingerverschiebung berechnen wir die Attribute der Zellen neu: Nehmen Sie bereits gezählte Frames und ändern Sie sie mit .transform . Alle Änderungen werden in einer Unterklasse von PizzaHalfSelectorLayout .


Wir lesen den aktuellen Pizza-Index


Der contentOffset können Sie contentOffset vergessen und durch die Nummer der aktuellen Pizza ersetzen. Dann müssen Sie nicht mehr über Koordinaten nachdenken. Alle Entscheidungen beziehen sich auf die Pizzanummer und deren Verschiebungsgrad von der Mitte des Bildschirms.


Es werden zwei Methoden benötigt: eine konvertiert contentOffset in eine contentOffset , die andere umgekehrt:


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

Die contentOffset Berechnung für die Bildschirmmitte wird in der extension gerendert:


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

Wir halten an der Pizza in der Mitte


Das erste, was wir tun müssen, ist die Pizza in der Mitte des Bildschirms anzuhalten. Die Methode targetContentOffset(forProposedContentOffset:) fragt, wo gestoppt werden soll, wenn sie bei der aktuellen Geschwindigkeit bei proposedContentOffset targetContentOffset(forProposedContentOffset:) anhalten soll.


Die Berechnung ist einfach: Sehen Sie sich an, in welche Pizza das proposedContentOffset ContentOffset fällt, und scrollen Sie so, dass es in der Mitte steht:


  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 hat zwei .normal : .normal und .fast . .fast besser für .fast geeignet:


 collectionView!.decelerationRate = .fast 

Aber es gibt ein Problem: Wenn wir nur ein wenig gescrollt haben, müssen wir auf Pizza bleiben und nicht zum nächsten springen. Es gibt keine Methode zum Ändern der Geschwindigkeit, so dass der Rückwärtssprung zwar in geringer Entfernung, aber mit sehr hoher Geschwindigkeit erfolgt:



Achtung, Hack!


Wenn sich die Zelle nicht ändert, geben wir das aktuelle contentOffset , sodass der contentOffset wird. Dann scrollen wir selbst mit dem Standard scrollToItem zum vorherigen Ort. Leider müssen Sie auch asynchron scrollen, damit der Code nach der return aufgerufen wird. Dann wird es während der Animation nur wenig verblassen.


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

Das Problem ist weg, jetzt kehrt die Pizza reibungslos zurück:



Erhöhen Sie die zentrale Pizza


Wir erzählen das Layout beim Umzug


Es ist notwendig, dass die zentrale Pizza allmählich zunimmt, wenn sie sich dem Zentrum nähert. Dazu müssen Sie die Parameter nicht einmal zu Beginn, sondern jedes Mal am Offset berechnen. Schaltet sich einfach ein:


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

Jetzt werden mit jedem Offset die Methoden prepare und layoutAttributesForElements(in:) aufgerufen. So können wir die UICollectionViewLayoutAttributes viele Male hintereinander aktualisieren und so die Position und Transparenz reibungslos ändern.


Zellen transformieren


Im Tabellenlayout lagen die Zellen untereinander und ihre Koordinaten wurden einmal gezählt. Jetzt werden wir sie abhängig von der Position relativ zur Mitte des Bildschirms ändern. Fügen Sie eine Methode hinzu, die sie im laufenden Betrieb ändert.


In der layoutAttributesForElements Methode müssen layoutAttributesForElements die Attribute aus der Oberklasse updateCells , die Attribute der Zellen updateCells und an die updateCells Methode übergeben:


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

Jetzt werden wir die Attribute der Zelle in einer Funktion ändern:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) 

Während der Bewegung müssen wir die Transparenz und Größe ändern und die Pizza in der Mitte halten.


Die Position der Zelle relativ zur Mitte des Bildschirms wird zweckmäßigerweise in normalisierter Form dargestellt. Befindet sich die Zelle in der Mitte, ist der Parameter 0, wenn er verschoben ist, ändert sich der Parameter von -1 beim Verschieben auf 1 beim Verschieben. Wenn die Werte weiter von Null als 1 / -1 entfernt sind, bedeutet dies, dass die Zelle nicht mehr zentral ist und sich nicht mehr ändert. Ich habe diesen Skalenparameter genannt:



Sie müssen die Differenz zwischen der Mitte des Rahmens und der Mitte des Bildschirms berechnen. Wenn wir die Differenz durch eine Konstante teilen, normalisieren wir den Wert, und min und max führen zu einem Bereich von -1 bis +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)) } } 

Größe


Mit einer normalisierten scale können Sie alles tun. Änderungen von -1 bis +1 sind zu stark, sie müssen für die Größe konvertiert werden. Zum Beispiel möchten wir, dass die Größe auf maximal 0,6 der Größe der zentralen Pizza verringert wird:


  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 Größe der .transform ändert sich relativ zur Mitte der Zellen. Die zentrale Zelle hat normScale = 0, ihre Größe ändert sich nicht:



Transparenz


Die Transparenz kann über den alpha Parameter geändert werden. Der scale , den wir bei der transform ist ebenfalls geeignet.


  cell.alpha = scale 

Jetzt ändert Pizza ihre Größe und Transparenz. Schon nicht so langweilig wie bei normalen Tischen.



Halbieren


Vorher haben wir mit einer Pizza gearbeitet: Wir haben das Referenzsystem von der Mitte aus eingestellt, die Größe und Transparenz geändert. Jetzt müssen Sie in zwei Hälften teilen.


Die Verwendung einer Sammlung ist zu schwierig: Sie müssen für jede Hälfte einen eigenen Gestenhandler schreiben. Es ist einfacher, zwei Sammlungen mit jeweils eigenem Layout zu erstellen. Erst jetzt gibt es anstelle einer ganzen Pizza Hälften.


Zwei Controller, ein Container


Fast immer UIViewController ich einen Bildschirm in mehrere UIViewController , von denen jeder seine eigene Aufgabe hat. Diesmal stellte sich heraus:



  1. Die Hauptsteuerung: Alle Teile sind darin zusammengebaut und die Schaltfläche „Mischen“.
  2. Controller mit zwei Behältern für Hälften, einer zentralen Signatur und Bildlaufanzeigen.
  3. Der Controller mit einem Kollektor (rechts weiß).
  4. Bodenplatte mit Preis.

Um zwischen der linken und der rechten Hälfte zu unterscheiden, habe ich enum gestartet. Es wird im Layout in der .orientation .orientation gespeichert:


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

Wir verschieben die Hälften in die Mitte


Das vorherige Layout hat aufgehört, das zu tun, was wir erwarten: Die Hälften begannen sich in die Mitte ihrer Sammlungen zu verschieben, nicht in die Mitte des Bildschirms:



Die Korrektur ist einfach: Sie müssen die Zellen horizontal zur Hälfte in die Mitte des Bildschirms verschieben:


  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 } 

Der Abstand zwischen den Hälften wird sofort gesteuert.



Zellenversatz


Es war einfach, runde Pizza in ein Quadrat zu passen, und für eine Hälfte braucht man ein halbes Quadrat:



Sie können die Berechnung der Frames neu schreiben: Halbieren Sie die Breite, richten Sie die Frames für jede Hälfte unterschiedlich zur Mitte aus. Ändern Sie der Einfachheit halber einfach den contentMode Bildes in der Zelle:


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


Drücken Sie die Pizza senkrecht


Die Pizzas nahmen ab, aber der Abstand zwischen ihren Zentren änderte sich nicht, es traten große Lücken auf. Sie können sie auf die gleiche Weise kompensieren, wie wir die Hälften in der Mitte ausgerichtet haben.


  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 } 

Infolgedessen sehen alle Kompensationen folgendermaßen aus:


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

Und die Zelleneinstellung ist wie folgt:


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

Nicht verwechseln: Die Rahmeneinstellung sollte vor der Transformation liegen. Wenn Sie die Reihenfolge ändern, ist das Ergebnis der Berechnungen völlig anders.


Fertig! Wir haben die Hälften geschnitten und in der Mitte ausgerichtet:



Bildunterschriften hinzufügen


Header werden auf die gleiche Weise wie Zellen erstellt, nur anstelle von UICollectionViewLayoutAttributes(forCellWith:) Sie den Konstruktor UICollectionViewLayoutAttributes(forSupplementaryViewOfKind:)
und geben Sie sie zusammen mit den layoutAttributesForElements(in:) in layoutAttributesForElements(in:)


Zunächst beschreiben wir eine Methode zum IndexPath eines Headers durch 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 } 

Die Rahmenberechnung ist in der defaultFrameForHeader Methode (später defaultFrameForHeader ) defaultFrameForHeader .


Jetzt können Sie den IndexPath sichtbaren Zellen IndexPath und die IndexPath für sie 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 } 

Ein furchtbar langer Funktionsaufruf ist in den headers(for:) versteckt headers(for:) Methode:


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

zIndex


Jetzt befinden sich die Zellen und Signaturen auf derselben "Höhe", sodass sie sich überlappen können. Um die Header immer höher zu halten, geben Sie ihnen zIndex größer als Null. Zum Beispiel 100.


Wir legen eine Position fest (eigentlich nicht)


Auf dem Bildschirm fixierte Signaturen sind etwas rätselhaft. Sie möchten das Problem beheben, aber umgekehrt, bewegen Sie sich ständig mit den bounds :



Im Code ist alles einfach: Wir erhalten die Position der Signatur auf dem Bildschirm und verschieben sie in 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) } 

Die Höhe der Signaturen kann unterschiedlich sein. Es ist besser, sie im Delegaten (und dort im Cache) zu berücksichtigen.


Animationen animieren


Alles ist den Zellen sehr ähnlich. Basierend auf der aktuellen scale können Sie die Transparenz der Zelle berechnen. Der Versatz kann über .transform eingestellt werden, sodass die Beschriftung relativ zu ihrem Rahmen verschoben wird:


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


Optimieren


Nach dem Hinzufügen von Headern ging die Leistung drastisch zurück. Es ist passiert, weil wir die Signaturen versteckt haben, sie aber trotzdem an UICollectionViewLayoutAttributes . Daraus werden der Hierarchie Überschriften hinzugefügt, die am Layout teilnehmen, aber nicht angezeigt. Wir haben nur die Zellen gezeigt, die sich mit den aktuellen bounds schneiden, und die Header müssen nach alpha gefiltert werden:


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

Wir stimmen der zentralen Unterschrift zu (Originalrezept)


Wir haben großartige Arbeit geleistet, aber es gab einen Widerspruch in der Benutzeroberfläche - wenn Sie zwei identische Hälften auswählen, werden sie zu einer normalen Pizza.


Wir haben beschlossen, es so zu belassen, aber den Zustand korrekt zu verarbeiten, was zeigt, dass es sich um eine normale Pizza handelt. Unsere neue Aufgabe ist es, ein Etikett für identische Pizzen in der Mitte anzuzeigen und an den Rändern zu verstecken.



Das Layout allein ist zu schwer zu lösen, da sich die Inschrift an der Kreuzung zweier Sammler befindet. Es ist einfacher, wenn der Controller, der beide Sammlungen enthält, die Bewegung aller Signaturen koordiniert.


Beim Scrollen übergeben wir den aktuellen Index an den Controller, er sendet den Index an die gegenüberliegende Hälfte. Wenn die Indizes übereinstimmen, wird der Titel der Originalpizza angezeigt. Wenn dies nicht der Fall ist, sind die Signaturen für jede Hälfte sichtbar.


So erfinden Sie Ihre Layouts


Am schwierigsten war es, herauszufinden, wie Sie Ihre Idee in die Datenverarbeitung umsetzen können. Zum Beispiel möchte ich, dass Pizzen wie eine Trommel rollen. Um das Problem zu verstehen, habe ich 4 übliche Schritte durchlaufen:


  1. Ich habe ein paar Staaten gezeichnet.
  2. Ich habe verstanden, wie die Elemente mit der Position des Bildschirms zusammenhängen (die Elemente bewegen sich relativ zur Mitte des Bildschirms).
  3. Erstellt Variablen, mit denen bequem gearbeitet werden kann (Bildschirmmitte, zentraler Pizzarahmen, Skala).
  4. Ich habe mir einfache Schritte ausgedacht, von denen jeder überprüft werden kann.

Zustände und Animationen lassen sich in Keynote leicht zeichnen. Ich nahm das Standardlayout und zeichnete die ersten beiden Schritte:



Das Video sieht folgendermaßen aus:



Es dauerte drei Änderungen:


  1. Anstelle von Frames aus dem Cache nehmen wir centerPizzaFrame .
  2. Lesen Sie mithilfe der scale den Versatz aus diesem Rahmen ab.
  3. 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 

, .


Erscheinungsdatum


, : , . : , .


, :


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

:



github , .


UICollectionViewLayout , A Tour of UICollectionView


, .

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


All Articles