Laut Apples Dokumentation (und auf der WWDC 2012 angepriesen) ist es möglich, das Layout UICollectionView
dynamisch einzustellen und die Änderungen sogar zu animieren:
Normalerweise geben Sie beim Erstellen einer Sammlungsansicht ein Layoutobjekt an, aber Sie können das Layout einer Sammlungsansicht auch dynamisch ändern. Das Layoutobjekt wird in der Eigenschaft collectionViewLayout gespeichert. Durch das Festlegen dieser Eigenschaft wird das Layout sofort direkt aktualisiert, ohne die Änderungen zu animieren. Wenn Sie die Änderungen animieren möchten, müssen Sie stattdessen die Methode setCollectionViewLayout: animiert: aufrufen.
In der Praxis habe ich jedoch festgestellt, UICollectionView
dass dies unerklärliche und sogar ungültige Änderungen an den contentOffset
Zellen bewirkt , die dazu führen , dass sich Zellen falsch bewegen, wodurch die Funktion praktisch unbrauchbar wird. Um das Problem zu veranschaulichen, habe ich den folgenden Beispielcode zusammengestellt, der an einen Standard-Controller für die Sammlungsansicht angehängt werden kann, der in ein Storyboard eingefügt wurde:
#import <UIKit/UIKit.h>
@interface MyCollectionViewController : UICollectionViewController
@end
@implementation MyCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
[self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}
@end
Die Steuerung setzt einen Standard UICollectionViewFlowLayout
in viewDidLoad
und zeigt eine einzelne Zelle auf dem Bildschirm. Wenn die Zellen ausgewählt sind, erstellt der Controller einen weiteren Standard UICollectionViewFlowLayout
und setzt ihn in der Sammlungsansicht mit dem animated:YES
Flag. Das erwartete Verhalten ist, dass sich die Zelle nicht bewegt. Das tatsächliche Verhalten ist jedoch, dass die Zelle außerhalb des Bildschirms gescrollt wird. Zu diesem Zeitpunkt ist es nicht einmal möglich, die Zelle wieder auf dem Bildschirm zu scrollen.
Ein Blick auf das Konsolenprotokoll zeigt, dass sich das contentOffset unerklärlicherweise geändert hat (in meinem Projekt von (0, 0) auf (0, 205)). Ich habe eine Lösung für die Lösung für den nicht animierten Fall veröffentlicht ( i.e. animated:NO
) veröffentlicht, aber da ich eine Animation benötige, bin ich sehr interessiert zu wissen, ob jemand eine Lösung oder Problemumgehung für den animierten Fall hat.
Als Randnotiz habe ich benutzerdefinierte Layouts getestet und das gleiche Verhalten erhalten.
quelle
Antworten:
Ich habe mir seit Tagen die Haare ausgezogen und eine Lösung für meine Situation gefunden, die helfen könnte. In meinem Fall habe ich ein kollabierendes Fotolayout wie in der Foto-App auf dem iPad. Es werden Alben mit den Fotos übereinander angezeigt. Wenn Sie auf ein Album tippen, werden die Fotos erweitert. Ich habe also zwei separate UICollectionViewLayouts und wechsle zwischen ihnen um.
[self.collectionView setCollectionViewLayout:myLayout animated:YES]
Ich hatte Ihr genaues Problem mit den vor der Animation springenden Zellen und erkannte, dass es das warcontentOffset
. Ich habe alles mit dem versuchtcontentOffset
aber es sprang immer noch während der Animation. Die oben genannte Lösung von Tyler hat funktioniert, aber die Animation war immer noch durcheinander.Dann bemerkte ich, dass es nur passiert, wenn ein paar Alben auf dem Bildschirm waren, nicht genug, um den Bildschirm auszufüllen. Mein Layout überschreibt
-(CGSize)collectionViewContentSize
wie empfohlen . Wenn nur wenige Alben vorhanden sind, ist die Inhaltsgröße der Sammlungsansicht geringer als die Inhaltsgröße der Ansichten. Das verursacht den Sprung, wenn ich zwischen den Sammlungslayouts umschalte.Also habe ich in meinen Layouts eine Eigenschaft namens minHeight festgelegt und sie auf die Höhe der übergeordneten Sammlungsansichten festgelegt. Dann überprüfe ich die Höhe, bevor ich zurückkomme.
-(CGSize)collectionViewContentSize
Ich stelle sicher, dass die Höhe> = die Mindesthöhe ist.Keine echte Lösung, aber jetzt funktioniert es einwandfrei. Ich würde versuchen, das einzustellen
contentSize
Ansicht Ihrer Sammlung auf mindestens die Länge der enthaltenen Ansicht festzulegen.Bearbeiten: Manicaesar hat eine einfache Problemumgehung hinzugefügt, wenn Sie von UICollectionViewFlowLayout erben:
-(CGSize)collectionViewContentSize { //Workaround CGSize superSize = [super collectionViewContentSize]; CGRect frame = self.collectionView.frame; return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame))); }
quelle
UICollectionViewLayout
enthält die überschreibbare MethodetargetContentOffsetForProposedContentOffset:
, mit der Sie bei einer Änderung des Layouts den richtigen Inhaltsversatz bereitstellen können. Dadurch wird die Animation korrekt animiert. Dies ist in iOS 7.0 und höher verfügbarquelle
contentOffset
), ist dies die perfekte Lösung. Danke @ cdemiris99.override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint) -> CGPoint { if let collectionView = self.collectionView { let currentContentOffset = collectionView.contentOffset if currentContentOffset.y < proposedContentOffset.y { return currentContentOffset } } return proposedContentOffset }
Das Problem wurde behoben. Danke für einen Hinweis.Dieses Problem hat mich auch gebissen und es scheint ein Fehler im Übergangscode zu sein. Soweit ich das beurteilen kann, wird versucht, sich auf die Zelle zu konzentrieren, die der Mitte des Layouts der Ansicht vor dem Übergang am nächsten liegt. Wenn sich jedoch vor dem Übergang keine Zelle in der Mitte der Ansicht befindet, wird immer noch versucht, die Stelle zu zentrieren, an der sich die Zelle nach dem Übergang befinden würde. Dies ist sehr deutlich, wenn Sie alwaysBounceVertical / Horizontal auf YES setzen, die Ansicht mit einer einzelnen Zelle laden und dann einen Layoutübergang durchführen.
Ich konnte dies umgehen, indem ich der Sammlung explizit anwies, sich nach dem Auslösen der Layoutaktualisierung auf eine bestimmte Zelle (in diesem Beispiel die erste sichtbare Zelle der Zelle) zu konzentrieren.
[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES]; // scroll to the first visible cell if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) { NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0]; [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES]; }
quelle
numberOfItemsInSection:
um 100 zurückzugeben. Wenn Sie auf die erste Zelle tippen, wird die Ansicht um mehrere Zeilen nach unten verschoben. Wenn Sie dann auf die erste sichtbare Zelle tippen, wird die Ansicht nach oben verschoben. Wenn Sie dann erneut auf die erste Zelle tippen, bleibt diese stationär (korrektes Verhalten). Wenn Sie auf eine Zelle in der unteren Reihe tippen, wird sie abprallt. Scrollen Sie in der Ansicht um mehrere Zeilen nach unten, tippen Sie auf eine beliebige Zeile, und die Ansicht wird nach oben verschoben.Ich springe mit einer späten Antwort auf meine eigene Frage ein.
Die TLLayoutTransitioning- Bibliothek bietet eine hervorragende Lösung für dieses Problem, indem die interaktiven Übergangs-APIs von iOS7 erneut beauftragt werden, nicht interaktive Übergänge von Layout zu Layout durchzuführen. Es bietet effektiv eine Alternative zum
setCollectionViewLayout
Lösen des Problems des Inhaltsversatzes und zum Hinzufügen mehrerer Funktionen:Benutzerdefinierte Beschleunigungskurven können definiert werden als
AHEasingFunction
Funktionen werden. Der endgültige Inhaltsversatz kann in Form eines oder mehrerer Indexpfade mit den Platzierungsoptionen Minimal, Mitte, Oben, Links, Unten oder Rechts angegeben werden.Versuchen Sie, die Resize-Demo auszuführen , um zu sehen, was ich meine im Arbeitsbereich "Beispiele" und mit den Optionen .
Die Verwendung ist wie folgt. Konfigurieren Sie zunächst Ihren View Controller so, dass eine Instanz von
TLTransitionLayout
:- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout { return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout]; }
Anstatt aufzurufen
setCollectionViewLayout
, rufen Sie danntransitionToCollectionViewLayout:toLayout
den in derUICollectionView-TLLayoutTransitioning
Kategorie definierten Aufruf auf :UICollectionViewLayout *toLayout = ...; // the layout to transition to CGFloat duration = 2.0; AHEasingFunction easing = QuarticEaseInOut; TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];
Dieser Aufruf initiiert einen interaktiven Übergang und intern einen
CADisplayLink
Rückruf, der den Übergangsfortschritt mit der angegebenen Dauer und Beschleunigungsfunktion steuert.Der nächste Schritt besteht darin, einen endgültigen Inhaltsversatz anzugeben. Sie können einen beliebigen Wert angeben, aber die in
toContentOffsetForLayout
definierte MethodeUICollectionView-TLLayoutTransitioning
bietet eine elegante Möglichkeit, Inhaltsversätze relativ zu einem oder mehreren Indexpfaden zu berechnen. Um beispielsweise eine bestimmte Zelle so nah wie möglich an der Mitte der Sammlungsansicht zu haben, rufen Sie unmittelbar danach Folgendes auftransitionToCollectionViewLayout
:NSIndexPath *indexPath = ...; // the index path of the cell to center TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter; CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement]; layout.toContentOffset = toOffset;
quelle
2019 tatsächliche Lösung
Angenommen, Sie haben eine Reihe von Layouts für Ihre Ansicht "Autos".
Nehmen wir an, Sie haben drei.
CarsLayout1: UICollectionViewLayout { ... CarsLayout2: UICollectionViewLayout { ... CarsLayout3: UICollectionViewLayout { ...
Es wird springen wenn Sie zwischen Layouts animieren.
Es ist nur ein unbestreitbarer Fehler von Apple. Es springt, wenn Sie animieren, ohne Frage.
Das Update ist das Folgende:
Sie müssen über einen globalen Float und die folgende Basisklasse verfügen:
var avoidAppleMessupCarsLayouts: CGPoint? = nil class FixerForCarsLayouts: UICollectionViewLayout { override func prepareForTransition(from oldLayout: UICollectionViewLayout) { avoidAppleMessupCarsLayouts = collectionView?.contentOffset } override func targetContentOffset( forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { if avoidAppleMessupCarsLayouts != nil { return avoidAppleMessupCarsLayouts! } return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) } }
Hier sind die drei Layouts für Ihren Bildschirm "Autos":
CarsLayout1: FixerForCarsLayouts { ... CarsLayout2: FixerForCarsLayouts { ... CarsLayout3: FixerForCarsLayouts { ...
Das ist es. Es funktioniert jetzt.
Unglaublich dunkel, Sie könnten verschiedene "Sätze" von Layouts (für Autos, Hunde, Häuser usw.) haben, die (möglicherweise) kollidieren könnten. Aus diesem Grund haben Sie für jede "Menge" eine globale und eine Basisklasse wie oben.
Dies wurde vor vielen Jahren von dem vorbeigehenden Benutzer @Isaacliu erfunden.
Ein Detail, FWIW in Isaaclius Codefragment,
finalizeLayoutTransition
wird hinzugefügt. Tatsächlich ist es logisch nicht notwendig.Tatsache ist, dass Sie dies jedes Mal tun müssen, bis Apple die Funktionsweise ändert, wenn Sie zwischen den Layouts der Sammlungsansicht animieren. So ist das Leben!
quelle
Einfach.
Animieren Sie Ihr neues Layout und das contentOffset von collectionView im selben Animationsblock.
[UIView animateWithDuration:0.3 animations:^{ [self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil]; [self.collectionView setContentOffset:CGPointMake(0, -64)]; } completion:nil];
Es wird
self.collectionView.contentOffset
konstant bleiben .quelle
Wenn Sie lediglich nach einem Inhaltsversatz suchen, der sich beim Übergang von Layouts nicht ändert, können Sie ein benutzerdefiniertes Layout erstellen und einige Methoden überschreiben, um den Überblick über das alte contentOffset zu behalten und es wiederzuverwenden:
@interface CustomLayout () @property (nonatomic) NSValue *previousContentOffset; @end @implementation CustomLayout - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset { CGPoint previousContentOffset = [self.previousContentOffset CGPointValue]; CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ; } - (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout { self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset]; return [super prepareForTransitionFromLayout:oldLayout]; } - (void)finalizeLayoutTransition { self.previousContentOffset = nil; return [super finalizeLayoutTransition]; } @end
Dazu wird lediglich der vorherige Inhaltsversatz vor dem Layoutübergang gespeichert
prepareForTransitionFromLayout
, der neue Inhaltsversatz überschriebentargetContentOffsetForProposedContentOffset
und gelöschtfinalizeLayoutTransition
. Ziemlich einfachquelle
targetContentOffset#forProposedContentOffset#withScrollingVelocity
!Wenn es hilft, die Erfahrung zu erweitern: Ich bin auf dieses Problem immer wieder gestoßen, unabhängig von der Größe meines Inhalts, unabhängig davon, ob ich einen Inhaltseinsatz oder einen anderen offensichtlichen Faktor festgelegt habe. Meine Problemumgehung war also etwas drastisch. Zuerst habe ich UICollectionView unterklassifiziert und hinzugefügt, um unangemessene Einstellungen für den Inhaltsversatz zu bekämpfen:
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setContentOffset:(CGPoint)contentOffset { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; } - (void)setContentSize:(CGSize)contentSize { _declineContentOffset ++; [super setContentSize:contentSize]; _declineContentOffset --; }
Ich bin nicht stolz darauf, aber die einzige praktikable Lösung scheint darin zu bestehen, jeden Versuch der Sammlungsansicht, einen eigenen Inhaltsoffset festzulegen, der sich aus einem Aufruf von ergibt, vollständig abzulehnen
setCollectionViewLayout:animated:
. Empirisch sieht es so aus, als ob diese Änderung direkt im unmittelbaren Aufruf erfolgt, was offensichtlich nicht durch die Benutzeroberfläche oder die Dokumentation garantiert wird, aber aus Sicht der Kernanimation sinnvoll ist, sodass ich mit der Annahme vielleicht nur zu 50% unzufrieden bin.Es gab jedoch ein zweites Problem: UICollectionView fügte jetzt einen kleinen Sprung zu den Ansichten hinzu, die sich in einem neuen Layout der Sammlungsansicht an derselben Stelle befanden - indem sie um etwa 240 Punkte nach unten gedrückt und dann wieder an die ursprüngliche Position animiert wurden. Ich bin mir nicht
CAAnimation
sicher , warum, aber ich habe meinen Code geändert, um damit umzugehen, indem ich das s abgetrennt habe, das zu Zellen hinzugefügt wurde, die sich tatsächlich nicht bewegten:- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { // collect up the positions of all existing subviews NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary]; for(UIView *view in [self subviews]) { positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]]; } // apply the new layout, declining to allow the content offset to change _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; // run through the subviews again... for(UIView *view in [self subviews]) { // if UIKit has inexplicably applied animations to these views to move them back to where // they were in the first place, remove those animations CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"]; NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]]; if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue) { NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]]; if([targetValue isEqualToValue:sourceValue]) [[view layer] removeAnimationForKey:@"position"]; } } }
Dies scheint Ansichten, die sich tatsächlich bewegen, nicht zu behindern oder dazu zu führen, dass sie sich falsch bewegen (als ob sie erwarten würden, dass alles um sie herum um etwa 240 Punkte nach unten zeigt und mit ihnen an die richtige Position animiert wird).
Das ist also meine aktuelle Lösung.
quelle
Ich habe jetzt wahrscheinlich zwei Wochen damit verbracht, verschiedene Layouts so zu gestalten, dass sie reibungslos miteinander übergehen. Ich habe festgestellt, dass das Überschreiben des vorgeschlagenen Offsets in iOS 10.2 funktioniert, aber in der vorherigen Version tritt immer noch das Problem auf. Was meine Situation ein bisschen verschlimmert, ist, dass ich aufgrund eines Bildlaufs in ein anderes Layout wechseln muss, sodass die Ansicht gleichzeitig gescrollt und gewechselt wird.
Tommys Antwort war das einzige, was in Versionen vor 10.2 für mich funktioniert hat. Ich mache jetzt folgendes.
class HackedCollectionView: UICollectionView { var ignoreContentOffsetChanges = false override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setContentOffset(contentOffset, animated: animated) } override var contentOffset: CGPoint { get { return super.contentOffset } set { guard ignoreContentOffsetChanges == false else { return } super.contentOffset = newValue } } override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setCollectionViewLayout(layout, animated: animated) } override var contentSize: CGSize { get { return super.contentSize } set { guard ignoreContentOffsetChanges == false else { return } super.contentSize = newValue } } }
Wenn ich dann das Layout einstelle, mache ich das ...
let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100) UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in // I'm also doing something in my layout, but this may be redundant now layout.overriddenContentOffset = nil }) collectionView.ignoreContentOffsetChanges = true }, completion: { _ in collectionView.ignoreContentOffsetChanges = false collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false) })
quelle
targetContentOffset#forProposedContentOffset#withScrollingVelocity
Das hat endlich bei mir funktioniert (Swift 3)
self.collectionView.collectionViewLayout = UICollectionViewFlowLayout() self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)
quelle
y: -118
magische Zahl? Diese Lösung scheint für eine bestimmte Situation zu sein und ist daher nicht allzu hilfreich.