V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tudoutiaoya
V2EX  ›  iOS

iOS 实现三张图片左右循环滚动的效果,大家有没有什么思路? 需求是:屏幕中总共要显示三张图片,中间是图片的完整部分,左右两边分别是图片的一部分内容,每次滑动都相当于那种分页的效果

  •  
  •   tudoutiaoya · 233 天前 · 976 次点击
    这是一个创建于 233 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我的思路是用 UICollectionView ,定义 100 组(每组就是那三张图片),然后当向左滑动到头的时候或者向右滑动到头的时候,定位到中间 50 组数据的部分,但是这样会有一个切换效果:就是当你向左滑动,本来动画是向左滑动的动画,但是当向左滑动到头的时候,他会向右切换到中间的位置,同样的向右滑动也是这样的道理,导致有明显的切换效果,大家有什么解决方案吗

    或者采用其他什么思路实现左右循环滚动丝滑的切换

    要实现的效果图; http://cdn.tudoutiao.pro/2023-09-07%2011.23.28.gif

    我的仓库地址: https://github.com/tudoutiaoya/ScrollPicture

    下面是我的代码

    
    import UIKit
    
    class HorizontalRollViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
        
        let collectionView: UICollectionView
        let images = ["image1", "image2", "image3"]
        let layout: UICollectionViewFlowLayout
        
        var myOffsetX = 0.0 // 记录上次的 offsetx 便于判断是左滑还是右滑
        let groupNum = 100 // 定义多少个组
        
        let lineSpacing = 30.0
        let itemWidth = UIScreen.main.bounds.width/2 // 卡片宽度
        let itemHeigh = UIScreen.main.bounds.height/2
        
        init() {
            layout = UICollectionViewFlowLayout()
            layout.minimumLineSpacing = self.lineSpacing
            layout.itemSize = CGSize(width: itemWidth, height: itemHeigh)
            layout.scrollDirection = .horizontal
            
            collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
            collectionView.decelerationRate = .fast
            
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override func viewDidLoad() {
            view.backgroundColor = .white
            setupSubView()
            // 初始定位到中间
            collectionView.scrollToItem(at: IndexPath.init(item: groupNum/2 * images.count , section: 0), at: .centeredHorizontally, animated: false)
        }
        
        func setupSubView() {
            collectionView.frame = view.bounds
            collectionView.dataSource = self
            collectionView.delegate = self
            collectionView.isPagingEnabled = false
            // 注册单元格
            collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
            view.addSubview(collectionView)
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            // 图片的数量
            return groupNum * images.count
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for:indexPath)
            // 移除之前的子视图
            cell.contentView.subviews.forEach { $0.removeFromSuperview() }
            // 取余 计算出应该在 images 数组哪个位置
            let imageIndex = indexPath.item % images.count
            let imageView = UIImageView(image: UIImage(named: images[imageIndex]))
            imageView.frame = cell.bounds
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            cell.contentView.addSubview(imageView)
            return cell
        }
        
        
        func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
            // 停止滑动时,当前的偏移量(即最近停止的位置)
            self.myOffsetX = scrollView.contentOffset.x
        }
        
        // collectionView.pagingEnabled = NO;
        // 禁止分页滑动时,根据偏移量判断滑动到第几个 item
        // 滑动 “减速滚动时” 是触发的代理,当用户用力滑动或者清扫时触发
        func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
            self.scrollToNextPageOrLastPage(scrollView)
        }
        
        // 用户拖拽时 调用
        func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
            self.scrollToNextPageOrLastPage(scrollView)
        }
    
        func scrollToNextPageOrLastPage(_ scrollView: UIScrollView) {
            
            // 到达左右边界,定位到中间
            let contentWidth = (itemWidth+lineSpacing) * Double(groupNum*images.count) // 内容的总宽度
            let adjustedContentWidth = contentWidth - lineSpacing // 调整后的内容宽度,减去最后一个间距
            let rightOffset = adjustedContentWidth - scrollView.bounds.width // 右侧边界的偏移量
            if (scrollView.contentOffset.x >= rightOffset || scrollView.contentOffset.x <= 0) {
                collectionView.scrollToItem(at: IndexPath.init(item: groupNum/2 * images.count , section: 0), at: .centeredHorizontally, animated: false)
                print("切换了")
                return
            }
            
            // 之前停止的位置,判断左滑、右滑
            if (scrollView.contentOffset.x > self.myOffsetX) { // 左滑,下一个( i 最大为 cell 个数)
                
                // 计算移动的 item 的个数( item.width + 间距)
                let i = Int(scrollView.contentOffset.x / (itemWidth + lineSpacing) + 1)
    
                let indexPath = IndexPath(row: i, section: 0)
                // item 居中显示
                collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
                
            } else { // 右滑,上一个( i 最小为 0 )
                
                let i = Int(scrollView.contentOffset.x / (itemWidth + lineSpacing) + 1)
                
                let indexPath = IndexPath(row: i, section: 0)
                
                collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    
            }
            
            
        }
    }
    
    
    
    
    11 条回复    2023-09-08 12:10:20 +08:00
    wenjor
        1
    wenjor  
       233 天前
    考虑下直接写动画?
    Chang12
        2
    Chang12  
       233 天前
    FSPagerView
    iOCZ
        3
    iOCZ  
       233 天前
    无限滚动已经出现那么多年了,思路无非就是你说的这种。如果是定时器自动滚动的话,每次都滚动中间那组就行了。如果是手动触发的话,向左向右滚动到尽头应该还是比较累的。
    iOCZ
        4
    iOCZ  
       233 天前
    讲道理,当你不同滚动的时候,你无法偷偷换到中间的那组,一般会选择滚动结束的时候偷偷把 offset 设置回去
    iyeatse
        5
    iyeatse  
       233 天前
    怎么弄这么麻烦,scrollViewWillEndDragging 的第三个参数是个指针,可以修改的
    tudoutiaoya
        6
    tudoutiaoya  
    OP
       233 天前
    @iOCZ 能详细讲讲不佬,刚学 iOS😭
    iOCZ
        7
    iOCZ  
       233 天前
    @tudoutiaoya 我说的这个是 SDCycleScrollView 的实现思路,不过他似乎没处理你这种手工滚到头的情况,你可以看看源码。。。我看你这个没定时器
    V2SuperUser
        8
    V2SuperUser  
       232 天前
    根据 https://juejin.cn/post/6940140043042291748 的缩放效果改了一个,你试试效果

    ```Swift

    import UIKit

    class ViewController: UIViewController {

    private let margin: CGFloat = 20
    private var itemW: CGFloat = .zero
    private let cellID = "baseCellID"
    private let cellCount = 100
    private var collectionView: UICollectionView!

    override func viewDidLoad() {
    super.viewDidLoad()
    setUpView()
    }

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    }

    func setUpView() {
    let layout = ZGFlowLayout()
    let collH: CGFloat = 200
    let itemH = collH - margin * 2
    itemW = view.bounds.width - margin * 2 - 100
    layout.itemSize = CGSize(width: itemW, height: itemH)
    layout.minimumLineSpacing = margin
    layout.scrollDirection = .horizontal

    collectionView = UICollectionView(frame: CGRect(x: 0, y: 180, width: view.bounds.width, height: collH), collectionViewLayout: layout)
    collectionView.backgroundColor = .black
    collectionView.showsHorizontalScrollIndicator = false
    collectionView.dataSource = self
    collectionView.delegate = self

    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellID)
    view.addSubview(collectionView)
    scrollTo(index: cellCount/2)
    }
    }

    extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource{

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return cellCount
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath)
    let colors: [UIColor] = [.red, .yellow, .blue]
    cell.backgroundColor = colors[indexPath.item % 3]
    return cell
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    let scrollStop = !scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating
    guard scrollStop else { return }
    ZGScrollViewDidEndScroll(scrollView: scrollView)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    //防止滚动到最后或者最前,如果觉得改变时突兀,可以增加 cellCount
    let page = getCurrentPage(scrollView: scrollView)
    if page >= 95 || page <= 5 {
    scrollTo(index: page%3 + cellCount+1)
    }
    }

    private func ZGScrollViewDidEndScroll(scrollView: UIScrollView) {
    let page = getCurrentPage(scrollView: scrollView)
    scrollTo(index: page%3 + cellCount+1)
    }

    private func scrollTo(index: Int){
    collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false)
    }

    private func getCurrentPage(scrollView: UIScrollView) -> Int{
    //第一个 page 偏移量会少了多显示出来的一半,不使用之后的计算,直接判定为 0
    var page: CGFloat = 0
    if scrollView.contentOffset.x > 0 {
    //计算单次滑动的偏移量
    let scrollW = scrollView.frame.width
    //显示的多出一半的宽度
    let half = (scrollW - itemW)/2
    //除第一个外其余每次滑动的偏移量
    let eachOffset = (itemW+margin)
    //第一个 cell 的偏移量
    let firstOffset = eachOffset-half
    page = (scrollView.contentOffset.x-firstOffset)/(eachOffset) + 1
    }
    return Int(page)
    }
    }

    class ZGFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)
    let centerX = collectionView!.contentOffset.x + collectionView!.bounds.width / 2
    attributes?.forEach({ (attr) in
    let pad = abs(centerX - attr.center.x)
    let factor = 0.0009
    let scale = 1 / (1 + pad * CGFloat(factor))
    attr.transform = CGAffineTransform(scaleX: scale, y: scale)
    })
    return attributes
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var targetPoint = proposedContentOffset
    let centerX = proposedContentOffset.x + collectionView!.bounds.width / 2
    let attrs = self.layoutAttributesForElements(in: CGRect(x: proposedContentOffset.x, y: proposedContentOffset.y, width: collectionView!.bounds.size.width, height: collectionView!.bounds.size.height))
    var moveDistance: CGFloat = CGFloat(MAXFLOAT)
    attrs!.forEach { (attr) in
    if abs(attr.center.x - centerX) < abs(moveDistance) {
    moveDistance = attr.center.x - centerX
    }
    }
    if targetPoint.x > 0 && targetPoint.x < collectionViewContentSize.width - collectionView!.bounds.width {
    targetPoint.x += moveDistance
    }
    return targetPoint
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
    return true
    }

    override var collectionViewContentSize: CGSize {
    return CGSize(width: sectionInset.left + sectionInset.right + (CGFloat(collectionView!.numberOfItems(inSection: 0)) * (itemSize.width + minimumLineSpacing)) - minimumLineSpacing, height: 0)
    }
    }


    ```
    V2SuperUser
        9
    V2SuperUser  
       232 天前
    @V2SuperUser 为啥我的代码不能像 OP 一样有格式
    V2SuperUser
        10
    V2SuperUser  
       232 天前
    @V2SuperUser 修正:两处 scrollTo(index: page%3 + cellCount+1)改为 scrollTo(index: page%3 + cellCount/2+1)
    V2SuperUser
        11
    V2SuperUser  
       232 天前
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   952 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:34 · PVG 03:34 · LAX 12:34 · JFK 15:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.