it-swarm.com.de

Das dynamische Festlegen des Layouts für UICollectionView führt zu einer unerklärlichen Änderung von contentOffset

Laut der Dokumentation von Apple (und auf der WWDC 2012 angekündigt) ist es möglich, das Layout dynamisch auf UICollectionView zu setzen und die Änderungen sogar zu animieren:

Normalerweise geben Sie ein Layoutobjekt an, wenn Sie eine Sammlungsansicht erstellen, Sie können das Layout einer Sammlungsansicht jedoch auch dynamisch ändern. Das Layoutobjekt wird in der collectionViewLayout-Eigenschaft gespeichert. Durch das Festlegen dieser Eigenschaft wird das Layout direkt aktualisiert, ohne die Änderungen zu animieren. Wenn Sie die Änderungen animieren möchten, müssen Sie stattdessen die Methode setCollectionViewLayout: animated: aufrufen.

In der Praxis habe ich jedoch festgestellt, dass UICollectionView unerklärliche und sogar ungültige Änderungen an contentOffset vornimmt, was dazu führt, dass sich Zellen falsch verschieben, wodurch das Feature praktisch unbrauchbar wird. Um das Problem zu veranschaulichen, habe ich den folgenden Beispielcode zusammengestellt, der an einen standardmäßigen Auflistungsansicht-Controller angefügt werden kann, der in einem Storyboard abgelegt 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

Der Controller legt in UICollectionViewFlowLayout einen Standardwert viewDidLoad fest und zeigt eine einzelne Zelle auf dem Bildschirm an. Wenn die Zellen ausgewählt sind, erstellt der Controller einen anderen Standardwert UICollectionViewFlowLayout und setzt ihn in der Sammlungsansicht mit dem Flag animated:YES. Das erwartete Verhalten ist, dass sich die Zelle nicht bewegt. Das tatsächliche Verhalten ist jedoch, dass die Zelle außerhalb des Bildschirms scrollt. Zu diesem Zeitpunkt ist es nicht einmal möglich, die Zelle auf dem Bildschirm zurückzurollen.

Ein Blick auf das Konsolenprotokoll zeigt, dass das contentOffset unerklärlicherweise geändert wurde (in meinem Projekt von (0, 0) zu (0, 205)). Ich habe eine Lösung für die Lösung für den nicht animierten Fall (i.e. animated:NO) veröffentlicht. Da ich jedoch eine Animation benötige, bin ich sehr interessiert, ob jemand eine Lösung oder einen Workaround für den animierten Fall hat.

Nebenbei habe ich benutzerdefinierte Layouts getestet und das gleiche Verhalten erhalten.

66
Timothy Moose

Ich habe mir schon seit Tagen die Haare ausgezogen und habe eine Lösung für meine Situation gefunden, die helfen könnte ... In meinem Fall habe ich ein zusammenfallendes Fotolayout wie in der Foto-App auf dem iPad. Es zeigt Alben mit den Fotos übereinander und wenn Sie auf ein Album tippen, werden die Fotos vergrößert. Was ich also habe, sind zwei separate UICollectionViewLayouts, die mit [self.collectionView setCollectionViewLayout:myLayout animated:YES] zwischen ihnen umschalten. Ich hatte genau Ihr Problem mit den Zellen, die vor der Animation springen, und erkannte, dass es die contentOffset war. Ich habe alles mit der contentOffset ausprobiert, aber es ist während der Animation immer noch gesprungen. Die oben genannte Lösung von tyler funktionierte, aber sie machte immer noch Probleme mit der Animation.

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 Kollektionslayouts umschalte.

Also habe ich in meinen Layouts eine Eigenschaft mit dem Namen minHeight festgelegt und auf die Höhe der Sammlungsansichten des übergeordneten Elements gesetzt. Dann überprüfe ich die Höhe, bevor ich in -(CGSize)collectionViewContentSize zurückkomme. Ich stelle sicher, dass die Höhe> = minimale Höhe ist.

Keine echte Lösung, aber jetzt funktioniert es gut. Ich würde versuchen, die contentSize Ihrer Sammlungsansicht so einzustellen, dass sie mindestens so lang ist wie sie.

edit: Manicaesar fügte eine einfache Problemumgehung hinzu, 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)));
}
28
tassinari

UICollectionViewLayout enthält die überschreibbare Methode targetContentOffsetForProposedContentOffset: , mit der Sie während einer Layoutänderung den korrekten Inhalt-Offset angeben können. Dies wird richtig animiert. Dies ist in iOS 7.0 und höher verfügbar

31
cdemiris99

Ich springe mit einer späten Antwort auf meine eigene Frage.

Die Bibliothek " TLLayoutTransitioning " bietet eine großartige Lösung für dieses Problem, indem die interaktiven APIs von iOS7 für nicht interaktive Übergänge von Layout zu Layout erneut ausgeführt werden. Es bietet effektiv eine Alternative zu setCollectionViewLayout, löst das Problem des Inhaltsversatzes und fügt mehrere Funktionen hinzu:

  1. Animationsdauer
  2. 30+ Beschleunigungskurven (mit freundlicher Genehmigung von Warren Moore AHEasing library )
  3. Mehrere Versatzmodi für Inhalte

Benutzerdefinierte Beschleunigungskurven können als AHEasingFunction-Funktionen definiert werden. Der endgültige Versatz des Inhalts kann in Bezug auf einen oder mehrere Indexpfade mit den Platzierungsoptionen Minimal, Mitte, Oben, Links, Unten oder Rechts angegeben werden.

Um zu verstehen, was ich meine, versuchen Sie, die Resize Demo im Arbeitsbereich Beispiele auszuführen und mit den Optionen herumzuspielen.

Die Verwendung ist so. Konfigurieren Sie zuerst Ihren View Controller, um eine Instanz von TLTransitionLayout zurückzugeben:

- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
    return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}

Rufen Sie statt setCollectionViewLayout den in der Kategorie transitionToCollectionViewLayout:toLayout definierten UICollectionView-TLLayoutTransitioning 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 Fortschritt des Übergangs mit der angegebenen Dauer und Beschleunigungsfunktion steuert.

Der nächste Schritt besteht darin, einen endgültigen Inhaltsoffset anzugeben. Sie können einen beliebigen Wert angeben. Die in UICollectionView-TLLayoutTransitioning definierte toContentOffsetForLayout-Methode bietet jedoch eine elegante Methode zur Berechnung von Inhaltsversätzen relativ zu einem oder mehreren Indexpfaden. Um beispielsweise eine bestimmte Zelle so nahe wie möglich in der Mitte der Sammlungsansicht zu haben, führen Sie den folgenden Aufruf unmittelbar nach transitionToCollectionViewLayout aus:

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;
7
Timothy Moose

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 war. Befindet sich jedoch nicht zufällig eine Zelle in der Mitte der Ansicht vor dem Übergang, versucht sie immer noch, die Stelle zu zentrieren, an der sich die Zelle nach dem Übergang befinden würde. Dies ist sehr klar, 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 sagte, dass sie sich auf eine bestimmte Zelle (in diesem Beispiel die erste sichtbare Zelle) konzentrieren soll, nachdem das Layout-Update ausgelöst wurde. 

[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];
}
7
tyler

Einfach.

Animieren Sie das neue ContentOffset Ihres neuen Layouts und Ihrer Sammlung 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];

self.collectionView.contentOffset bleibt konstant.

5
nikolsky

Swift 3.

UICollectionViewLayout Unterklasse. Der Weg cdemiris99 schlug vor:

override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
    return collectionView?.contentOffset ?? CGPoint.zero
}

Ich habe es selbst getestet mit meinem eigenen layout

3
iWheelBuy

Wenn Sie lediglich nach einem Versatz des Inhalts suchen, der beim Übergang von Layouts nicht geändert wird, können Sie ein benutzerdefiniertes Layout erstellen und einige Methoden überschreiben, um das alte contentOffset zu verfolgen und es erneut zu verwenden:

@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

Das Speichern des vorherigen Inhaltsversatzes vor dem Layoutübergang in prepareForTransitionFromLayout, das Überschreiben des neuen Inhaltsversatzes in targetContentOffsetForProposedContentOffset und das Löschen in finalizeLayoutTransition. Ziemlich einfach

2
Isaac liu

Wenn es hilft, den Erfahrungsschatz zu vergrößern: Ich habe dieses Problem hartnäckig angegangen, unabhängig von der Größe meines Inhalts, unabhängig davon, ob ich einen Inhaltseinsatz oder einen anderen offensichtlichen Faktor festgelegt hatte. Mein Workaround war also etwas drastisch. Zuerst habe ich UICollectionView subclassed und hinzugefügt, um unangemessene Einstellungen für das Versetzen von Inhalten zu verhindern:

- (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 es zu sein, jeden Versuch der Sammlungsansicht, seinen eigenen Inhalt-Offset zu setzen, der aus einem Aufruf von setCollectionViewLayout:animated: resultiert, vollständig abzulehnen. Empirisch sieht es so aus, als ob diese Änderung direkt im unmittelbaren Aufruf auftritt, was offensichtlich nicht durch die Benutzeroberfläche oder die Dokumentation garantiert wird, aber aus Sicht der Core-Animation sinnvoll ist. Daher ist mir die Annahme vielleicht nur zu 50% unangenehm.

Es gab jedoch ein zweites Problem: UICollectionView fügte jetzt den Ansichten, die sich an einem neuen Layout für die Sammlungsansicht befanden, einen kleinen Sprung hinzu, drückte sie um 240 Punkte nach unten und animierte sie wieder an die ursprüngliche Position. Ich weiß nicht warum, aber ich änderte meinen Code, um damit umzugehen, indem ich die CAAnimations löschte, die zu Zellen hinzugefügt wurden, die sich eigentlich 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 die Sicht nicht zu behindern, die sich tatsächlich bewegt, oder sie veranlasst, dass sie sich nicht korrekt bewegen (als hätten sie erwartet, dass alles um sie herum um 240 Punkte nach unten ist und sich mit ihnen an die richtige Position bewegt).

Das ist also meine aktuelle Lösung.

1
Tommy

Ich habe wahrscheinlich seit ungefähr zwei Wochen versucht, verschiedene Layouts für einen reibungslosen Übergang zwischen den beiden zu schaffen. Ich habe festgestellt, dass das Überschreiben des vorgeschlagenen Versatzes in iOS 10.2 funktioniert, aber in der vorherigen Version habe ich immer noch das Problem. Die Sache, die meine Situation etwas verschlechtert, ist, dass ich durch ein Scrollen in ein anderes Layout wechseln muss, sodass die Ansicht gleichzeitig scrollt und wechselt. 

Tommys Antwort war das einzige, was für mich in Versionen vor 10.2 funktionierte. 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 setze, 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)
})
0
Mark Bridges