Grundlegendes zum UICollectionViewLayout mit der Photos-App

Hallo habr Mein Name ist Nikita, ich arbeite bei ABBYY an mobilen SDKs und beschäftige mich auch mit der UI-Komponente zum Scannen und bequemen Anzeigen mehrseitiger Dokumente auf einem Smartphone. Diese Komponente reduziert die Zeit für die Entwicklung von Anwendungen, die auf der ABBYY Mobile Capture-Technologie basieren , und besteht aus mehreren Teilen. Erstens eine Kamera zum Scannen von Dokumenten; zweitens einen Bearbeitungsbildschirm mit den Aufnahmeergebnissen (dh automatisch aufgenommenen Fotos) und einen Bildschirm zum Korrigieren der Ränder des Dokuments.

Es genügt dem Entwickler, ein paar Methoden aufzurufen - und jetzt ist in seiner Anwendung bereits eine Kamera verfügbar, die Dokumente automatisch scannt. Zusätzlich zu den konfigurierten Kameras müssen Sie den Kunden jedoch einen bequemen Zugriff auf die Scanergebnisse ermöglichen, d. H. automatisch aufgenommene Fotos. Und wenn der Kunde den Vertrag oder die Charta scannt, kann es eine Menge solcher Fotos geben.

In diesem Beitrag werde ich auf die Schwierigkeiten eingehen, die bei der Implementierung des Editor-Bildschirms mit den Ergebnissen der Dokumentenerfassung aufgetreten sind. Der Bildschirm selbst ist ein zwei UICollectionView , ich werde sie groß und klein nennen. Ich werde die Möglichkeiten zum manuellen Anpassen der Ränder des Dokuments und anderer Arbeiten mit dem Dokument weglassen und mich während des Bildlaufs auf Animationen und Layoutfunktionen konzentrieren. Unten auf GIF können Sie sehen, was am Ende passiert ist. Ein Link zum Repository befindet sich am Ende des Artikels.



Als Referenz schaue ich oft auf Apple-Systemanwendungen. Wenn Sie Animationen und andere Schnittstellenlösungen ihrer Anwendungen sorgfältig betrachten, beginnen Sie, ihre aufmerksame Haltung gegenüber verschiedenen Kleinigkeiten zu bewundern. Als Referenz betrachten wir nun die Fotoanwendung (iOS 12). Ich werde Ihre Aufmerksamkeit auf die Besonderheiten dieser Anwendung lenken und dann versuchen, diese umzusetzen.

Wir werden die meisten Anpassungsoptionen UICollectionViewFlowLayout , sehen, wie gängige Techniken wie Parallaxe und Karussell implementiert werden, und die mit benutzerdefinierten Animationen verbundenen Probleme beim Einfügen und Löschen von Zellen diskutieren.

FunktionsĂĽberprĂĽfung


Um Einzelheiten hinzuzufĂĽgen, beschreibe ich, welche besonderen Kleinigkeiten mir in der Anwendung " Fotos" gefallen haben, und setze sie dann in der entsprechenden Reihenfolge um.

  1. Parallaxeffekt in einer groĂźen Sammlung
  2. Elemente einer kleinen Sammlung sind zentriert.
  3. Dynamische Größe von Artikeln in einer kleinen Sammlung
  4. Die Logik zum Platzieren der Elemente einer kleinen Zelle hängt nicht nur vom contentOffset ab, sondern auch von Benutzerinteraktionen
  5. Benutzerdefinierte Animationen zum Verschieben und Löschen
  6. Der Index der "aktiven" Zelle geht beim Orientierungswechsel nicht verloren

1. Parallaxe


Was ist Parallaxe?
Das Parallaxen-Scrollen ist eine Technik in der Computergrafik, bei der sich Hintergrundbilder langsamer an der Kamera vorbeibewegen als Vordergrundbilder, wodurch eine Illusion von Tiefe in einer 2D-Szene erzeugt und das Gefühl des Eintauchens in die virtuelle Erfahrung verstärkt wird.
Sie können feststellen, dass sich der Rahmen der Zelle beim Scrollen schneller bewegt als das Bild, das sich darin befindet.


Fangen wir an! Erstellen Sie eine Unterklasse der Zelle, und fĂĽgen Sie die UIImageView darin ein.

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

Jetzt müssen Sie verstehen, wie Sie die imageView , um einen Parallaxeeffekt zu erzielen. Dazu müssen Sie das Verhalten der Zellen während des Bildlaufs neu definieren. Apple:
Vermeiden Sie die Unterklasse von UICollectionView . Die Sammlungsansicht hat nur ein geringes oder kein eigenes Erscheinungsbild. Stattdessen werden alle Ansichten aus Ihrem Datenquellenobjekt und alle layoutbezogenen Informationen aus dem Layoutobjekt abgerufen. Wenn Sie versuchen, Elemente in drei Dimensionen anzuordnen, mĂĽssen Sie ein benutzerdefiniertes Layout implementieren, mit dem die 3D-Transformation fĂĽr jede Zelle und Ansicht entsprechend festgelegt wird.
Ok, lassen Sie uns unser Layout-Objekt erstellen. UICollectionView verfĂĽgt ĂĽber eine Eigenschaft collectionViewLayout , ĂĽber die Informationen zur Positionierung von Zellen UICollectionView werden. UICollectionViewFlowLayout ist eine Implementierung des abstrakten UICollectionViewLayout , der Eigenschaft collectionViewLayout .
UICollectionViewLayout wartet darauf, dass jemand eine Unterklasse erstellt und den entsprechenden Inhalt bereitstellt. UICollectionViewFlowLayout ist eine konkrete Klasse von UICollectionViewLayout , in der alle vier UICollectionViewLayout implementiert sind, dass die Zellen in einer Rasteranordnung angeordnet werden.
Erstellen Sie eine Unterklasse von UICollectionViewFlowLayout und überschreiben Sie deren layoutAttributesForElements(in:) . Die Methode gibt ein Array von UICollectionViewLayoutAttributes , das Informationen zum Anzeigen einer bestimmten Zelle enthält.

Die Auflistung fordert Attribute jedes Mal an, wenn sich das contentOffset ändert, sowie wenn das Layout ungültig ist. Darüber hinaus erstellen wir benutzerdefinierte Attribute, indem wir die Eigenschaft parallaxValue hinzufügen, mit der festgelegt wird, um wie viel das Bild gegenüber dem Bild der Zelle verzögert wird. Für Attributunterklassen müssen Sie NSCopiyng für sie überschreiben. Apple:
Wenn Sie benutzerdefinierte Layoutattribute in Unterklassen unterteilen und implementieren, müssen Sie auch die geerbte Methode isEqual: überschreiben, um die Werte Ihrer Eigenschaften zu vergleichen. In iOS 7 und höher werden in der Sammlungsansicht keine Layoutattribute angewendet, wenn diese Attribute nicht geändert wurden. Sie ermittelt, ob sich die Attribute geändert haben, indem sie die alten und neuen Attributobjekte mit der Methode isEqual: vergleicht. Da die Standardimplementierung dieser Methode nur die vorhandenen Eigenschaften dieser Klasse überprüft, müssen Sie Ihre eigene Version der Methode implementieren, um zusätzliche Eigenschaften zu vergleichen. Wenn Ihre benutzerdefinierten Eigenschaften alle gleich sind, rufen Sie super und geben Sie den resultierenden Wert am Ende Ihrer Implementierung zurück.
Wie finde ich parallaxValue ? Lassen Sie uns berechnen, wie viel Sie benötigen, um die Mitte der Zelle so zu verschieben, dass sie in der Mitte steht. Wenn dieser Abstand größer als die Breite der Zelle ist, hämmern Sie darauf. Teilen Sie andernfalls diesen Abstand durch die Breite der Zelle . Je näher dieser Abstand an Null ist, desto schwächer ist der Parallaxeneffekt.

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


Bild

Wenn die Auflistung die erforderlichen Attribute erhält, wenden die Zellen sie an. Dieses Verhalten kann in der Unterklasse der Zelle überschrieben werden. imageView auf den Wert in Abhängigkeit von parallaxValue . Damit die Verschiebung von Bildern mit contentMode == .aspectFit ordnungsgemäß funktioniert, reicht dies jedoch nicht aus, da der Bilderrahmen nicht mit dem imageView Rahmen übereinstimmt, um den der Inhalt beschnitten wird, wenn clipsToBounds == true . Setzen Sie eine Maske, die der Größe des Bildes entspricht, mit dem entsprechenden contentMode und wir werden es bei Bedarf aktualisieren. Jetzt funktioniert alles!

 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. Elemente einer kleinen Sammlung sind zentriert




Hier ist alles sehr einfach. Dieser Effekt kann erzielt werden, indem links und rechts große inset . Wenn Sie nach rechts / links scrollen, müssen Sie erst dann mit dem bouncing beginnen bouncing wenn die letzte Zelle den sichtbaren Inhalt verlassen hat. Das heißt, der sichtbare Inhalt sollte der Größe der Zelle entsprechen.

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


Bild


Weitere contentOffset zum Zentrieren: Wenn die Sammlung den Bildlauf abgeschlossen hat, fordert das Layout ein contentOffset an, bei dem contentOffset soll. Ăśberschreiben Sie dazu targetContentOffset(forProposedContentOffset:withScrollingVelocity:) . Apple:
Wenn Sie möchten, dass das Bildlaufverhalten an bestimmten Grenzen ausgerichtet wird, können Sie diese Methode überschreiben und den Punkt ändern, an dem der Bildlauf angehalten werden soll. Sie können diese Methode beispielsweise verwenden, um den Bildlauf an einer Grenze zwischen Elementen immer anzuhalten, anstatt in der Mitte eines Elements anzuhalten.
Um alles schön zu machen, halten wir immer in der Mitte der nächsten Zelle. Das Berechnen des Mittelpunkts der nächsten Zelle ist eine eher triviale Aufgabe, aber Sie müssen vorsichtig sein und das 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. Die dynamische Größe der Elemente einer kleinen Sammlung


Wenn Sie durch eine große Sammlung contentOffset ändert sich das contentOffset für eine kleine. Darüber hinaus ist die zentrale Zelle einer kleinen Sammlung nicht so groß wie die anderen. Seitenzellen haben eine feste Größe und die mittlere fällt mit dem Seitenverhältnis des enthaltenen Bildes zusammen.


Sie können die gleiche Technik wie bei der Parallaxe anwenden. Erstellen wir ein benutzerdefiniertes UICollectionViewFlowLayout für eine kleine Sammlung und definieren prepareAttributes(attributes: neu prepareAttributes(attributes: Da die prepareAttributes(attributes: der kleinen Sammlung weiterhin kompliziert ist, erstellen wir eine separate Entität zum Speichern und Berechnen der Zellgeometrie.

 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 verfügt über eine collectionViewContentSize Eigenschaft, die die Größe des Bereichs bestimmt, in dem ein UICollectionViewFlowLayout kann. Um unser Leben nicht zu verkomplizieren, lassen wir es konstant, unabhängig von der Größe der zentralen Zelle. Für die richtige Geometrie für jede Zelle müssen Sie das aspectRatio Bildes und die Entfernung der Mitte der Zelle von contentOffset . Je näher die Zelle ist, size.width / size.height näher ist ihre size.width / size.height am aspectRatio . Verschieben Sie beim affineTransform einer bestimmten Zelle die verbleibenden Zellen (rechts und links davon) mit affineTransform . Es stellt sich heraus, dass Sie zum Berechnen der Geometrie einer bestimmten Zelle die Attribute der Nachbarn (sichtbar) kennen müssen.

 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 wird als fast gleichbedeutend mit 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. Die Logik zum Platzieren der Elemente einer kleinen Zelle hängt nicht nur vom contentOffset ab, sondern auch von Benutzerinteraktionen


Wenn ein Benutzer einen Bildlauf durch eine kleine Sammlung durchführt, haben alle Zellen dieselbe Größe. Beim Scrollen einer großen Sammlung ist dies nicht der Fall. ( siehe GIFs 3 und 5 ). Schreiben wir einen Animator, der die Eigenschaften des ThumbnailLayout Layouts aktualisiert. Der Animator speichert DisplayLink in sich selbst und ruft den Block 60 Mal pro Sekunde auf, um Zugriff auf den aktuellen Fortschritt zu erhalten. Es ist einfach, verschiedene easing functions am Animator zu easing functions . Die Implementierung kann auf dem Github unter dem Link am Ende des Beitrags eingesehen werden.

Geben Sie die Eigenschaft ThumbnailLayout in ThumbnailLayout , mit der die expanding aller Cell multipliziert wird. Es stellt sich heraus, dass die expandingRate aspectRatio , wie stark sich das aspectRatio bestimmten Bildes auf dessen Größe auswirkt, wenn es zentriert wird. Mit expandingRate == 0 alle Zellen dieselbe Größe. Am Anfang des Bildlaufs einer kleinen Sammlung wird ein Animator ausgeführt, der die expandingRate auf 0 und am Ende des Bildlaufs umgekehrt auf 1 setzt. Tatsächlich ändern sich beim Aktualisieren des Layouts die Größe der zentralen Zelle und der Seitenzellen. Kein contentOffset mit contentOffset und contentOffset !

 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. Benutzerdefinierte Animationen zum Verschieben und Löschen


Es gibt viele Artikel, in denen beschrieben wird, wie benutzerdefinierte Animationen zum Aktualisieren von Zellen erstellt werden. In unserem Fall helfen sie uns jedoch nicht. In Artikeln und Lernprogrammen wird beschrieben, wie die Attribute einer aktualisierten Zelle überschrieben werden. In unserem Fall verursacht das Ändern des Layouts der gelöschten Zelle Nebenwirkungen - die expanding benachbarten Zelle, die während der Animation tendenziell die Stelle der gelöschten Zelle einnimmt, ändert sich.

Das Aktualisieren von Inhalten in einem UICollectionViewFlowLayout funktioniert wie folgt. Nach dem Löschen / Hinzufügen einer Zelle wird die Methode prepare(forCollectionViewUpdates:) . Sie gibt ein Array mit UICollectionViewUpdateItem , das UICollectionViewUpdateItem , welche Zellen an welchen Indizes aktualisiert / gelöscht / hinzugefügt wurden. Als nächstes ruft das Layout eine Gruppe von Methoden auf

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

und ihre Freunde für Dekoration / ergänzende Ansichten. Wenn die Attribute für die aktualisierten Daten empfangen werden, wird finalizeCollectionViewUpdates aufgerufen. Apple:
Die Sammlungsansicht ruft diese Methode als letzten Schritt auf, bevor Änderungen an der Position animiert werden. Diese Methode wird innerhalb des Animationsblocks aufgerufen, der zum Ausführen aller Einfüge-, Lösch- und Verschiebungsanimationen verwendet wird, sodass Sie bei Bedarf mit dieser Methode zusätzliche Animationen erstellen können. Andernfalls können Sie damit Last-Minute-Aufgaben ausführen, die mit der Verwaltung der Statusinformationen Ihres Layoutobjekts verbunden sind.
Das Problem ist, dass wir Attribute nur für die aktualisierte Zelle spezialisieren können und sie für alle Zellen auf unterschiedliche Weise ändern müssen. Die neue mittlere Zelle sollte aspectRatio ändern, und die seitlichen Zellen sollten transform .


Nachdem untersucht wurde, wie die Standardanimation von Auflistungszellen beim Löschen / Einfügen funktioniert, wurde bekannt, dass die CABasicAnimation in den CABasicAnimation enthalten, das dort geändert werden kann, wenn Sie die Animation für die verbleibenden Zellen anpassen möchten. Es wurde noch schlimmer, als die Protokolle zeigten, dass zwischen performBatchUpdates und prepare(forCollectionViewUpdates:) prepareAttributes(attributes:) prepare(forCollectionViewUpdates:) ein Aufruf erfolgt und möglicherweise bereits die falsche Anzahl von Zellen vorhanden ist, obwohl collectionViewUpdates noch nicht gestartet wurden, ist es sehr schwierig, dies zu verwalten und zu verstehen. Was kann man dagegen tun? Sie können diese eingebauten Animationen deaktivieren!

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

Bewaffnet mit den bereits geschriebenen Animatoren führen wir alle erforderlichen Animationen auf Anforderung zum Löschen durch und starten das dataSource Update am Ende der Animation. Auf diese Weise vereinfachen wir die Animation der Sammlung beim Aktualisieren, da wir selbst steuern, wann sich die Anzahl der Zellen ändert.

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

Wie funktionieren solche Animationen? Speichern Sie in ThumbnailLayout optionale BroschĂĽren, mit denen die Geometrie bestimmter Zellen aktualisiert wird.

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

Mit einem solchen Werkzeug können Sie alles mit der Geometrie der Zellen machen, während der Arbeit des Animators Aktualisierungen ausgeben und diese im Kompliment entfernen. Es besteht auch die Möglichkeit, Updates zu kombinieren.

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

Der Löschanimationscode ist ziemlich umständlich und befindet sich in der Datei DeleteAnimation.swift im Repository. Die Animation der Fokusumschaltung zwischen Zellen wird auf die gleiche Weise implementiert.



6. Der Index der „aktiven“ Zelle geht beim Ändern der Ausrichtung nicht verloren


scrollViewDidScroll(_ scrollView:) wird aufgerufen, auch wenn Sie einfach einen Wert in contentOffset und die Ausrichtung ändern. Wenn der Bildlauf zweier Sammlungen synchronisiert wird, können bei der Aktualisierung des Layouts einige Probleme auftreten. Der folgende Trick hilft: Bei Layout-Aktualisierungen können Sie scrollView.delegate auf 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 } } 

Wenn Sie die Zellengröße zum Zeitpunkt der Änderung der Ausrichtung aktualisieren, sieht dies folgendermaßen aus:

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

Um den gewünschten contentOffset beim Ändern der Ausrichtung nicht zu verlieren, können Sie scrollView.delegate in scrollView.delegate . Wenn Sie die Ausrichtung ändern, wird das Layout deaktiviert, wenn Sie shouldInvalidateLayout(forBoundsChange:) . Wenn Sie bounds ändern bounds Layout aufgefordert, contentOffset zu targetContentOffset(forProposedContentOffset:) . targetContentOffset(forProposedContentOffset:) , müssen Sie targetContentOffset(forProposedContentOffset:) neu targetContentOffset(forProposedContentOffset:) . Apple:
Während Layoutaktualisierungen oder beim Übergang zwischen Layouts ruft die Sammlungsansicht diese Methode auf, um Ihnen die Möglichkeit zu geben, den vorgeschlagenen Inhaltsoffset zu ändern, der am Ende der Animation verwendet wird. Sie können diese Methode überschreiben, wenn die Animationen oder Übergänge dazu führen, dass Elemente auf eine Weise positioniert werden, die für Ihr Design nicht optimal ist.

Die Auflistungsansicht ruft diese Methode auf, nachdem die Methoden prepare() und collectionViewContentSize aufgerufen wurden.


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





Danke fĂĽrs Lesen!

Alle Codes finden Sie unter github.com/YetAnotherRzmn/PhotosApp

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


All Articles