最终效果

更新示例.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

文/ZeroJ(简书作者)
原文链接:http://www.jianshu.com/p/b84f4dd96d0c
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

转载于:https://www.cnblogs.com/fengmin/p/5460698.html

动手写一个快速集成网易新闻,腾讯视频,头条首页的ScrollPageView,显示滚动视图...相关推荐

  1. 自己动手写一个推荐系统,推荐系统小结,推荐系统:总体介绍、推荐算法、性能比较, 漫谈“推荐系统”, 浅谈矩阵分解在推荐系统中的应用...

    自己动手写一个推荐系统 废话: 最近朋友在学习推荐系统相关,说是实现完整的推荐系统,于是我们三不之一会有一些讨论和推导,想想索性整理出来. 在文中主要以工程中做推荐系统的流程着手,穿插一些经验之谈,并 ...

  2. 用装饰者模式 动手写一个导弹系统组装功能(滑稽+1)

    话接上文,敌人导弹来袭,我们的雷达做出了及时反应,通过观察者模式通知了五大战区做好防御, <用观察者模式 动手写一个导弹预警系统(滑稽)> 现在轮到我们反击了. 目前国防部发布了指示要求我 ...

  3. java 手编线程池_死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  4. 自己动手写一个印钞机 第四章

    2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...

  5. Spring Boot 动手写一个 Start

    我们在使用SpringBoot 项目时,引入一个springboot start依赖,只需要很少的代码,或者不用任何代码就能直接使用默认配置,再也不用那些繁琐的配置了,感觉特别神奇.我们自己也动手写一 ...

  6. 自己动手写一个nodejs的日志生成器

    自己动手写一个nodejs的logger 最近正在边学边用node.js开发个人应用的server,由于有用到websocket相关,想对websocket的通信选择性的做下日志记录,所以萌发了自己动 ...

  7. 自己动手写一个印钞机 第二章

    2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...

  8. 学习较底层编程:动手写一个C语言编译器

    动手编写一个编译器,学习一下较为底层的编程方式,是一种学习计算机到底是如何工作的非常有效方法. 编译器通常被看作是十分复杂的工程.事实上,编写一个产品级的编译器也确实是一个庞大的任务.但是写一个小巧可 ...

  9. 自己动手写一个 strace

    这次主要分享一下一个动手的东西,就是自己动手写一个 strace 工具. 用过 strace 的同学都知道,strace 是用来跟踪进程调用的 系统调用,还可以统计进程对 系统调用 的统计等.stra ...

最新文章

  1. python编程可视化小程序_人人都可以写的可视化Python小程序第二篇:旋转的烟花...
  2. 解决Android emulator PANIC: Missing emulator engine program for ‘x86‘ CPU.
  3. python——闭包
  4. 【转】.NET Core全面扫盲贴
  5. POJ3009-Curling 2.0
  6. 作家笔下的数学与数学家
  7. 阿里开发者招聘节 | 面试题02-04:给定一个二叉搜索树(BST),找到树中第K小的节点
  8. 手工制作机器人用彩泥_灌浆壶和手工壶的区别,你知道吗?
  9. CY7C68013 USB接口相机开发记录 - 第一天:资料下载
  10. Java读写二进制文件示例
  11. 格式工厂 wav 比特率_智能音乐格式转换
  12. vue点击按钮切换样式
  13. Edge浏览器运行卡顿怎么办 怎样让Edge浏览器速度更快
  14. 信息报送不及时整改措施_意见建议及整改措施
  15. 你平时都怎么记笔记?给好学的你安利10个最好用的记笔记神器!
  16. 【故障检测】基于 KPCA 的故障检测(Matlab代码实现)
  17. php抓取网页上的指定内容
  18. QImage 图像格式小结 Format_RGB32
  19. Induction Networks for Few-Shot Text Classification(2020-08-31)
  20. effective c++第四章(条款18-25)

热门文章

  1. 2017软件工程第三次作业--效能分析
  2. HTML Viewer的定制
  3. Mysql 5.8 参数调优
  4. Xmind settings lower
  5. vCenter Server管理:[2]创建只读账户
  6. Python 编码风格参考
  7. NetApp 7MTT 工具进行存储数据的迁移
  8. 二十四种设计模式:备忘录模式(Memento Pattern)
  9. golang 远程传输文件
  10. Linux基金会:Linux已经战胜微软