先來放一波效果圖嫌套。逆屡。。踱讨。魏蔗。
在百度地圖搜索POI痹筛,展示POI列表時(shí)莺治,會(huì)有這種效果廓鞠,當(dāng)滑到底部時(shí),地圖會(huì)聯(lián)動(dòng)縮放谣旁,這里先針對列表TableView做一下分析床佳,所以沒有添加這個(gè)效果,但是是小問題榄审,先不用管砌们。
想要做成這種效果,肯定是需要兩層滾動(dòng)scrollView來實(shí)現(xiàn)搁进。底層scrollView來實(shí)現(xiàn)分段效果浪感,內(nèi)層scrollView來實(shí)現(xiàn)列表展示的作用。
這里兩個(gè)豎直滾動(dòng)的視圖饼问,就會(huì)發(fā)生滑動(dòng)沖突影兽。
這里有一些關(guān)鍵點(diǎn):
- 底層的滑動(dòng)View,如何做成上中下三段效果匆瓜,不能用
isPagingEnabled
屬性來實(shí)現(xiàn)赢笨,這里用了scrollViewWillEndDragging
代理加動(dòng)畫來實(shí)現(xiàn)。 - 當(dāng)?shù)讓觭crollView滾動(dòng)至最底部時(shí)驮吱,要求不遮擋地圖的觸摸事件茧妒,可以正常的與地圖進(jìn)行交互。這里用到了響應(yīng)鏈
hitTest:withEvent
方法來解決左冬。 - 雙層滾動(dòng)視圖的滑動(dòng)沖突桐筏。
為了方便,scrollView的分段三個(gè)級別用拇砰,1級梅忌,2級,3級來說明除破。
控件層級介紹:
- 黑色層:當(dāng)為2級時(shí)牧氮,可以點(diǎn)擊button,跳轉(zhuǎn)到3級瑰枫。
- 紅色層:scrollView踱葛,提供分段滑動(dòng)效果。
- 白色層:展示數(shù)據(jù)列表光坝。
分析具體實(shí)現(xiàn)
1尸诽、上中下三段效果實(shí)現(xiàn)
分頁效果首先想到就是isPagingEnabled
屬性,但是他有局限性盯另,不能隨便分頁性含,只能在每個(gè)分頁大小一樣的情況下使用,所以這里不能使用鸳惯。
這里主要用到了scrollView的代理方法商蕴。
//將要結(jié)束手勢拖拽叠萍,開始減速
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
這個(gè)代理是手指離開屏幕開始減速,并且提供了兩個(gè)參數(shù):
- velocity:手指離開屏幕時(shí)的初速度究恤。若不為0俭令,則會(huì)減速到
targetContentOffset
表示的目的地。 - targetContentOffset:指針類型部宿,表示要到達(dá)的目的地。因?yàn)槭侵羔橆愋推芭龋晕覀兛梢孕薷乃闹道碚牛@就是我們可以實(shí)現(xiàn)分段效果的重點(diǎn)。
直接上代碼绵患,看一下這里面的計(jì)算邏輯雾叭。
//這里處理滑動(dòng)手勢結(jié)束后,頁面跳轉(zhuǎn)效果
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//不是底層scrollView落蝙,則退出织狐!
if scrollView == self.tableView {
return
}
let currentOffsetY = scrollView.contentOffset.y
var gotoPointY: CGFloat = 0
//計(jì)算滑動(dòng)手勢結(jié)束后,頁面準(zhǔn)備的位置 ---(這里只分析了速度velocity不為0時(shí)筏勒,當(dāng)速度為0時(shí)移迫,用下面的方式計(jì)算位置)
if currentOffsetY <= contentOffsetMaxY && currentOffsetY > contentOffsetMidY {
if velocity.y > 0 {
gotoPointY = contentOffsetMaxY
}else if velocity.y < 0{
gotoPointY = contentOffsetMidY
}
}else if currentOffsetY < contentOffsetMidY && currentOffsetY > contentOffsetMinY {
if velocity.y > 0 {
gotoPointY = contentOffsetMidY
}else{
gotoPointY = contentOffsetMinY
}
}else{
gotoPointY = contentOffsetMinY
}
//當(dāng)滑動(dòng)速度為0時(shí),判斷當(dāng)前位置距離哪一級別最近
if velocity.y == 0 {
var distance: CGFloat = contentOffsetMaxY
let currentOffsetArray = [contentOffsetMinY, contentOffsetMidY, contentOffsetMaxY]
for item in currentOffsetArray {
let temp = abs(item - currentOffsetY)
if distance > temp {
distance = temp
gotoPointY = item
}
}
}
//動(dòng)畫到指定位置
moveScrollView(scrollView: scrollView, initialSpringVelocity: velocity.y, movePointY: gotoPointY, isAnimate: true)
//這里是重點(diǎn)管行,把計(jì)算好的最后位置給到targetContentOffset
targetContentOffset.pointee = CGPoint(x: 0, y: gotoPointY)
}
計(jì)算好需要的位置坐標(biāo)厨埋,設(shè)置到targetContentOffset
屬性。moveScrollView
添加動(dòng)畫
//動(dòng)畫將scrollView移動(dòng)到指定位置
func moveScrollView(scrollView: UIScrollView, initialSpringVelocity velocity: CGFloat, movePointY: CGFloat, isAnimate: Bool) {
//可以先設(shè)置一下當(dāng)前偏移量捐顷。
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
UIView.animate(withDuration: 0.25, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: velocity, options: UIView.AnimationOptions.curveEaseOut, animations: {
//動(dòng)畫到計(jì)算好的指定偏移量
scrollView.contentOffset.y = movePointY
}, completion: nil)
}
這樣其實(shí)就大體實(shí)現(xiàn)了荡陷,分三段的效果。
2迅涮、視圖顯示到3級時(shí)废赞,如何實(shí)現(xiàn)不遮擋地圖觸摸事件
因?yàn)閟crollView是全屏布局,才可以全屏滾動(dòng)叮姑。這樣不可避免的就會(huì)遮擋住地圖的觸摸事件交互唉地,這跟我們的需求是不符的。所以需要我們來做一個(gè)處理戏溺,這里用到了hitTest:withEvent
方法來解決渣蜗。
- 簡單介紹一下
hitTest:withEvent
方法:
響應(yīng)鏈傳遞的關(guān)鍵方法,當(dāng)手勢交互時(shí)旷祸,會(huì)逐級的用此方法來尋找到要響應(yīng)事件的視圖耕拷。我們可以重寫方法,來進(jìn)行業(yè)務(wù)需求的操作托享。
所以我們可以通過此方法骚烧,來判斷scrollView在什么時(shí)候可以響應(yīng)手勢事件浸赫,什么時(shí)候忽略手勢事件。
創(chuàng)建一個(gè)UIScrollView類赃绊,重寫父類hitTest:withEvent
方法既峡,上代碼:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self {
return nil
}
return hitView
}
解讀一下:
- 從父類方法中,獲取到將要響應(yīng)事件的視圖
hitView
碧查,如果這個(gè)視圖是scrollView
本身运敢,則返回nil
代表這層view不響應(yīng)事件,事件會(huì)繼續(xù)傳遞忠售,就會(huì)傳遞到scrollView
下一層視圖传惠。 - 如果
hitView
不是scrollView
本身,則表示是scrollView
的子視圖響應(yīng)手勢事件稻扬,也就是說手指是觸碰的子View卦方,不是直接觸碰scrollView
,所以正常return這個(gè)子視圖就好了泰佳。
這里的業(yè)務(wù)需求就是手指觸碰白色tableView才可以拖動(dòng)盼砍,觸碰紅色區(qū)域不會(huì)影響下一層的Map地圖交互。(這里因?yàn)槲抑虚g添加了一個(gè)黑色圖層逝她,是想實(shí)現(xiàn)在2級時(shí)浇坐,點(diǎn)擊黑色按鈕,可以到達(dá)3級汽绢,當(dāng)?shù)?級時(shí)吗跋,黑色按鈕設(shè)置isUserInteractionEnabled = false
,不會(huì)影響地圖交互)
3宁昭、解決滑動(dòng)沖突
滑動(dòng)tableView的時(shí)候跌宛,還需要讓scrollView也同時(shí)滾動(dòng),所以首先要讓他們之間的滾動(dòng)互相不受影響积仗。
這里需要用到UIGestureRecognizerDelegate
代理里的方法疆拘。
- 還是需要自定義UITableView類,實(shí)現(xiàn)代理
UIGestureRecognizerDelegate
的方法??
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
返回true
時(shí)寂曹,代表tableView有多個(gè)手勢時(shí)不會(huì)干擾哎迄,這樣就不會(huì)影響到scrollView的滾動(dòng)憔涉。
有很多地方都會(huì)用到孵滞,比如給tableView添加手勢時(shí),返回true
就可以多個(gè)手勢一起觸發(fā)弦牡。
- 不互相干擾了渺氧,但是還是不能實(shí)現(xiàn)效果旨涝,還需要去控制他們什么時(shí)候才可以讓他們滾動(dòng)。
監(jiān)聽滑動(dòng)偏移量侣背,來進(jìn)行判斷控制白华。所以用到scrollView的代理方法:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//這里主要解決滑動(dòng)沖突
if scrollView == self.scrollView {
//這里用 > 就好
if scrollView.contentOffset.y > contentOffsetMaxY {
scrollView.contentOffset.y = contentOffsetMaxY
//設(shè)置標(biāo)記慨默,用來判斷視圖是否可以滾動(dòng)
scrollViewMove = false
tableViewMove = true
}
//當(dāng)scrollView不能滾動(dòng)時(shí),設(shè)置其偏移量
if !scrollViewMove {
scrollView.contentOffset.y = contentOffsetMaxY
}
}else if scrollView == self.tableView {
//同scrollView的滾動(dòng)View類似操作弧腥。
if scrollView.contentOffset.y <= 0 {
scrollView.contentOffset.y = 0
tableViewMove = false
scrollViewMove = true
}
if !tableViewMove {
scrollView.contentOffset.y = 0
}
}
}
添加兩個(gè)標(biāo)記(其實(shí)用一個(gè)標(biāo)記也可以)厦取,來控制視圖是否可以滾動(dòng)。
這樣來控制管搪,就可以解決問題了虾攻。
重要的關(guān)鍵點(diǎn),現(xiàn)在都解決了更鲁,其他一些細(xì)枝末節(jié)就不記錄了台谢。
貼上源碼以供參考:
項(xiàng)目源碼地址