我正在开发一些类似自动收报机的功能,而且正在使用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];
}
}
答案 0 :(得分:39)
您只需在动画块中添加[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];
答案 1 :(得分:7)
您可以尝试使用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];
}
答案 2 :(得分:2)
我已经根据这些答案中已有的内容制作了一个通用的手动动画师,因为所有内容都可以简化为百分比浮点值和一个块。
class ManualAnimator {
enum AnimationCurve {
case linear, parametric, easeInOut, easeIn, easeOut
func modify(_ x: CGFloat) -> CGFloat {
switch self {
case .linear:
return x
case .parametric:
return x.parametric
case .easeInOut:
return x.quadraticEaseInOut
case .easeIn:
return x.quadraticEaseIn
case .easeOut:
return x.quadraticEaseOut
}
}
}
private var displayLink: CADisplayLink?
private var start = Date()
private var total = TimeInterval(0)
private var closure: ((CGFloat) -> Void)?
private var animationCurve: AnimationCurve = .linear
func animate(duration: TimeInterval, curve: AnimationCurve = .linear, _ animations: @escaping (CGFloat) -> Void) {
guard duration > 0 else { animations(1.0); return }
reset()
start = Date()
closure = animations
total = duration
animationCurve = curve
let d = CADisplayLink(target: self, selector: #selector(tick))
d.add(to: .current, forMode: .common)
displayLink = d
}
@objc private func tick() {
let delta = Date().timeIntervalSince(start)
var percentage = animationCurve.modify(CGFloat(delta) / CGFloat(total))
//print("%:", percentage)
if percentage < 0.0 { percentage = 0.0 }
else if percentage >= 1.0 { percentage = 1.0; reset() }
closure?(percentage)
}
private func reset() {
displayLink?.invalidate()
displayLink = nil
}
}
extension CGFloat {
fileprivate var parametric: CGFloat {
guard self > 0.0 else { return 0.0 }
guard self < 1.0 else { return 1.0 }
return ((self * self) / (2.0 * ((self * self) - self) + 1.0))
}
fileprivate var quadraticEaseInOut: CGFloat {
guard self > 0.0 else { return 0.0 }
guard self < 1.0 else { return 1.0 }
if self < 0.5 { return 2 * self * self }
return (-2 * self * self) + (4 * self) - 1
}
fileprivate var quadraticEaseOut: CGFloat {
guard self > 0.0 else { return 0.0 }
guard self < 1.0 else { return 1.0 }
return -self * (self - 2)
}
fileprivate var quadraticEaseIn: CGFloat {
guard self > 0.0 else { return 0.0 }
guard self < 1.0 else { return 1.0 }
return self * self
}
}
实施
let initialOffset = collectionView.contentOffset.y
let delta = collectionView.bounds.size.height
let animator = ManualAnimator()
animator.animate(duration: TimeInterval(1.0), curve: .easeInOut) { [weak self] (percentage) in
guard let `self` = self else { return }
self.collectionView.contentOffset = CGPoint(x: 0.0, y: initialOffset + (delta * percentage))
if percentage == 1.0 { print("Done") }
}
将 animate 函数与 init 方法结合起来可能是值得的。不过这没什么大不了的。
答案 3 :(得分:0)
我怀疑UICollectionView
正在尝试通过等待更新前滚动结束来提高性能。
也许你可以把动画分成夹头,虽然我不确定它会有多顺畅。
或者可能在滚动期间定期调用setNeedsDisplay?
或者,也许UICollectionView的替换可能要么你需要,要么可以修改为:
答案 4 :(得分:0)
这是一个快速实现,其中的注释解释了为什么需要这样做。
这个想法与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
}
答案 5 :(得分:0)
如果您需要在用户开始拖动UICollectionView之前启动动画(例如从一个页面移动到另一个页面),您可以使用此变通方法预加载侧面单元格:
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()
}
}
}
如果没有此解决方法,UICollectionView将在动画开始之前删除不与目标边界相交的单元格。
P.S。这仅在您需要下一个或上一个页面的动画时才有效。
答案 6 :(得分:-5)
改为使用:scrollToItemAtIndexPath
:
[UIView animateWithDuration:duration animations:^{
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}];