私はいくつかのティッカーのような機能に取り組んでおり、UICollectionView
を使用しています。元々はscrollViewでしたが、collectionViewを使用するとセルの追加/削除が簡単になると考えています。
私は次のようにcollectionViewをアニメーション化しています:
- (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];
}
これはスクロールビューでは正常に機能し、アニメーションはコレクションビューで発生します。ただし、実際には、アニメーションの最後に表示されているセルのみがレンダリングされます。 contentOffsetを調整しても、cellForItemAtIndexPath
が呼び出されることはありません。 contentOffsetが変更されたときにセルをレンダリングするにはどうすればよいですか?
編集:もう少し参考のために(それが大いに役立つかどうかはわかりません):
- (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];
}
}
次のように、アニメーションブロック内に[self.view layoutIfNeeded];
を追加するだけです。
[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];
CADisplayLinkを使用して、アニメーションを自分で駆動してみることができます。とにかく線形アニメーションカーブを使用しているので、これを設定するのはそれほど難しくありません。これがあなたのために働くかもしれない基本的な実装です:
@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];
}
これがSwiftの実装であり、これが必要な理由を説明するコメントが付いています。
考え方はdevdavidの回答と同じですが、実装アプローチのみが異なります。
/*
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
}
UICollectionView
は、スクロールが終了するまで待ってから更新することで、パフォーマンスを向上させようとしているのではないかと思います。
アニメーションをチャックに分割することもできますが、それがどれほどスムーズかはわかりません。
または、スクロール中に定期的にsetNeedsDisplayを呼び出しますか?
あるいは、UICollectionViewのこの置換は、必要なものを必要とするか、そうするように変更することができます。