美文网首页iOS技术资料iOS开发专题iOS 大神之路
动手写一个快速集成网易新闻,腾讯视频,头条首页的ScrollPa

动手写一个快速集成网易新闻,腾讯视频,头条首页的ScrollPa

作者: ZeroJ | 来源:发表于2016-04-14 14:17 被阅读7651次

    OC版简书

    OC版源码

    最终效果

    更新示例.gif
    示例效果.gif 示例效果1.gif 示例效果2.gif 示例效果3.gif 示例效果4.gif 示例效果5.gif 示例效果6.gif

    一.构思部分:

    打算分为三个部分, 滑块部分View, 内容显示部分View, 包含滑块View和显示内容View的View,以便于可以灵活的使用

    1. 滑块部分View

    1.1 要实现滑块可以滚动, 考虑可以直接使用collectionView, 但是这里还是直接使用scrollView方便里面的控件布局

    1.2 要实现滑块的点击, 可以直接使用UIButton, 但是经过尝试, 要让button的frame随着文字的宽度来自适应实现比较麻烦, 所以选择了使用UILabel添加点击手势来实现点击事件,这里使用了closures来实现(可以使用代理模式)

    1.3 实现对应的滚动条和遮盖同步移动的效果,文字颜色渐变功能(在点击的时候直接使用一个动画就可以简单的完成了)

    2. 内容显示部分View

    2.1 用来作为包含子控制器的view的容器, 并且实现可以分页滚动的效果

    2.2 要实现分页滚动, 可以使用UIScrollView来实现, 但是这样就要考虑UIScrollView上的各个view的复用的问题, 其中细节还是很麻烦, 所以直接使用了UICollectionView(利用他的重用机制)来实现

    2.3 将每一个子控制器的view直接添加到对应的每一个cell的contentView中来展示, 所以这里需要注意cell重用可能带来的内容显示不正常的问题, 这里采用了每次添加contentView的内容时移除所有的subviews(也可以直接给每个cell用不同的reuseIdentifier实现)

    2.4 实现实时监控滚动的进度提供给滑块部分来同步调整滚动条和遮盖,文字颜色的渐变, 并且在每次滚动完成的时候可以通知给滑块来调整他的内容

    3. 包含滑块View和显示内容View的View

    3.1 因为滑块部分View和内容显示部分View是相对独立的部分, 在这里只需要实现两者的通信即可

    3.2 可以自定义滑块部分View和内容显示部分View的frame

    实现部分

    a. 滑块部分

    1. 基本属性

    /// 所有的title设置 -> 使用了一个结构体, 将可以自定义的部分全部暴露了出来, 使用的时候就可以比较方便的自定义很多属性  -> 初始化时传入的
    var segmentStyle: SegmentStyle
    
    /// 点击响应的closures
    var titleBtnOnClick:((label: UILabel, index: Int) -> Void)?
    /// 用来缓存所有标题的宽度, 达到根据文字的字数和font自适应控件的宽度 -> 为了只计算一次文字的宽度
    private var titlesWidthArray: [CGFloat] = []
    /// 所有的标题 -> 初始化时传入的
    var titles:[String]
     /// 缓存标题labels -> 以便于通过下标直接取值
    private var labelsArray: [UILabel] = []
    
    
        /// 滚动条
    private lazy var scrollLine: UIView? = {[unowned self] in
        let line = UIView()
        return self.segmentStyle.showLine ? line : nil
    }()
    /// 遮盖 -> 懒加载
    private lazy var coverLayer: UIView? = {[unowned self] in
        let cover = UIView()
        cover.layer.cornerRadius = CGFloat(self.segmentStyle.coverCornerRadius)
        // 这里只有一个cover 需要设置圆角, 故不用考虑离屏渲染的消耗, 直接设置 masksToBounds 来设置圆角
        cover.layer.masksToBounds = true
        
        return self.segmentStyle.showCover ? cover : nil
    
    }()
    
    
        /// 背景图片
    var backgroundImage: UIImage? = nil {
        didSet {
            // 在设置了背景图片的时候才添加imageView
            if let image = backgroundImage {
                backgroundImageView.image = image
                insertSubview(backgroundImageView, atIndex: 0)
    
            }
        }
    }
    private lazy var backgroundImageView: UIImageView = {[unowned self] in
        let imageView = UIImageView(frame: self.bounds)
        return imageView
    }()
    
    ```
    

    逻辑处理

        init(frame: CGRect, segmentStyle: SegmentStyle, titles: [String]) {
        self.segmentStyle = segmentStyle
        self.titles = titles
        super.init(frame: frame)
        // 这个函数里面设置了基本属性中的titles, labelsArray, titlesWidthArray,并且添加了label到scrollView上
        setupTitles()
        // 这个函数里面设置了遮盖, 滚动条,和label的初始化位置
        setupUI()
    }
    
    
    func titleLabelOnClick(tapGes: UITapGestureRecognizer) -> 处理点击title的时候实现标题的切换,和遮盖,滚动条...的位置调整, 同时执行了相应点击得儿blosure, 以便于外部相应点击方法
    
    func adjustTitleOffSetToCurrentIndex(currentIndex: Int) -> 更改scrollview的contentOffSet来居中显示title
    
    
     // 手动滚动时需要提供动画效果
    func adjustUIWithProgress(progress: CGFloat,  oldIndex: Int, currentIndex: Int) -> 提供给外部来执行标题切换之间的动画效果(注意这个方法里面进行了一些简单的数学计算以便于"同步" 滚动滚动条和cell )
    这里以滑块的位置x变化为例, 其他类似
    
    let xDistance = currentLabel.frame.origin.x - oldLabel.frame.origin.x
    这个xDistance就是滑块将要从一个label下面移动到下一个label下面所需要移动的路程,
    
    这个progress是外界提供来的, 表示当前已经移动的百分比(0 --- 1)是多少了,所以可以改变当前滑块的x为之前的x + 已经完成滚动的距离(xDistance * progress)
    scrollLine?.frame.origin.x = oldLabel.frame.origin.x + xDistance * progress
    
    这样就达到了滑块的位置随着提供的progress同步移动
    

    b 内容显示部分View

    基本属性

    /// 所有的子控制器
        private var childVcs: [UIViewController] = []
        /// 用来禁止调用scrollview的代理来进行相关的计算
        var forbidTouchToAdjustPosition = false
        /// 用来记录开始滚动的offSetX -> 用于判断滚动的方向是向左还是向右, 同时方便设置下面两个Index
        private var oldOffSetX:CGFloat = 0.0
        private var oldIndex = 0
        private var currentIndex = 1
        
        weak var delegate: ContentViewDelegate?
        
        // UICollectionView用来显示子控制器的view的内容
        private lazy var collectionView: UICollectionView = {[weak self] in
            let flowLayout = UICollectionViewFlowLayout()
            
            let collection = UICollectionView(frame: CGRectZero, collectionViewLayout: flowLayout)
            
            if let strongSelf = self {
                flowLayout.itemSize = strongSelf.bounds.size
                flowLayout.scrollDirection = .Horizontal
                flowLayout.minimumLineSpacing = 0
                flowLayout.minimumInteritemSpacing = 0
                
                collection.bounces = false
                collection.showsHorizontalScrollIndicator = false
                collection.frame = strongSelf.bounds
                collection.collectionViewLayout = flowLayout
                collection.pagingEnabled = true
                // 如果不设置代理, 将不会调用scrollView的delegate方法
                collection.delegate = strongSelf
                collection.dataSource = strongSelf
                collection.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: ContentView.cellId) 
             }
            return collection
        }()
    
    
    

    逻辑处理

    // 初始化设置frame和子控制器
        init(frame:CGRect, childVcs:[UIViewController]) {
            self.childVcs = childVcs
            super.init(frame: frame)
            // 设置collectionView的frame和添加collectionView同时做相关的数据错误判断
            commonInit()
        }
        
        
        func setContentOffSet(offSet: CGPoint , animated: Bool)  // 提供给外部来设置contentOffSet    -> 比如说点击了滑块切换title时同时切换内容显示
        
        
        
    extension ContentView: UICollectionViewDelegate, UICollectionViewDataSource{
        其中的设置了cell的内容和个数
    
    }
    
    
    extension ContentView: UIScrollViewDelegate {
    
    这里面使用到了监控滚动的过程, 以便于计算滚动的进度和页数的改变, 同时使用代理来完成相应的工作
    主要逻辑在func scrollViewWillBeginDragging(scrollView: UIScrollView) 方法里面
    }
    
    
    定义了一个protocol来完成相关的操作
    protocol ContentViewDelegate: class {
        func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat)
        func contentViewDidEndMoveToIndex(currentIndex: Int)
        var segmentView: ScrollSegmentView { get }
    }
    
    // 由于每个遵守这个协议的都需要执行些相同的操作, 所以直接使用协议扩展统一完成,协议遵守者只需要提供segmentView即可
    extension ContentViewDelegate {
        
        // 内容每次滚动完成时调用, 确定title和其他的控件的位置
        func contentViewDidEndMoveToIndex(currentIndex: Int) {
            segmentView.adjustTitleOffSetToCurrentIndex(currentIndex)
            segmentView.adjustUIWithProgress(1.0, oldIndex: currentIndex, currentIndex: currentIndex)
        }
        
        // 内容正在滚动的时候,同步滚动滑块的控件
        func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat) {
            segmentView.adjustUIWithProgress(progress, oldIndex: fromIndex, currentIndex: toIndex)
        }
    }
    
    
    
    

    c. 包含滑块View和显示内容View的View

    这一部分比较简单直接看代码就ok了

    //
    //  ScrollPageView.swift
    //  ScrollViewController
    //
    //  Created by jasnig on 16/4/6.
    //  Copyright © 2016年 ZeroJ. All rights reserved.
    //
    
    import UIKit
    
    class ScrollPageView: UIView {
        static let cellId = "cellId"
        var segmentStyle = SegmentStyle()
        
        var segView: ScrollSegmentView!
        var contentView: ContentView!
        
        var titlesArray: [String] = []
        /// 所有的子控制器
        var childVcs: [UIViewController] = []
        
        init(frame:CGRect, segmentStyle: SegmentStyle, titles: [String], childVcs:[UIViewController]) {
            self.childVcs = childVcs
            self.titlesArray = titles
            self.segmentStyle = segmentStyle
            assert(childVcs.count == titles.count, "标题的个数必须和子控制器的个数相同")
            super.init(frame: frame)
            // 初始化设置了frame后可以在以后的任何地方直接获取到frame了, 就不必重写layoutsubview()方法在里面设置各个控件的frame
            commonInit()
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        
        func commonInit() {
    
            segView = ScrollSegmentView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: 44), segmentStyle: segmentStyle, titles: titlesArray)
            
            contentView = ContentView(frame: CGRect(x: 0, y: CGRectGetMaxY(segView.frame), width: bounds.size.width, height: bounds.size.height - 44), childVcs: childVcs)
            contentView.delegate = self
            
            addSubview(contentView)
            addSubview(segView)
            // 在这里调用了懒加载的collectionView, 那么之前设置的self.frame将会用于collectionView,如果在layoutsubviews()里面没有相关的处理frame的操作, 那么将导致内容显示不正常
            // 避免循环引用
            segView.titleBtnOnClick = {[unowned self] (label: UILabel, index: Int) in
                
                // 不要执行collectionView的scrollView的滚动代理方法
                self.contentView.setContentOffSet(CGPoint(x: self.contentView.bounds.size.width * CGFloat(index), y: 0), animated: false)
            }
    
    
        }
     
    }
    
    
    extension ScrollPageView: ContentViewDelegate {
    
        var segmentView: ScrollSegmentView {
            return segView
        }
    
    }
    
    

    使用方法

    使用方式一

    Snip20160414_1.png

    使用方式二

    Snip20160414_3.png

    更新说明: 所有更新内容都在源码中有示例

    * 2016/04/22 增加自定义选中下标功能, 增加 简书个人主页的使用示例

    * 2016/05/02 增加动态更新显示内容


    详细请移步源码, 如果您觉得有帮助,不妨给个star鼓励一下, 欢迎关注

    相关文章

      网友评论

      • 18b7e4b39401:发现了一个bug, 就是当子控制器中tableView的cell数量很少的时候, 快速拖动一下tableView往上滑动, 会导致tableView的偏移异常,segmentView会遮盖住部分cell, 但是当慢慢的拖动tabelView的时候是正常的,不会被遮盖
        18b7e4b39401:微博&简书个人主页效果
      • yanghaofeng666:大佬,问一下,这个demo里有个 微博&简书个人主页效果 的界面,能不能实现刷新的时候刷新每个单独的列表,而不是刷新整个界面的头部?
      • leonBreeze:你好,使用过程中我发现有些问题,有一些疑惑,
        例如:// 处理当前子控制器的生命周期 : 已知问题, 当push的时候会被调用两次
        - (void)willMoveToWindow:(nullable UIWindow *)newWindow 这个为什么view消失时仍会调用呢,而且当pop回来时,如果页面有tableview的话,会自动调用tableview的- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法
      • 繁华乱世沧桑了谁的容颜:OC版有BUG 不能用啊 在iPhone 6Plus上面的 iOS10.3.2系统上 title进去就往下跑消失了 左滑一下才显示,可能是iOS 11一下都有这个问题 研究半天不知道怎么解决啊
      • 翀鹰精灵:希望后续能够支持cocoapods
      • Passon_Fang:用 pod 导入后好多语法错 (swift3),作者还有计划更新吗?
      • 35b0fedb0676:collectionview的移动有oc的吗
      • feng_dev:期待 故事板 方式
      • jshto:问下大神可以自定义滚动条宽度吗
      • lwhldy:我的第一个上线项目用的就是大神你得这个😬😬
      • lwhldy:你好,如果每个内容页面的数据不同 如何处理呢
      • skylor:666
      • 叶星龙:我去~整的晕头转向的!还好弄明白了!但是我还是懒得去重用VC!
      • 莜12138:大神 我看了好几天你的文章了 膜拜膜拜
      • 太二道士:不错,先支持一下,希望能提供swift3.0的版本
      • icoder:当滑倒最后一条是,一直滑动,会奔溃,建议在segmentview 的进度更新标题的方法做一个数组越界的判断
        ZeroJ:@icoder 那你可以下载最新的代码试试
        icoder:@ZeroJ 会奔溃的,当滑动segment 到最后一个,还不停的滑,松开手就奔溃了
        ZeroJ:@icoder 朋友, 会崩溃么? 我的项目中是不会崩溃的, 里面肯定是做了相关的越界判断这种操作的
      • 听小马儿说:你好,没有oc版的动态修改tag值的demo吗
        ZeroJ:@EowynQ 😆, oc版的没有, 可以自己研究下
      • macfai:我是绮鑫,发现我的扣扣里咱们的群找不到了
        ZeroJ:@macfai :smile:不是在好友列表?
        macfai:@ZeroJ不是吧,你给解散了:flushed:,你扣扣多少啊,加你一下吧
        ZeroJ:@macfai 嗯,里面都是些百度都能找到的问题, 我给解散了
      • macfai:一男,群号是不是被盗了,啥情况啊 :flushed:
      • 夜神月No1:公司一个需求正需要,非常感谢
      • by小杰:太牛了!大神
      • 650c659e28f7:你好 多谢提供这么好的库,但我有一个问题,如果我载入5个vc,而这5个vc都有网络请求,这样加载的话5个网络请求会同时发生,而不是滑倒哪个哪个进行网络请求
        ZeroJ:@1ast0bject 嗯 加这个573645936
      • Jayne_Kuo:表示看了一遍懵逼了 封装的太好了!
        ZeroJ:@Jayne_Kuo :smile:夸奖了
        Jayne_Kuo:@ZeroJ 菜逼表示 研究了一遍 整个人感觉都不好了 大神就是大神 :joy: :joy: :joy: 想研究都不知道从哪里下手 还用到了runtime
        ZeroJ:@Jayne_Kuo:smile:各部分比较独立
      • iYeso:写的很好 很喜欢
        ZeroJ:@greedyDoor :smiley::smiley:希望有帮助
      • march_1991:很明白呀
        ZeroJ:@march_27066 :smile::smile:代码注释比较多
      • angelen:哈哈哈,我昨天也想分享这样子的,没想到你做得那么全,我还是拿下来好好研究一下先。。 :scream:
        angelen:@ZeroJ 我在头疼那个颜色渐变的。。。现在还没有什么思路。。
        angelen:@ZeroJ 我还没有写成框架:https://github.com/angelen10/ZLNetEase
        ZeroJ:@angelen :grin:朋友,你也可以分享出来互相学习,毕竟想法不一样
      • 这条鱼有点甜:请问大神,如果在pager上面加一个共用的header view,往上滑动时标签栏悬浮,怎么实现?或者有没有类似的开源库推荐?
        ZeroJ:@Zaaach 不好意思,这个我之前没有测试scrollview,明天我改改
        这条鱼有点甜: @ZeroJ 太感谢了!!
        ZeroJ:@Zaaach 你好, 这里我写了一个实现方案, 你可以参考下
        http://www.jianshu.com/p/6be2597345e4
      • 程旭媛:请问为什么还需要子控制器呢?直接在显示内容的view里添加【array count】个view不可以么
        ZeroJ:@jswift 对的,这样分开开发方便
        jswift:@小木偶的 肯定使用其他控制器管理自己的view呢, 一个控制器管理这么多的view不是很坑么
        ZeroJ:@小木偶的 将子控制器的view交给父控制器的view来管理
      • 月下独酌灬:嗯,写的很好。
        月下独酌灬:@BW开发者 欢迎互粉😏
        ZeroJ:@BW开发者 :grin:欢迎交流
      • 人贩子:mark
        jswift:@人贩子 我是来膜拜大神的
        人贩子:@jswift :scream::scream::scream::scream:
        jswift:@人贩子 :smile: :smile:
      • 梦生何欢:文章已收录专题,来,喝了这碗鸡汤。欢迎关注投稿!
        ZeroJ::smile::smile:好的👌🏻
      • Mg明明就是你:💪
        ZeroJ:@Mg明明就是你 :smile::smile:
      • b8cbc856f2f1:感谢(❁´ω`❁)
        ZeroJ:@huangboweicom :grin::grin:不客气,欢迎关注
      • 雷鸣1010:只想说,有源码,太帅了
        ZeroJ:@雷鸣1010 :grin:有用就好
      • jswift:正需要, 感谢无私贡献
        ZeroJ:@jswift 多谢支持 :smiley:
        jswift:@ZeroJ 膜拜大神, 持续关注,期待
        ZeroJ:@jswift :smile: 很高兴帮到你, 欢迎交流

      本文标题:动手写一个快速集成网易新闻,腾讯视频,头条首页的ScrollPa

      本文链接:https://www.haomeiwen.com/subject/ecmglttx.html