ios animation uicollectionview contentoffset

ios - La animación de UICollectionView contentOffset no muestra celdas no visibles



animation (6)

Estoy trabajando en alguna funcionalidad similar a un ticker y estoy usando un UICollectionView . Originalmente era un scrollView, pero creemos que un collectionView hará que sea más fácil agregar / eliminar celdas.

Estoy animando el collectionView con lo siguiente:

- (void)beginAnimation { [UIView animateWithDuration:((self.collectionView.collectionViewLayout.collectionViewContentSize.width - self.collectionView.contentOffset.x) / 75) delay:0 options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState) animations:^{ self.collectionView.contentOffset = CGPointMake(self.collectionView.collectionViewLayout.collectionViewContentSize.width, 0); } completion:nil]; }

Esto funciona bien para la vista de desplazamiento, y la animación ocurre con la vista de colección. Sin embargo, solo se representan las celdas que son visibles al final de la animación. El ajuste de contentOffset no hace que se cellForItemAtIndexPath a cellForItemAtIndexPath . ¿Cómo puedo hacer que las celdas se procesen cuando el contentOffset cambia?

EDITAR: Para un poco más de referencia (no estoy seguro de si es de mucha ayuda):

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { TickerElementCell *cell = (TickerElementCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"TickerElementCell" forIndexPath:indexPath]; cell.ticker = [self.fetchedResultsController objectAtIndexPath:indexPath]; return cell; } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { // ... [self loadTicker]; } - (void)loadTicker { // ... if (self.animating) { [self updateAnimation]; } else { [self beginAnimation]; } } - (void)beginAnimation { if (self.animating) { [self endAnimation]; } if ([self.tickerElements count] && !self.animating && !self.paused) { self.animating = YES; self.collectionView.contentOffset = CGPointMake(1, 0); [UIView animateWithDuration:((self.collectionView.collectionViewLayout.collectionViewContentSize.width - self.collectionView.contentOffset.x) / 75) delay:0 options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState) animations:^{ self.collectionView.contentOffset = CGPointMake(self.collectionView.collectionViewLayout.collectionViewContentSize.width, 0); } completion:nil]; } }


Aquí hay una implementación rápida, con comentarios que explican por qué se necesita esto.

La idea es la misma que en la respuesta de devdavid, solo el enfoque de implementación es diferente.

/* Animated use of `scrollToContentOffset:animated:` doesn''t give enough control over the animation duration and curve. Non-animated use of `scrollToContentOffset:animated:` (or contentOffset directly) embedded in an animation block gives more control but interfer with the internal logic of UICollectionView. For example, cells that are not visible for the target contentOffset are removed at the beginning of the animation because from the collection view point of view, the change is not animated and the cells can safely be removed. To fix that, we must control the scroll ourselves. We use CADisplayLink to update the scroll offset step-by-step and render cells if needed alongside. To simplify, we force a linear animation curve, but this can be adapted if needed. */ private var currentScrollDisplayLink: CADisplayLink? private var currentScrollStartTime = Date() private var currentScrollDuration: TimeInterval = 0 private var currentScrollStartContentOffset: CGFloat = 0.0 private var currentScrollEndContentOffset: CGFloat = 0.0 // The curve is hardcoded to linear for simplicity private func beginAnimatedScroll(toContentOffset contentOffset: CGPoint, animationDuration: TimeInterval) { // Cancel previous scroll if needed resetCurrentAnimatedScroll() // Prevent non-animated scroll guard animationDuration != 0 else { logAssertFail("Animation controlled scroll must not be used for non-animated changes") collectionView?.setContentOffset(contentOffset, animated: false) return } // Setup new scroll properties currentScrollStartTime = Date() currentScrollDuration = animationDuration currentScrollStartContentOffset = collectionView?.contentOffset.y ?? 0.0 currentScrollEndContentOffset = contentOffset.y // Start new scroll currentScrollDisplayLink = CADisplayLink(target: self, selector: #selector(handleScrollDisplayLinkTick)) currentScrollDisplayLink?.add(to: RunLoop.current, forMode: .commonModes) } @objc private func handleScrollDisplayLinkTick() { let animationRatio = CGFloat(abs(currentScrollStartTime.timeIntervalSinceNow) / currentScrollDuration) // Animation is finished guard animationRatio < 1 else { endAnimatedScroll() return } // Animation running, update with incremental content offset let deltaContentOffset = animationRatio * (currentScrollEndContentOffset - currentScrollStartContentOffset) let newContentOffset = CGPoint(x: 0.0, y: currentScrollStartContentOffset + deltaContentOffset) collectionView?.setContentOffset(newContentOffset, animated: false) } private func endAnimatedScroll() { let newContentOffset = CGPoint(x: 0.0, y: currentScrollEndContentOffset) collectionView?.setContentOffset(newContentOffset, animated: false) resetCurrentAnimatedScroll() } private func resetCurrentAnimatedScroll() { currentScrollDisplayLink?.invalidate() currentScrollDisplayLink = nil }


Puedes intentar usar un CADisplayLink para conducir la animación tú mismo. Esto no es demasiado difícil de configurar, ya que de todos modos está utilizando una curva de animación lineal. Aquí hay una implementación básica que puede funcionar para usted:

@property (nonatomic, strong) CADisplayLink *displayLink; @property (nonatomic, assign) CFTimeInterval lastTimerTick; @property (nonatomic, assign) CGFloat animationPointsPerSecond; @property (nonatomic, assign) CGPoint finalContentOffset; -(void)beginAnimation { self.lastTimerTick = 0; self.animationPointsPerSecond = 50; self.finalContentOffset = CGPointMake(..., ...); self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)]; [self.displayLink setFrameInterval:1]; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } -(void)endAnimation { [self.displayLink invalidate]; self.displayLink = nil; } -(void)displayLinkTick { if (self.lastTimerTick = 0) { self.lastTimerTick = self.displayLink.timestamp; return; } CFTimeInterval currentTimestamp = self.displayLink.timestamp; CGPoint newContentOffset = self.collectionView.contentOffset; newContentOffset.x += self.animationPointsPerSecond * (currentTimestamp - self.lastTimerTick) self.collectionView.contentOffset = newContentOffset; self.lastTimerTick = currentTimestamp; if (newContentOffset.x >= self.finalContentOffset.x) [self endAnimation]; }


Si necesita iniciar la animación antes de que el usuario comience a arrastrar UICollectionView (por ejemplo, de una página a otra), puede usar esta solución para precargar las celdas laterales:

func scroll(to index: Int, progress: CGFloat = 0) { let isInsideAnimation = UIView.inheritedAnimationDuration > 0 if isInsideAnimation { // workaround // preload left & right cells // without this, some cells will be immediately removed before animation starts preloadSideCells() } collectionView.contentOffset.x = (CGFloat(index) + progress) * collectionView.bounds.width if isInsideAnimation { // workaround // sometimes invisible cells not removed (because of side cells preloading) // without this, some invisible cells will persists on superview after animation ends removeInvisibleCells() UIView.performWithoutAnimation { self.collectionView.layoutIfNeeded() } } } private func preloadSideCells() { collectionView.contentOffset.x -= 0.5 collectionView.layoutIfNeeded() collectionView.contentOffset.x += 1 collectionView.layoutIfNeeded() } private func removeInvisibleCells() { let visibleCells = collectionView.visibleCells let visibleRect = CGRect( x: max(0, collectionView.contentOffset.x - collectionView.bounds.width), y: collectionView.contentOffset.y, width: collectionView.bounds.width * 3, height: collectionView.bounds.height ) for cell in visibleCells { if !visibleRect.intersects(cell.frame) { cell.removeFromSuperview() } } }

Sin esta solución, UICollectionView eliminará las celdas, que no intersectan los límites del objetivo, antes de que comience la animación.

PS Esto funciona solo si necesitas animar a la página siguiente o anterior .


Simplemente debe agregar [self.view layoutIfNeeded]; Dentro del bloque de animación, así:

[UIView animateWithDuration:((self.collectionView.collectionViewLayout.collectionViewContentSize.width - self.collectionView.contentOffset.x) / 75) delay:0 options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState) animations:^{ self.collectionView.contentOffset = CGPointMake(self.collectionView.collectionViewLayout.collectionViewContentSize.width, 0); [self.view layoutIfNeeded]; } completion:nil];


Sospecho que UICollectionView está intentando mejorar el rendimiento esperando hasta el final del desplazamiento antes de actualizar.

Tal vez podría dividir la animación en mandriles, aunque no estoy seguro de qué tan suave sería.

¿O tal vez llamar a setNeedsDisplay periódicamente durante el desplazamiento?

Alternativamente, tal vez este reemplazo para UICollectionView querrá o necesita ser modificado para hacerlo:

https://github.com/steipete/PSTCollectionView


Utilice :scrollToItemAtIndexPath lugar:

[UIView animateWithDuration:duration animations:^{ [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO]; }];