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

245 天前
 tudoutiaoya

我的思路是用 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)

        }
        
        
    }
}



994 次点击
所在节点    iOS
11 条回复
wenjor
245 天前
考虑下直接写动画?
Chang12
245 天前
FSPagerView
iOCZ
245 天前
无限滚动已经出现那么多年了,思路无非就是你说的这种。如果是定时器自动滚动的话,每次都滚动中间那组就行了。如果是手动触发的话,向左向右滚动到尽头应该还是比较累的。
iOCZ
245 天前
讲道理,当你不同滚动的时候,你无法偷偷换到中间的那组,一般会选择滚动结束的时候偷偷把 offset 设置回去
iyeatse
245 天前
怎么弄这么麻烦,scrollViewWillEndDragging 的第三个参数是个指针,可以修改的
tudoutiaoya
245 天前
@iOCZ 能详细讲讲不佬,刚学 iOS😭
iOCZ
245 天前
@tudoutiaoya 我说的这个是 SDCycleScrollView 的实现思路,不过他似乎没处理你这种手工滚到头的情况,你可以看看源码。。。我看你这个没定时器
V2SuperUser
244 天前
根据 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
244 天前
@V2SuperUser 为啥我的代码不能像 OP 一样有格式
V2SuperUser
244 天前
@V2SuperUser 修正:两处 scrollTo(index: page%3 + cellCount+1)改为 scrollTo(index: page%3 + cellCount/2+1)
V2SuperUser
244 天前

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/971795

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX