原文地址:jiar.me/article/Mul…git
本文旨在对于SegementSlide库实现原理的讲解,有兴趣的同窗,欢迎前往Github地址浏览。github
现在的app中,愈来愈多地采用以下图所示的设计,通常用在诸如『用户主页』、『话题详情页』、『专题详情页』等这些场景。一般,这些场景会带有头部视图(头部视图可能要求支持滚动渐变),下面紧接着的是分页控件,最下面是滚动列表。编程
以下图所示:swift
为了方便下面的说明,在开始以前,先约定几个说法,下面的各类方案,大都离不开在最底层放上一个UIScrollView
(竖直方向滚动),咱们称之为rootScrollView
。不管分页控件下方有多少个子界面,总有一个当前界面,咱们称当前界面下的UIScrollView
(竖直方向滚动)为childScrollView
。缓存
isScrollEnabled
属性这是咱们第一时间能想到的方案,经过给rootScrollView
和childScrollView
实现UIScrollViewDelegate
,并在func scrollViewDidScroll(_ scrollView: UIScrollView)
方法中实时将scrollView.contentOffset.y
与临界值进行对比从而修改二者scrollView
的isScrollEnabled
属性值来达到目的。bash
大体代码以下微信
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == rootScrollView {
if scrollView.contentOffset.y >= headerStickyHeight {
scrollView.contentOffset.y = headerStickyHeight
rootScrollView.isScrollEnabled = false
childScrollView.isScrollEnabled = true
}
} else {
if scrollView.contentOffset.y <= 0 {
scrollView.contentOffset.y = 0
childScrollView.isScrollEnabled = false
rootScrollView.isScrollEnabled = true
}
}
}
复制代码
方法简单,可是有个不太能接受的交互问题,但凡将isScrollEnabled
设置为false
,此次的滑动手势就会被打断,从表现上来看,就是滑动到临界值时滑动会被中断。app
在这篇文章这篇文章中,做者提供了一种利用自定义手势的方式来实现。 可是,只是添加普通的滑动手势是不够的,UIScrollView
是自带阻尼效果的,所以引入了UIDynamicAnimator
来实现阻尼效果。 这是一种不错的思路。不过彻底自定义手势来实现UIScrollView
的效果,须要考虑的细节过多,挺难处理得跟系统的效果一致(写这篇文章的时候,下载了做者提供的源码,commitID
为ff7b76f8468bc87fea8ea6975d8b9fe1173ab031
,在真机iPhone X
上运行,感受仍是有交互上的问题)。此外,由于是自定义手势,手势不是直接做用在UIScrollView
上的,UIScrollView
的ScrollIndicator
是没法显示的,经过改变UIScrollView
的contentOffset
,其ScrollIndicator
也是没法显示的,必需要手势做用在UIScrollView
上才行。使用UIScrollView
的flashScrollIndicators()
来强迫ScrollIndicator
显示出来?...可能还真行,不过我没试过,感受太粗暴了。ide
这应该是目前相对主流的一种实现方式,好比在这篇文章中,即是介绍了这种方式。据我观察Twitter和微博的用户主页多是使用这种方式实现的(写这篇文章的时候,Twitter版本为:7.41.2,微博版本为:9.2.0,推测错了的话还望见谅)工具
该方案的核心为有两点:
rootScrollView
和childScrollView
都能接收到滑动手势(由于手势是做用到UIScrollview
上的,天然是能显示ScrollIndicator
的)。作法是让rootScrollView
实现UIGestureRecognizerDelegate
的代理方法func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
,并在适当的时机返回true
。这部分的代码大体以下:
class SegementSlideScrollView: UIScrollView, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
复制代码
固然只是如此的话,是不够的,这样的结果是滑动的时候,致使rootScrollView
和childScrollView
一块儿滚动。
rootScrollView
滚动,以及什么时候容许childScrollView
。这部分代码大体以下:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == rootScrollView {
if !canParentViewScroll {
rootScrollView.contentOffset.y = headerStickyHeight // point A
canChildViewScroll = true
} else if scrollView.contentOffset.y >= headerStickyHeight {
rootScrollView.contentOffset.y = headerStickyHeight
canParentViewScroll = false
canChildViewScroll = true
}
} else {
if !canChildViewScroll {
childScrollView.contentOffset.y = 0 // point B
} else if scrollView.contentOffset.y <= 0 {
canChildViewScroll = false
canParentViewScroll = true
}
}
}
复制代码
如上代码所示,控制rootScrollView
或者是childScrollView
不可滚动的方式是将二者的contentOffset.y
设置为一个固定值(见注释point A
和point B
),并非简单地将isScrollEnabled
设置false
而已。
没问题了?不,也是有不足之处的: 在第一个界面使用手指向上滑动,让头部视图彻底被隐藏后再向上滑动一些,让childScrollView
的contentOffset.y
处于大于0
的状态,随后,左右切换到第二个界面,使用手指向下滑动,彻底拉出头部视图,而后再切换回第一个界面,这个时候,使用手指在屏幕上稍微滑动一下,rootScrollView
或是childScrollView
的contentOffset.y
会突变,从表现上看,就是发生『位置突变现象』
问题产生的缘由是什么? canParentViewScroll
和childScrollView
始终为一对相反的值,浏览上诉代码,会发如今point A
和point B
处,将rootScrollView
或者是childScrollView
的contentOffset.y
设置为了一个固定值。这样的处理,当始终在同一个界面滑动的时候,不会有问题,可是,在切换界面后,因为rootScrollView
是共用的,在新界面改动了rootScrollView
的contentOffset.y
,切换回原界面后,稍作滑动,定会执行point A
或是point B
其中的一处代码,从而致使『位置突变现象』。
在微博和Twitter中对此问题作了简单的处理。微博上,在切换至新界面以前,将原界面的childScrollView
的contentOffset.y
值重置为了0
。Twitter上,则是在合适的时机作了重置。这也是推测二者多是使用了该方案的缘由。
以下图所示:
SegementSlide是使用 方案III 来实现的。
此外我但愿它还能支持一些别的特性:
rootScrollView
上实现阻尼效果,我但愿也能在childScrollView
上实现,能够选择任意一个阻尼来使用。(有阻尼,就能够配套下拉刷新工具来使用了)navigation
上随着滚动改变背景色、标题、leftItem颜色、rightItem颜色,或是背景色透明之类的),也能够自定义渐变效果对此,大都已经实现:
import SegementSlide
class HomeViewController: SegementSlideViewController {
......
override var headerHeight: CGFloat? {
return view.bounds.height/4
}
override var headerView: UIView? {
return UIView()
}
override var titlesInSwitcher: [String] {
return ["Swift", "Ruby", "Kotlin"]
}
override func segementSlideContentViewController(at index: Int) -> SegementSlideContentScrollViewDelegate? {
return ContentViewController()
}
override func viewDidLoad() {
super.viewDidLoad()
canCacheScrollState = true
reloadData()
scrollToSlide(at: 0, animated: false)
}
}
复制代码
import SegementSlide
class ContentViewController: UITableViewController, SegementSlideContentScrollViewDelegate {
......
@objc var scrollView: UIScrollView {
return tableView
}
}
复制代码
重写SegementSlideViewController
的属性bouncesType
,它是一个枚举类型:
enum BouncesType {
case parent
case child
}
复制代码
默认值为.parent
,以下重写,便可实现『子阻尼』效果:
class HomeViewController: SegementSlideViewController {
......
override var bouncesType: BouncesType {
return .child
}
}
复制代码
如何使得在头部滑动也能实现滚动联动效果? 我在SegementSlideHeaderView
中重写了方法func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
,在合适的状况下返回了childScrollView
。目前这不是一个最优的方法,由于我没可以在这个方法中判断出这个事件是滑动仍是点击事件,这里还能够优化。
既能够支持使用头部视图,也能够不须要头部视图 SegementSlideViewController
是实现这套方案的基类,其中有一个headerView
属性,该属性为可选值,返回nil
则表示不须要头部视图。我在项目配套的Example
工程中,其中的首页即是没有头部视图的示例,不过增长了下拉显示navigation
、上滑隐藏navigation
的效果。通常使用 方案III 的例子,在rootScrollView
上使用了UITableView
,为了使用UITableView
的tableHeaderView
属性,以及吸顶效果。SegementSlide
在v1
版本的时候,使用了UICollectionView
,也是处于一样的目的,现v2
已经改为了UIScrollView
,吸顶效果的话,能够经过增长一条到view.safeAreaLayoutGuide.topAnchor
的约束来实现。
快速应用头部渐变效果? TransparentSlideViewController
是继承于SegementSlideViewController
的子类,其中的headerView
属性已被改为非可选值。其中另外定义了一些属性,用于头部视图处于『显示状态』或是『嵌入状态』时,titleView
和navigationBar
对应属性的改动。
以下所示:
typealias DisplayEmbed<T> = (display: T, embed: T)
override var isTranslucents: DisplayEmbed<Bool> {
return (true, false)
}
override var attributedTexts: DisplayEmbed<NSAttributedString?> {
return (nil, nil)
}
override var barStyles: DisplayEmbed<UIBarStyle> {
return (.black, .default)
}
override var barTintColors: DisplayEmbed<UIColor?> {
return (nil, .white)
}
override var tintColors: DisplayEmbed<UIColor> {
return (.white, .black)
}
复制代码
其中DisplayEmbed
为一个typealias
表示『显示状态』或是『嵌入状态』时的值。
须要注意的是:
TransparentSlideViewController
中的titleView
是使用自定义的方式并赋值给navigationItem.titleView
来实现的,最早考虑的是修改navigationBar
的titleTextAttributes
属性,实践下来,发现会出现titleTextAttributes
已经修改完毕,可是效果没有改变的状况。TransparentSlideViewController
会在viewWillAppear
时保存navigation
上对应样式的状态,并在viewWillDisappear
时进行还原,来保证从一个TransparentSlideViewController
(A)进入到另外一个TransparentSlideViewController
(B)时,navigation
上样式的状态不会有错误,因此也不应在viewDidLoad
时修改navigation
上的样式,由于B
的viewDidLoad
先于A
的viewWillDisappear
执行。若是须要自定义渐变效果,能够模仿TransparentSlideViewController
继承SegementSlideViewController
来实现须要的效果。Example
中使用的是原生的UINavigationController
,和TransparentSlideViewController
配合起来,能够作到还算满意的效果。可是,实际状况下每一个项目中可能会去改动默认的navigation
,若是TransparentSlideViewController
不适用,则须要使用自定义的方式来支持已有项目。
子控件既可结合一块儿使用,也能够单独使用 目前SegementSlideSwitcherView
和SegementSlideContentView
既能够做为SegementSlideViewController
的子控件来使用,也能够单独拿出来使用,Example
工程中的NoticeViewController
即是单独使用的例子,实现了将switcher
放在navigation
上的效果。
红点显示? SegementSlideSwitcherView
支持了红点显示
enum BadgeType {
case none
case point
case count(Int)
}
复制代码
红点类型为枚举值,从上述代码能够看出红点是支持『普通红点显示』还有『带数字红点显示』。
上面在第3点已经提到,『头部滑动也能实现滚动联动效果』目前对此的解决方法不是最优。
方案III 所提到的『位置突变现象』,我在SegementSlideViewController
中提供了canCacheScrollState
属性,值为true
时,在切换界面的时候会缓存当前的canParentViewScroll
、canChildViewScroll
以及rootScrollView
的contentOffset.y
值,并在切换回该界面的时候恢复;值为false
时,即为相似微博的处理,在切换到新界面前将当前界面的childScrollView
的contentOffset.y
值置为0
。设置为true
时会有一个效果,担忧这个效果难以被接受,故将该值的默认值设置为了false
。
效果以下:
但这仍不是一个很好的处理方式。
point A
和point B
处将contentOffset.y
强制设值来阻止滚动,同时也致使了滚动切换时『动能』不足的结果,也就是还不够流畅。天然是要解决上面提到的三点不足的地方,要想让联动完美般流畅,仍是须要使用一个滚动,而不是两个。我在本地开了个v3
分支作了个尝试,在视图顶层覆盖一层透明的UIScrollView
,借用它的手势、它的contentOffset
来控制rootScrollView
和childScrollView
的contentOffset
,能够解决上述提到的三个须要优化的点,可是同时也带来了其余好多问题,这里就不细说了,哪天问题都解决了,更新了v3
版本,再来补充说明吧。
编写本文时,SegementSlide的版本号为2.0-beta-13
。另外,本站还未开通评论功能,如对本文中的内容存在疑问,或者发现文中的不正确之处,欢迎在本文的掘金地址评论区中友善提出。如对本项目有任何疑问,欢迎前往issues提出,同时也欢迎来Pull requests,为本项目作贡献。
『欢迎关注个人我的微信订阅号,我将不按期分享编程相关内容』