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.
- Parallaxeffekt in einer groĂźen Sammlung
- Elemente einer kleinen Sammlung sind zentriert.
- Dynamische Größe von Artikeln in einer kleinen Sammlung
- Die Logik zum Platzieren der Elemente einer kleinen Zelle hängt nicht nur vom contentOffset ab, sondern auch von Benutzerinteraktionen
- Benutzerdefinierte Animationen zum Verschieben und Löschen
- 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 } }



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

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)
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] = [:]
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