原文地址: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,爲本項目作貢獻。
『歡迎關注個人我的微信訂閱號,我將不按期分享編程相關內容』