25

我正在开发一些类似自动收报机的功能,并且正在使用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];
    }
}
4

7 回答 7

44

您应该简单地[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];
于 2015-03-04T14:59:46.027 回答
9

您可以尝试使用 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];
}
于 2014-03-28T21:01:48.580 回答
3

我以这些答案中已有的内容为基础,制作了一个通用的手动动画师,因为一切都可以浓缩为百分比浮点值和一个块。

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 方法结合起来可能是值得的。不过这并不是什么大问题。

于 2021-01-23T14:12:21.540 回答
1

这是一个快速的实现,注释解释了为什么需要这样做。

这个想法与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
}
于 2017-02-20T23:51:02.653 回答
0

我怀疑这UICollectionView是试图通过等到滚动结束再更新来提高性能。

也许你可以把动画分成几块,虽然我不确定那会是多么流畅。

或者也许在滚动期间定期调用 setNeedsDisplay?

或者,也许这个 UICollectionView 的替代品需要你需要,或者可以修改为这样做:

https://github.com/steipete/PSTCollectionView

于 2014-03-25T14:37:44.507 回答
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 将在动画开始之前删除不与目标边界相交的单元格。

PS这仅在您需要动画到下一页上一页时才有效。

于 2018-02-22T13:27:46.650 回答
-5

改用:scrollToItemAtIndexPath

[UIView animateWithDuration:duration animations:^{
    [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
                                    atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}];
于 2014-03-24T17:32:58.887 回答