iOS 实现三张图片左右循环滚动的效果,大家有没有什么思路? 需求是:屏幕中总共要显示三张图片,中间是图片的完整部分,左右两边分别是图片的一部分内容,每次滑动都相当于那种分页的效果 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tudoutiaoya
V2EX    iOS

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

  •  
  •   tudoutiaoya 2023-09-07 17:11:05 +08:00 2039 次点击
    这是一个创建于 841 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我的思路是用 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  
       2023-09-07 17:41:02 +08:00
    考虑下直接写动画?
    Chang12
        2
    Chang12  
       2023-09-07 17:51:29 +08:00
    FSPagerView
    iOCZ
        3
    iOCZ  
       2023-09-07 18:17:57 +08:00
    无限滚动已经出现那么多年了,思路无非就是你说的这种。如果是定时器自动滚动的话,每次都滚动中间那组就行了。如果是手动触发的话,向左向右滚动到尽头应该还是比较累的。
    iOCZ
        4
    iOCZ  
       2023-09-07 18:32:22 +08:00
    讲道理,当你不同滚动的时候,你无法偷偷换到中间的那组,一般会选择滚动结束的时候偷偷把 offset 设置回去
    iyeatse
        5
    iyeatse  
       2023-09-07 18:43:39 +08:00
    怎么弄这么麻烦,scrollViewWillEndDragging 的第三个参数是个指针,可以修改的
    tudoutiaoya
        6
    tudoutiaoya  
    OP
       2023-09-07 19:04:11 +08:00
    @iOCZ 能详细讲讲不佬,刚学 iOS
    iOCZ
        7
    iOCZ  
       2023-09-07 20:04:22 +08:00
    @tudoutiaoya 我说的这个是 SDCycleScrollView 的实现思路,不过他似乎没处理你这种手工滚到头的情况,你可以看看源码。。。我看你这个没定时器
    V2SuperUser
        8
    V2SuperUser  
       2023-09-08 11:34:31 +08:00
    根据 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) -> UICllectionViewCell {
    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  
       2023-09-08 11:36:47 +08:00
    @V2SuperUser 为啥我的代码不能像 OP 一样有格式
    V2SuperUser
        10
    V2SuperUser  
       2023-09-08 11:40:56 +08:00
    @V2SuperUser 修正:两处 scrollTo(index: page%3 + cellCount+1)改为 scrollTo(index: page%3 + cellCount/2+1)
    V2SuperUser
        11
    V2SuperUser  
       2023-09-08 12:10:20 +08:00
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2788 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 02:23 PVG 10:23 LAX 18:23 JFK 21:23
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86