嵌套UIScrollview的滑動沖突解決方案

雖然蘋果建議我們不要嵌套 UIScrollview ,但是這話到底是對開發者說的,架不住設計師就是要這么畫設計圖。
我遇到的場景類似這樣,頂部有一個優先展示的封面,當封面滾出屏幕后下面內容的 ScrollView 開始滾動。因為介紹、目錄、評價三個tab是可以左右滾動的,所以具體內容是嵌套在一個 ScrollView 里的。

但是其實面臨的問題都是相似的,嵌套了 ScrollView 后,需要一個條件來控制某個具體時刻哪個 ScrollView 響應滑動事件。

第一步就是通過設置 ScrollView 的 isScrollEnabled 屬性來控制哪個響應。

    @IBOutlet weak var mainScrollView: UIScrollView!
    @IBOutlet weak var subScrollView: UIScrollView!

    let maxOffsetY: CGFloat = 150

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == mainScrollView {
            if scrollView.contentOffset.y >= maxOffsetY {
                mainScrollView.isScrollEnabled = false
                subScrollView.isScrollEnabled = true
            }
        }else {
            // 當 subScrollView 滑動到頂時,停止響應,mainScrollView 開始響應。
            if scrollView.contentOffset.y <= 0 {
                subScrollView.isScrollEnabled = false
                mainScrollView.isScrollEnabled = true
            }
        }
    }

在代理方法<code> scrollViewDidScroll </code>中進行判斷,當mainScrollView 劃出 150 后,停止響應滾動,這時手指再滑動,自然就是展示內容的 subScrollView 響應了。

但是這個簡單的方案在實際操作中有一個很大的弊端。用戶并不總是完美的在滑動了 150 pt后,停下手。然后再開始進行下一次的滑動。如果用戶一次性滾了 160 ,mainScrollView 就會在滾動到 150 后停止響應,此時因為是一次手勢, subScrollView 也不會響應。用戶就會有被中斷的感覺,需要第二次滑動才能繼續滾動。

所以這種簡單的粗暴的方案不能解決問題。畢竟用戶是我們爸爸。

大家都知道每個 ScrollView 都帶了一個 panGestureRecognizer 。此時,我們開始幻想,要是可以自己控制這個 pan 手勢就好了。
于是我們寫下了這行的代碼開始進行嘗試:

        mainScrollView.panGestureRecognizer.delegate = self

大家都知道 iOS 的開發體驗特別好,蘋果的 API 都特別為大家著想。程序 run 起來后 xcode 就會給你一個 crash:

'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'

我來翻譯一下:


解決的思路是我們自己添加一個聯動的 pan 手勢。注意在<code>UIGestureRecognizerDelegate</code>中的<code>shouldRecognizeSimultaneously</code>返回 true。

  let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.panGestureRecognizerAction(recognizer:)))
  pan.delegate = self
  mainScrollView.addGestureRecognizer(pan)

  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
                        shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

這樣設置之后就可以在不影響滾動的情況下獲取 pan 手勢的參數。

接著我們定義兩個 bool 值判斷當前滾動的 ScrollView

    var mainScrollEnabled = false
    var subScrollEnabled = false

在 scrollViewDidScroll 中設置:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == mainScrollView {
            if scrollView.contentOffset.y >= maxOffsetY {
                scrollView.setContentOffset(CGPoint(x: 0, y: maxOffsetY), animated: false)
                mainScrollView.isScrollEnabled = false
                subScrollView.isScrollEnabled = true
                subScrollEnabled = true
                 mainScrollEnabled = false
            }
        }else {
            if scrollView.contentOffset.y <= 0 {
                scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
                subScrollView.isScrollEnabled = false
                mainScrollView.isScrollEnabled = true
                mainScrollEnabled = true
                subScrollEnabled = false
            }
        }
    }

接著我們在 pan 的手勢的 recognizer 中響應處理這種滑動經過臨界區域的情況:

var currentPanY: CGFloat = 0

func panGestureRecognizerAction(recognizer: UIPanGestureRecognizer) {
        if recognizer.state != .changed{
            currentPanY = 0
            // 每次滑動結束都清空狀態
            mainScrollEnabled = false
            subScrollEnabled = false
        }else {
           let currentY = recognizer.translation(in: mainScrollView).y
           // 說明在這次滑動過程中經過了臨界點
            if mainScrollEnabled || subScrollEnabled {
                if currentPanY == 0 {
                    currentPanY = currentY  //記錄下經過臨界點是的 y
                }
                let offsetY = currentPanY - currentY //計算在臨界點后的 offsetY

                if mainScrollEnabled {
                   let supposeY = maxOffsetY + offsetY
                    if supposeY >= 0 {
                        mainScrollView.contentOffset = CGPoint(x: 0, y: supposeY)
                    }else {
                        mainScrollView.contentOffset = CGPoint.zero
                    }
                }else {
                    subScrollView.contentOffset = CGPoint(x: 0, y: offsetY)
                }
            }
        }
    }

這種方案的缺點就是經過臨界點的運動曲線會不自然。原生的滑動有個仿物理的彈性,而我們是直接設置 contentOffset 得到的,是線性的。

補充

iOS 嵌套UIScrollview的滑動沖突另一種解決方案的文章中補充了用 DynamicKit 模仿 ScrollView 的慣性。雖然復雜了一些但是確實效果好多了。

歡迎關注我的微博:@沒故事的卓同學

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容