簡(jiǎn)書(shū)編輯器目測(cè)不支持html
的一些語(yǔ)法庐船,導(dǎo)致部分圖片顯示有問(wèn)題淳附,對(duì)于圖片又最大只支持5M畅哑,建議前往原文地址或者是掘金地址閱讀。
本文旨在對(duì)于SegementSlide庫(kù)實(shí)現(xiàn)原理的講解鸟妙,有興趣的同學(xué),歡迎前往Github地址瀏覽挥吵。
[圖片上傳失敗...(image-bd1108-1550054195196)]
背景
如今的app中重父,越來(lái)越多地采用如下圖所示的設(shè)計(jì),一般用在諸如『用戶主頁(yè)』蔫劣、『話題詳情頁(yè)』坪郭、『專題詳情頁(yè)』等這些場(chǎng)景。通常脉幢,這些場(chǎng)景會(huì)帶有頭部視圖(頭部視圖可能要求支持滾動(dòng)漸變)歪沃,下面緊接著的是分頁(yè)控件,最下面是滾動(dòng)列表嫌松。
如下圖所示:
<p align="center">
<img src="https://github.com/Jiar/ImageHosting/blob/master/jiar.me/iOS/Multi-tier%20UIScrollView%20nested%20scrolling%20solution/transparent.gif?raw=true">
</p>
各種方案以及優(yōu)缺點(diǎn)
為了方便下面的說(shuō)明沪曙,在開(kāi)始之前,先約定幾個(gè)說(shuō)法萎羔,下面的各種方案液走,大都離不開(kāi)在最底層放上一個(gè)UIScrollView
(豎直方向滾動(dòng)),我們稱之為rootScrollView
。無(wú)論分頁(yè)控件下方有多少個(gè)子界面缘眶,總有一個(gè)當(dāng)前界面嘱根,我們稱當(dāng)前界面下的UIScrollView
(豎直方向滾動(dòng))為childScrollView
。
I 控制isScrollEnabled
屬性
這是我們第一時(shí)間能想到的方案巷懈,通過(guò)給rootScrollView
和childScrollView
實(shí)現(xiàn)UIScrollViewDelegate
该抒,并在func scrollViewDidScroll(_ scrollView: UIScrollView)
方法中實(shí)時(shí)將scrollView.contentOffset.y
與臨界值進(jìn)行對(duì)比從而修改兩者scrollView
的isScrollEnabled
屬性值來(lái)達(dá)到目的。
大致代碼如下
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
}
}
}
方法簡(jiǎn)單顶燕,但是有個(gè)不太能接受的交互問(wèn)題凑保,但凡將isScrollEnabled
設(shè)置為false
,這次的滑動(dòng)手勢(shì)就會(huì)被打斷涌攻,從表現(xiàn)上來(lái)看欧引,就是滑動(dòng)到臨界值時(shí)滑動(dòng)會(huì)被中斷。
II 自定義滑動(dòng)手勢(shì)
在這篇文章這篇文章中恳谎,作者提供了一種利用自定義手勢(shì)的方式來(lái)實(shí)現(xiàn)芝此。
但是,只是添加普通的滑動(dòng)手勢(shì)是不夠的惠爽,UIScrollView
是自帶阻尼效果的癌蓖,因此引入了UIDynamicAnimator
來(lái)實(shí)現(xiàn)阻尼效果。
這是一種不錯(cuò)的思路婚肆。不過(guò)完全自定義手勢(shì)來(lái)實(shí)現(xiàn)UIScrollView
的效果租副,需要考慮的細(xì)節(jié)過(guò)多,挺難處理得跟系統(tǒng)的效果一致(寫(xiě)這篇文章的時(shí)候较性,下載了作者提供的源碼用僧,commitID
為ff7b76f8468bc87fea8ea6975d8b9fe1173ab031
,在真機(jī)iPhone X
上運(yùn)行赞咙,感覺(jué)還是有交互上的問(wèn)題)责循。此外,因?yàn)槭亲远x手勢(shì)攀操,手勢(shì)不是直接作用在UIScrollView
上的院仿,UIScrollView
的ScrollIndicator
是無(wú)法顯示的,通過(guò)改變UIScrollView
的contentOffset
速和,其ScrollIndicator
也是無(wú)法顯示的歹垫,必須要手勢(shì)作用在UIScrollView
上才行。使用UIScrollView
的flashScrollIndicators()
來(lái)強(qiáng)迫ScrollIndicator
顯示出來(lái)颠放?...可能還真行排惨,不過(guò)我沒(méi)試過(guò),感覺(jué)太粗暴了碰凶。
III 手勢(shì)穿透
這應(yīng)該是目前相對(duì)主流的一種實(shí)現(xiàn)方式暮芭,比如在這篇文章中鹿驼,便是介紹了這種方式。據(jù)我觀察Twitter和微博的用戶主頁(yè)可能是使用這種方式實(shí)現(xiàn)的(寫(xiě)這篇文章的時(shí)候辕宏,Twitter版本為:7.41.2畜晰,微博版本為:9.2.0,推測(cè)錯(cuò)了的話還望見(jiàn)諒)
該方案的核心為有兩點(diǎn):
- 讓滑動(dòng)手勢(shì)穿透使得
rootScrollView
和childScrollView
都能接收到滑動(dòng)手勢(shì)(因?yàn)槭謩?shì)是作用到UIScrollview
上的瑞筐,自然是能顯示ScrollIndicator
的)舷蟀。做法是讓rootScrollView
實(shí)現(xiàn)UIGestureRecognizerDelegate
的代理方法func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
,并在適當(dāng)?shù)臅r(shí)機(jī)返回true
面哼。
這部分的代碼大致如下:
class SegementSlideScrollView: UIScrollView, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
當(dāng)然只是如此的話,是不夠的扫步,這樣的結(jié)果是滑動(dòng)的時(shí)候魔策,導(dǎo)致rootScrollView
和childScrollView
一起滾動(dòng)。
- 增加兩個(gè)標(biāo)志位來(lái)控制何時(shí)允許
rootScrollView
滾動(dòng)河胎,以及何時(shí)允許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
不可滾動(dòng)的方式是將兩者的contentOffset.y
設(shè)置為一個(gè)固定值(見(jiàn)注釋point A
和point B
)游岳,并不是簡(jiǎn)單地將isScrollEnabled
設(shè)置false
而已政敢。
沒(méi)問(wèn)題了?不胚迫,也是有不足之處的:
在第一個(gè)界面使用手指向上滑動(dòng)喷户,讓頭部視圖完全被隱藏后再向上滑動(dòng)一些,讓childScrollView
的contentOffset.y
處于大于0
的狀態(tài)访锻,隨后褪尝,左右切換到第二個(gè)界面,使用手指向下滑動(dòng)期犬,完全拉出頭部視圖河哑,然后再切換回第一個(gè)界面,這個(gè)時(shí)候龟虎,使用手指在屏幕上稍微滑動(dòng)一下璃谨,rootScrollView
或是childScrollView
的contentOffset.y
會(huì)突變,從表現(xiàn)上看鲤妥,就是發(fā)生『位置突變現(xiàn)象』
問(wèn)題產(chǎn)生的原因是什么佳吞?
canParentViewScroll
和childScrollView
始終為一對(duì)相反的值,瀏覽上訴代碼旭斥,會(huì)發(fā)現(xiàn)在point A
和point B
處容达,將rootScrollView
或者是childScrollView
的contentOffset.y
設(shè)置為了一個(gè)固定值。這樣的處理垂券,當(dāng)始終在同一個(gè)界面滑動(dòng)的時(shí)候花盐,不會(huì)有問(wèn)題羡滑,但是,在切換界面后算芯,由于rootScrollView
是共用的柒昏,在新界面改動(dòng)了rootScrollView
的contentOffset.y
,切換回原界面后熙揍,稍做滑動(dòng)职祷,定會(huì)執(zhí)行point A
或是point B
其中的一處代碼,從而導(dǎo)致『位置突變現(xiàn)象』届囚。
在微博和Twitter中對(duì)此問(wèn)題做了簡(jiǎn)單的處理有梆。微博上,在切換至新界面之前意系,將原界面的childScrollView
的contentOffset.y
值重置為了0
泥耀。Twitter上,則是在合適的時(shí)機(jī)做了重置蛔添。這也是推測(cè)兩者可能是使用了該方案的原因痰催。
如下圖所示:
<p align="center">
<div style="display:flex">
<img style="flex-grow:1" src="https://github.com/Jiar/ImageHosting/blob/master/jiar.me/iOS/Multi-tier%20UIScrollView%20nested%20scrolling%20solution/weibo.gif?raw=true" width="49%">
<img style="flex-grow:1" src="https://github.com/Jiar/ImageHosting/blob/master/jiar.me/iOS/Multi-tier%20UIScrollView%20nested%20scrolling%20solution/twitter.gif?raw=true" width="49%">
</div>
</p>
SegementSlide的需求
SegementSlide是使用 方案III 來(lái)實(shí)現(xiàn)的。
此外我希望它還能支持一些別的特性:
- 簡(jiǎn)單易用的接口
- 一般使用 方案III 實(shí)現(xiàn)的例子迎瞧,大都只是支持在
rootScrollView
上實(shí)現(xiàn)阻尼效果夸溶,我希望也能在childScrollView
上實(shí)現(xiàn),可以選擇任意一個(gè)阻尼來(lái)使用凶硅。(有阻尼缝裁,就可以配套下拉刷新工具來(lái)使用了) - 一般使用 方案III 實(shí)現(xiàn)的例子,大都是需要手指在子視圖部分滑動(dòng)才能實(shí)現(xiàn)聯(lián)動(dòng)足绅,希望也能在頭部滑動(dòng)實(shí)現(xiàn)聯(lián)動(dòng)
- 既可以支持使用頭部視圖压语,也可以不需要頭部視圖
- 頭部視圖可以使用簡(jiǎn)單的接口實(shí)現(xiàn)滾動(dòng)漸變效果(
navigation
上隨著滾動(dòng)改變背景色、標(biāo)題编检、leftItem顏色胎食、rightItem顏色,或是背景色透明之類的)允懂,也可以自定義漸變效果 - 子控件既可結(jié)合一起使用厕怜,也可以單獨(dú)使用
- 分頁(yè)標(biāo)題旁可以顯示紅點(diǎn)
...
對(duì)此,大都已經(jīng)實(shí)現(xiàn):
- 看下如下示例代碼蕾总,是否還算簡(jiǎn)單易用:
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
}
}
- 已經(jīng)能否支持“父阻尼”和“子阻尼”效果了
重寫(xiě)SegementSlideViewController
的屬性bouncesType
粥航,它是一個(gè)枚舉類型:
enum BouncesType {
case parent
case child
}
默認(rèn)值為.parent
,如下重寫(xiě)生百,即可實(shí)現(xiàn)『子阻尼』效果:
class HomeViewController: SegementSlideViewController {
......
override var bouncesType: BouncesType {
return .child
}
}
如何使得在頭部滑動(dòng)也能實(shí)現(xiàn)滾動(dòng)聯(lián)動(dòng)效果递雀?
我在SegementSlideHeaderView
中重寫(xiě)了方法func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
,在合適的情況下返回了childScrollView
蚀浆。目前這不是一個(gè)最優(yōu)的方法缀程,因?yàn)槲覜](méi)能夠在這個(gè)方法中判斷出這個(gè)事件是滑動(dòng)還是點(diǎn)擊事件搜吧,這里還可以優(yōu)化。既可以支持使用頭部視圖杨凑,也可以不需要頭部視圖
SegementSlideViewController
是實(shí)現(xiàn)這套方案的基類滤奈,其中有一個(gè)headerView
屬性,該屬性為可選值撩满,返回nil
則表示不需要頭部視圖蜒程。我在項(xiàng)目配套的Example
工程中,其中的首頁(yè)便是沒(méi)有頭部視圖的示例伺帘,不過(guò)增加了下拉顯示navigation
昭躺、上滑隱藏navigation
的效果。一般使用 方案III 的例子伪嫁,在rootScrollView
上使用了UITableView
窍仰,為了使用UITableView
的tableHeaderView
屬性,以及吸頂效果礼殊。SegementSlide
在v1
版本的時(shí)候,使用了UICollectionView
针史,也是處于同樣的目的晶伦,現(xiàn)v2
已經(jīng)改成了UIScrollView
,吸頂效果的話啄枕,可以通過(guò)增加一條到view.safeAreaLayoutGuide.topAnchor
的約束來(lái)實(shí)現(xiàn)婚陪。快速應(yīng)用頭部漸變效果?
TransparentSlideViewController
是繼承于SegementSlideViewController
的子類频祝,其中的headerView
屬性已被改成非可選值泌参。其中另外定義了一些屬性,用于頭部視圖處于『顯示狀態(tài)』或是『嵌入狀態(tài)』時(shí)常空,titleView
和navigationBar
對(duì)應(yīng)屬性的改動(dòng)沽一。
如下所示:
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
為一個(gè)typealias
表示『顯示狀態(tài)』或是『嵌入狀態(tài)』時(shí)的值。
需要注意的是:
-
TransparentSlideViewController
中的titleView
是使用自定義的方式并賦值給navigationItem.titleView
來(lái)實(shí)現(xiàn)的漓糙,最先考慮的是修改navigationBar
的titleTextAttributes
屬性铣缠,實(shí)踐下來(lái),發(fā)現(xiàn)會(huì)出現(xiàn)titleTextAttributes
已經(jīng)修改完畢昆禽,但是效果沒(méi)有改變的情況蝗蛙。 -
TransparentSlideViewController
會(huì)在viewWillAppear
時(shí)保存navigation
上對(duì)應(yīng)樣式的狀態(tài),并在viewWillDisappear
時(shí)進(jìn)行還原醉鳖,來(lái)保證從一個(gè)TransparentSlideViewController
(A)進(jìn)入到另一個(gè)TransparentSlideViewController
(B)時(shí)捡硅,navigation
上樣式的狀態(tài)不會(huì)有錯(cuò)誤,所以也不該在viewDidLoad
時(shí)修改navigation
上的樣式盗棵,因?yàn)?code>B的viewDidLoad
先于A
的viewWillDisappear
執(zhí)行壮韭。
如果需要自定義漸變效果北发,可以模仿TransparentSlideViewController
繼承SegementSlideViewController
來(lái)實(shí)現(xiàn)需要的效果。Example
中使用的是原生的UINavigationController
泰涂,和TransparentSlideViewController
配合起來(lái)鲫竞,可以做到還算滿意的效果。但是逼蒙,實(shí)際情況下每個(gè)項(xiàng)目中可能會(huì)去改動(dòng)默認(rèn)的navigation
从绘,如果TransparentSlideViewController
不適用,則需要使用自定義的方式來(lái)支持已有項(xiàng)目是牢。
子控件既可結(jié)合一起使用僵井,也可以單獨(dú)使用
目前SegementSlideSwitcherView
和SegementSlideContentView
既可以作為SegementSlideViewController
的子控件來(lái)使用,也可以單獨(dú)拿出來(lái)使用驳棱,Example
工程中的NoticeViewController
便是單獨(dú)使用的例子批什,實(shí)現(xiàn)了將switcher
放在navigation
上的效果。紅點(diǎn)顯示社搅?
SegementSlideSwitcherView
支持了紅點(diǎn)顯示
enum BadgeType {
case none
case point
case count(Int)
}
紅點(diǎn)類型為枚舉值驻债,從上述代碼可以看出紅點(diǎn)是支持『普通紅點(diǎn)顯示』還有『帶數(shù)字紅點(diǎn)顯示』。
還需要優(yōu)化的點(diǎn)
上面在第3點(diǎn)已經(jīng)提到形葬,『頭部滑動(dòng)也能實(shí)現(xiàn)滾動(dòng)聯(lián)動(dòng)效果』目前對(duì)此的解決方法不是最優(yōu)合呐。
方案III 所提到的『位置突變現(xiàn)象』,我在
SegementSlideViewController
中提供了canCacheScrollState
屬性笙以,值為true
時(shí)淌实,在切換界面的時(shí)候會(huì)緩存當(dāng)前的canParentViewScroll
、canChildViewScroll
以及rootScrollView
的contentOffset.y
值猖腕,并在切換回該界面的時(shí)候恢復(fù)拆祈;值為false
時(shí),即為類似微博的處理倘感,在切換到新界面前將當(dāng)前界面的childScrollView
的contentOffset.y
值置為0
放坏。設(shè)置為true
時(shí)會(huì)有一個(gè)效果,擔(dān)心這個(gè)效果難以被接受老玛,故將該值的默認(rèn)值設(shè)置為了false
轻姿。
效果如下:
<p align="center">
<img src="https://github.com/Jiar/ImageHosting/blob/master/jiar.me/iOS/Multi-tier%20UIScrollView%20nested%20scrolling%20solution/canCacheScrollState.gif?raw=true">
</p>
但這仍不是一個(gè)很好的處理方式。
- 聯(lián)動(dòng)滾動(dòng)切換的時(shí)候逻炊,還沒(méi)有達(dá)到完美的流暢效果互亮。由于
point A
和point B
處將contentOffset.y
強(qiáng)制設(shè)值來(lái)阻止?jié)L動(dòng),同時(shí)也導(dǎo)致了滾動(dòng)切換時(shí)『動(dòng)能』不足的結(jié)果余素,也就是還不夠流暢豹休。
接下去要做的事
自然是要解決上面提到的三點(diǎn)不足的地方,要想讓聯(lián)動(dòng)完美般流暢桨吊,還是需要使用一個(gè)滾動(dòng)威根,而不是兩個(gè)凤巨。我在本地開(kāi)了個(gè)v3
分支做了個(gè)嘗試,在視圖頂層覆蓋一層透明的UIScrollView
洛搀,借用它的手勢(shì)敢茁、它的contentOffset
來(lái)控制rootScrollView
和childScrollView
的contentOffset
,可以解決上述提到的三個(gè)需要優(yōu)化的點(diǎn)留美,但是同時(shí)也帶來(lái)了其他好多問(wèn)題彰檬,這里就不細(xì)說(shuō)了,哪天問(wèn)題都解決了谎砾,更新了v3
版本逢倍,再來(lái)補(bǔ)充說(shuō)明吧。
參考
結(jié)束語(yǔ)
編寫(xiě)本文時(shí)景图,SegementSlide的版本號(hào)為2.0-beta-13
较雕。另外,本站還未開(kāi)通評(píng)論功能挚币,如對(duì)本文中的內(nèi)容存在疑問(wèn)亮蒋,或者發(fā)現(xiàn)文中的不正確之處,歡迎在本文的掘金地址評(píng)論區(qū)中友善提出妆毕。如對(duì)本項(xiàng)目有任何疑問(wèn)慎玖,歡迎前往issues提出,同時(shí)也歡迎來(lái)Pull requests设塔,為本項(xiàng)目做貢獻(xiàn)。
<p align="center">
『歡迎關(guān)注我的個(gè)人微信訂閱號(hào)远舅,我將不定期分享編程相關(guān)內(nèi)容』
</p>
<p align="center">
<img src="https://github.com/Jiar/ImageHosting/blob/master/jiar.me/Dingyuehao.jpg?raw=true" title="Jiar's 微信訂閱號(hào)" style="width:200px;height:200px" />
</p>