Je travaille sur une fonctionnalité semblable à celle d'un ticker et j'utilise une variable UICollectionView
. C'était à l'origine une scrollView, mais nous pensons qu'une collectionView facilitera l'ajout/la suppression de cellules.
J'anime la collectionVoir comme suit:
- (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];
}
Cela fonctionne bien pour la vue de défilement, et l'animation se passe avec la vue de collection. Cependant, seules les cellules visibles à la fin de l'animation sont réellement rendues. Ajuster le contentOffset ne provoque pas l'appel de cellForItemAtIndexPath
. Comment obtenir le rendu des cellules lorsque le contentOffset change?
EDIT: Pour un peu plus de référence (je ne sais pas si c'est beaucoup d'aide):
- (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];
}
}
Vous devriez simplement ajouter [self.view layoutIfNeeded];
à l'intérieur du bloc d'animation, comme suit:
[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];
Vous pouvez utiliser CADisplayLink pour piloter l’animation vous-même. Ce n'est pas trop difficile à configurer puisque vous utilisez quand même une courbe d'animation linéaire. Voici une implémentation de base qui peut fonctionner pour vous:
@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];
}
Voici une implémentation de Swift, avec des commentaires expliquant pourquoi cela est nécessaire.
L'idée est la même que dans la réponse de devdavid, seule l'approche d'implémentation est différente.
/*
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
}
Si vous devez lancer l'animation before user et faire glisser UICollectionView (par exemple, d'une page à une autre), vous pouvez utiliser cette solution de contournement pour précharger les cellules latérales:
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()
}
}
}
Sans cette solution de contournement, UICollectionView supprimera les cellules, qui ne croisent pas les limites de la cible, avant le début de l'animation.
P.S. Cela ne fonctionne que si vous avez besoin d’animer la page suivante ou précédente .
Je soupçonne que UICollectionView
essaie d'améliorer les performances en attendant la fin du défilement avant la mise à jour.
Peut-être pourriez-vous diviser l’animation en mandrins, bien que je ne sois pas sûr que ce soit fluide.
Ou peut-être appeler périodiquement setNeedsDisplay pendant le défilement?
Sinon, peut-être que ce remplacement de UICollectionView voudra soit que vous en ayez besoin, soit qu'il puisse être modifié pour le faire: