Animating UICollectionView contentOffset does not display non-visible cells

后端 未结 7 2103
旧巷少年郎
旧巷少年郎 2021-02-01 18:31

I\'m working on some ticker-like functionality and am using a UICollectionView. It was originally a scrollView, but we figure a collectionView will make it easier

相关标签:
7条回答
  • 2021-02-01 19:12

    Here is a swift implementation, with comments explaining why this is needed.

    The idea is the same as in devdavid's answer, only the implementation approach is different.

    /*
    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
    }
    
    0 讨论(0)
提交回复
热议问题