最近实现了类似小红书欢迎页面的循环滚动背景的效果,思路有两个:

1、使用两个同样大小的ImageView进行轮播

private var firstImageView: UIImageView 
private var secondImageView: UIImageView

1.1 第一个ImageView正常靠顶部布局,第二个ImageView的origin.y 是第一个ImageView的底部

public override func layoutSubviews() {
    super.layoutSubviews()
    
    let h = firstImageView.bounds.height
    let w = bounds.width
    firstImageView.frame = CGRect(x: 0, y: 0, width: w, height: h)
    secondImageView.frame = CGRect(x: 0, y: h, width: w, height: h)
}

1.2 然后对这两个ImageView同时做动画,将第一个ImageView的 origin.y = -height,第二个ImageView的 origin.y = 0

UIView.animate(withDuration: self.duration, delay: 0, options: [.curveLinear], animations: { [weak self] in
    self?.firstImageView.frame.origin.y = -pageHeight
    self?.secondImageView.frame.origin.y = 0
}) { _ in
    ...
}

1.3 在动画结束的时候,将第一个ImageView的 origin.y 设置为 height, 即把第一个ImageView放到第二个ImageView的下方,将第二个ImageView的 origin.y 设置为 0,同时交换两个ImageView

UIView.animate(withDuration: self.duration, delay: 0, options: [.curveLinear], animations: { [weak self] in
    ...
}) { [weak self] _ in
    guard let strongSelf = self else {
        return
    }
    strongSelf.firstImageView.frame.origin.y = pageHeight
    strongSelf.secondImageView.frame.origin.y = 0
    (strongSelf.firstImageView, strongSelf.secondImageView) = (strongSelf.secondImageView, strongSelf.firstImageView)
}

1.4 重复 2 - 4 即能做到无限循环播放。

UIView.animate(withDuration: self.duration, delay: 0, options: [.curveLinear], animations: { [weak self] in
    ...
}) { [weak self] _ in
    guard let strongSelf = self else {
        return
    }
    ...
    strongSelf.startAnimation()
}

2、使用UICollectionView

2.1 对 contentOffset.y进行动画。同时需要将UICollectionView的手势禁用

完整的代码:

@IBDesignable
public class RecycleAnimatedView: UIView {

    private var firstImageView: UIImageView
    
    private var secondImageView: UIImageView
    
    override public init(frame: CGRect) {
        firstImageView = UIImageView()
        secondImageView = UIImageView()
        super.init(frame: frame)
        commonInit()
    }
    
    required public init?(coder aDecoder: NSCoder) {
        firstImageView = UIImageView()
        secondImageView = UIImageView()
        super.init(coder: aDecoder)
        commonInit()
    }
    
    private func commonInit() {
        addSubview(firstImageView)
        addSubview(secondImageView)
        setNeedsDisplay()
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        
        let h = firstImageView.bounds.height
        let w = bounds.width
        firstImageView.frame = CGRect(x: 0, y: 0, width: w, height: h)
        secondImageView.frame = CGRect(x: 0, y: h, width: w, height: h)
    }
    
    @IBInspectable
    public var image: UIImage? {
        didSet {
            firstImageView.image = image
            secondImageView.image = image
            if let image = image {
                let height = bounds.width * image.size.height / image.size.width
                let size = CGSize(width: bounds.width, height: height)
                firstImageView.frame.size = size
                secondImageView.frame.size = size
                layoutIfNeeded()
            }
        }
    }
    
    @IBInspectable
    public var duration: TimeInterval = 20.0
    
    public func startAnimation() {
        DispatchQueue.main.async {
            let pageHeight = self.firstImageView.frame.height
            if pageHeight <= 0 {
                return
            }
            UIView.animate(withDuration: self.duration, delay: 0, options: [.curveLinear], animations: { [weak self] in
                self?.firstImageView.frame.origin.y = -pageHeight
                self?.secondImageView.frame.origin.y = 0
            }) { [weak self] _ in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.firstImageView.frame.origin.y = pageHeight
                strongSelf.secondImageView.frame.origin.y = 0
                (strongSelf.firstImageView, strongSelf.secondImageView) = (strongSelf.secondImageView, strongSelf.firstImageView)
                strongSelf.startAnimation()
            }
        }
    }
}

有兴趣的可以尝试用 UICollectionView的方式实现。