滾動穿透問題探索

貓不群發表於2019-01-24

在移動端開發中,模態(Modal)彈窗可以說是非常之常見了,作為正在看這篇文章的你,可能寫過基於Vue、React或者小程式的統一彈窗Modal元件;可能處理過定寬高或者不定寬高的情況等,看起來一個小小的模態框其實蘊藏了大大的知識點,本文就帶你去探索模態框中的滾動穿透(scroll chaining)問題。

背景

俗話說,產品有三寶:彈窗、浮層加引導,足以見彈窗在產品同學心目中的地位。對任意一個剛入門的前端同學來說,實現一個模態框基本都可以達到信手拈來的地步,但是,當模態框裡邊的內容滾動起來以後,就會出現各種各樣的讓人摸不著頭腦的問題,其中,最出名的想必就是滾動穿透。

那麼,什麼是滾動穿透呢?滾動穿透即:移動端彈出fixed彈窗的話,在彈窗上滑動會導致下層的頁面跟著滾動,這個叫 “滾動穿透”。首先,滾動穿透的現象是出現在移動端的,PC上不存在此種情況。瞭解了什麼是滾動穿透以及它出現的場景,那麼,接下來就直接進入正題。

H5的滾動穿透

首先我們需要明白,滾動穿透出現的前提是模態框裡的內容高度大於元素本身的高度,即滾動時出現。

解決方案一

當我們在模態框內部滾動的時候,底部內容也會跟著滾動,那麼如果禁止掉遮罩層的滾動事件,底部內容也就自然不會滾動了。(本文假設模態框和遮罩層都處在同一級),廢話不多說,翠花,上程式碼。

<div class="popup" v-if="showPopDialog" @click="closePopDialog" @touchmove="touchForbidden"></div>

touchForbidden(e){
    e.preventDefault()
},
複製程式碼

按照上面這樣操作以後,模態框裡邊的內容是正常滾動的,觸控背景也不會隨著滾動,這麼看來,好像我們的問題已經成功的解決了,但是實際情況並沒有這麼樂觀,經過反覆測試,發現當在模態框的頂部或者底部邊緣隨意滑動時,仍然能觸發底部內容的滑動。

滾動穿透問題探索

既然已經發現了觸發的規律,那順著規律去解決問題就好了嘛,順藤摸瓜,當開啟模態框時,我們可以進行邊緣檢測,當使用者手賤滑動到模態框頂部或者模態框最底部時,我們就禁止滑動,這樣應該就可以解決上述問題了,翠花,繼續上程式碼。

<div class="content" v-if="showPopDialog" @touchmove="touchMove" id="canmove" @touchstart="touchStart">
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
</div>
複製程式碼

我們在模態框上首先註冊touchstart事件,得到使用者首次觸控的y座標值

touchStart(e){
     this.firstY = e.targetTouches[0].clientY;
}
複製程式碼

然後,當使用者在模態框滑動的時候,得到其滑動過程中的y座標值,當滑動過程中觸點的clientY > stratY的時候表明滑動方向向下,在使用者往下滑的過程中,滑動距離為0則表明為使用者的滑動位置為模態框的最頂部,此時就得到了使用者滑動到模態框頂部的邊緣條件。

同樣的道理,當clientY < startY時表明滑動方向為向上,scrollTop + offsetHeight >= scrollHeight則表明已經滑動到了模態框最底部。

touchMove(e){
    let target = document.getElementById('canmove')
    let offsetHeight = target.offsetHeight,
      scrollHeight = target.scrollHeight;
    let changedTouches = e.changedTouches;
    let scrollTop = target.scrollTop;
    if (changedTouches.length > 0) {
      let touch = changedTouches[0] || {};
      let moveY = touch.clientY;
      if (moveY > this.firstY && scrollTop === 0) {
        // 滑動到彈窗頂部臨界條件
        e.preventDefault()
        return false
      } else if (moveY < this.firstY && scrollTop + offsetHeight >= scrollHeight) {
        // 滑動到底部臨界條件
        e.preventDefault()
        return false
      }
    }
}
複製程式碼

ok,接下來進行測試,再沒有發現任何問題,完美解決,方案一Done。

在解決有關滑動問題的過程中,總會出現clientHeight、offsetHeight、scrollHeight、scrollTop等不是親兄弟而勝似親兄弟的一系列葫蘆娃,有必要來複習一波關於他們的基礎知識。

  • offsetHeight和元素的滾動沒有任何關係,它只代表元素的高度,包括border、padding、水平滾動條但不包括margin
  • clientHeight類似於offsetHeight,不同的是它只包括padding不包括border、水平滾動條以及margin(引用網際網路例圖)

滾動穿透問題探索

  • scrollHeight只有當元素出現滾動時才有意義,元素不滾動時候,scrollHeight==clientHeight,當元素滾動時,scrollHeight的值為scrollTop + clientHeight

滾動穿透問題探索

  • scrollTop為元素滾動時隱藏的部分
  • offsetTop依然和滾動沒有關係,代表當前元素頂部距離最近父元素頂部的距離

滾動穿透問題探索
在我們處理滾動過程中,基本都會使用到上述變數,圖文結合的方式相信大家更容易理解清楚它們之間的關係。

解決方案二

接下來我們來研究方案二,在開啟彈窗的時候,可以通過為底部內容區域增加動態class來阻止底部內容滑動,但是這樣導致的問題是會丟失底部內容區域的滾動距離,沒事,不慌!在丟失滾動距離之前我們可以將它記錄下來,關閉彈窗的時候再將之前記錄的scrollTop設定回去不就ok了嗎?理論可行,接下來我們就正式開始coding...

當開啟模態框的時候,需要為底部內容區域增加一個動態class來阻止滑動,如下:

.forbidden_scroll{
    position: fixed;
    height: 100%;
  }
複製程式碼

開啟模態框之前,我們需要記錄當前的底部內容的scrollTop值,

touchmove(e){
    this.scrollTop = document.getElementById('scrollElement').scrollTop
}
複製程式碼

當關閉彈窗的時候,首先移除掉剛才新增的動態class,再將scrollTop設定回去即可。

closePopDialog(){
    this.showPopDialog = false
    this.top = -this.scrollTop
    this.showStyle = false
},
複製程式碼

整個模板部分程式碼如下:

<div class="main">
    <div :class="showStyle ? 'forbidden_open' : 'article'" id="scrollElement" :style="{'margin-top': top + 'px'}" @touchmove="touchmove">
      <div class="block_red">
        <div class="block_click" @click="openPopDialog">Click Me</div>
      </div>
      <div class="block_yellow"></div>
      <div class="block_green"></div>
    </div>
    <div class="popup" v-if="showPopDialog" @click="closePopDialog" @touchmove="touchForbidden">
    </div>
    <div class="content" v-if="showPopDialog">
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
      我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的我是來進行測試的
    </div>
  </div>
複製程式碼

經過測試,發現此方案也可以解決滾動穿透的問題,但是,手賤的我又一次嘗試了滑動到彈窗邊緣的情況,然鵝,還是觸發了底部內容的滑動,沒辦法,依然需要像方案一一樣進行邊緣檢測,才能完美解決問題。

上述討論的2種解決方案都是基於H5的,小程式中對於滾動穿透仍然沒有完美解決方案,根本原因在於小程式中沒有DOM的概念,不能像H5中可以任意操作DOM。

其他解決方案缺陷
  • @touchmove.prevent 無法解決滑動到邊緣又觸發底部滑動的問題
  • overscroll-behavior依然無法解決上述問題,並且相容性成迷caniuse.com/#search=ove…

上述討論都是基於H5的,在小程式中,可以使用scroll-view結合上述方案解決此類問題,但是仍然會存在滑動到邊緣觸發底部滾動問題,目前沒有完美的解決方案。

相關文章