整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「Vue項目」中的滾動組件&聯動效果從0到1(建議收藏)


          最近的一個項目做的是vue組件中的一個應用,「處理滾動列表」,這個應該是很常見的需求了,在項目中遇到的痛點,難點,如何一步步解決的,以及小細節一些優化。

          借鑒某課的思路,仿QQ音樂效果,記錄一下,自己字母解決這個難題,分享給你們,「希望對你們做移動端滾動列表問題有所幫助」


          GitHub倉庫

          效果

          處理滾動列表最終效果

          從最終效果來看,實現了三個我的難點

          • 第一個就是右側快速入口,點擊一個字母跳轉到相應的部分
          • 左上角的那個title是跟隨字母一起修改內容的
          • 向下滾動的話,會隨時刷新右側快速路口以及左上角字母title

          接下來就是一步步去實現,優化上面的效果


          第三方庫介紹

          better-scroll 移動端滾動的解決方案

          vue-lazyload 圖片懶加載

          基本上實現上面的效果就是基于這兩個第三方庫

          「better-scroll基本使用」

          經常會遇到的問題就是初始化了,「還是不能滾動」。那么對于這個而言,我最近用到一些經驗是什么呢?

          我們先看常見的html結果

          <div class="wrapper">
            <ul class="content">
              <li>...</li>
              <li>...</li>
              ...
            </ul>
            <!-- you can put some other DOMs here, it won't affect the scrolling
          </div>
          復制代碼

          滾動的原理是什么

          better-scroll原理說明

          wrapper是父容器,它一定要有「固定高度」,content是內容區域,它是父元素的第一個元素,它content會隨著內容的大小撐開而撐高,只有這個高度大于wrapper父容器高度時,才會出現滾動,也就是它的原理。

          那么我們怎么去初始化呢

          import BScroll from '@better-scroll/core'
          let wrapper = document.querySelector('.wrapper')
          let scroll = new BScroll(wrapper,{})
          //{}配置一些信息
          復制代碼

          點這里有文檔

          接下來就開始把


          從0到1完成

          scroll組件

          這個scroll組件是子組件,也可以算是個base組件,完成日常滾動的效果

          <template>
            <div ref="wrapper">
              <slot></slot>
            </div>
          </template>
          
          <script type="text/ecmascript-6">
            import BScroll from 'better-scroll'
          
            export default {
              props: {
                probeType: {
                  type: Number,
                  default: 1
                },
                click: {
                  type: Boolean,
                  default: true
                },
                listenScroll: {
                  type: Boolean,
                  default: false
                },
                data: {
                  type: Array,
                  default: null
                },
                pullup: {
                  type: Boolean,
                  default: false
                },
                beforeScroll: {
                  type: Boolean,
                  default: false
                },
                refreshDelay: {
                  type: Number,
                  default: 20
                }
              },
              mounted() {
                setTimeout(() => {
                  this._initScroll()
                }, 20)
              },
              methods: {
                // 初始化Scroll
                _initScroll() {
                  // 判斷是否初始化
                  if (!this.$refs.wrapper) {
                    return
                  }
                  // 調用Scroll實例,表現可以滑動
                  this.scroll = new BScroll(this.$refs.wrapper, {
                    probeType: this.probeType,
                    click: this.click
                  })
                  if (this.listenScroll) {
                    let me = this
                    this.scroll.on('scroll', (pos) => {
                      me.$emit('scroll', pos)
                    })
                  }
                  if (this.pullup) {
                    this.scroll.on('scrollEnd', () => {
                      if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
                        this.$emit('scrollToEnd')
                      }
                    })
                  }
                  if (this.beforeScroll) {
                    this.scroll.on('beforeScrollStart', () => {
                      this.$emit('beforeScroll')
                    })
                  }
                },
                disable() {
                  this.scroll && this.scroll.disable()
                },
                enable() {
                  this.scroll && this.scroll.enable()
                },
                refresh() {  // 刷新scroll,重新計算高度
                  this.scroll && this.scroll.refresh()
                },
                scrollTo() {
                  this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
                },
                scrollToElement() {
                  this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
                }
              },
              watch: {
                // 監聽到數據的變化,就會重新去refresh數據,重新去計算響應的數據
                data() {
                  setTimeout(() => {
                    this.refresh()
                  }, this.refreshDelay)
                }
              }
            }
          </script>
          <style scoped lang="stylus" rel="stylesheet/stylus">
          </style>
          復制代碼

          在父組件中導入即可

          完成列表滾動

          listview組件導入

          <template>
            <scroll
              :listen-scroll="listenScroll"
              :probe-type="probeType"
              :data="data"
              class="listview"
              ref="listview"
            >
              <ul>
                <li v-for="(group,index) in data" class="list-group" ref="listGroup" :key="index">
                  <h2 class="list-group-title">{{group.title}}</h2>
                  <uL>
                    <li
                      @click="selectItem(item)"
                      v-for="(item, index) in group.items"
                      class="list-group-item"
                      :key="index"
                    >
                      <img class="avatar" v-lazy="item.avatar" />
                      <span class="name">{{item.name}}</span>
                    </li>
                  </uL>
                </li>
              </ul>
            </scroll>
          </template>
          復制代碼

          然后導入scroll組件即可,看看效果

          處理滾動列表-實現列表滾動

          上面在listview組件中導入scroll組件,完成基本的列表滾動效果,接下來,完善一步一步效果吧。

          右側快速入口

          <div
                class="list-shortcut"
                @touchstart="onShortcutTouchStart"
                @touchmove.stop.prevent="onShortcutTouchMove"
              >
              <!-- data-index方便獲取一個列表中的index -->
                <ul>
                  <li
                    v-for="(item, index) in shortcutList"
                    :data-index="index"
                    class="item"
                    :class="{'current':currentIndex===index}"
                    :key="index"
                  >{{item}}</li>
                </ul>
              </div>
          復制代碼

          點擊右側快速路口的話,會跳轉到相應的title去,使用的方法就是

          scrollElement

          scrollToElement(el, time, offsetX, offsetY, easing)

          「這個方法很方便的解決了我們第一個難點」,現在就差獲取右側快速路口的索引值了

          給每一個li增加一個data-index屬性名稱,值為index下

          :data-index="index"
          復制代碼

          這樣子每次就可以獲取當前的索引值

          有了索引值,我們就可以直接調用srcollToElement()方法,完成左側的跳轉效果。

          this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0);
          // 這個index就是獲取到下標索引值,然后通過這個
          // 這個第二個參數是滾動的動畫的時間,我們默認為0就行,文檔上面也有專門的說明,可以去看看。
          復制代碼

          我們看看效果吧下

          處理滾動列表-實現點擊右側跳轉相應位置

          接下來完成「touchMove事件」,我們綁定到div上

          @touchmove.stop.prevent="onShortcutTouchMove"
          // 兩個修飾符阻止冒泡以及默認的事件
          復制代碼

          思路

          • 首先要監聽touchStart事件一開始錨點,也就是anchorIndex,還有保存e.touches[0].pageY, y軸上的位置信息,記作y1
          • 監聽touchuMove事件,保存y軸距離,記為y2,這個時候y2-y1就是y軸上的距離變化dataChange
          • 將這個距離dataChange除以高度,這里的高度,我選擇的是每個li的content+padding高度,這個高度的話,正好是整個一個li元素高度,我覺得很合理,delta = dataChange/ANCHOR_HEIGHT
          • 最后一開始的anchorIndex加上delta,就是最新的錨點,這個anchorIndex一定要取證,因為獲取的可能是字符串。

          看代碼

           onShortcutTouchStart(e) {
                  // 獲取到右側的列表索引值
                let anchorIndex = getData(e.target, "index");
                let firstTouch = e.touches[0];
                this.touch.y1 = firstTouch.pageY;   // 計入一開始y軸上的位置
                this.touch.anchorIndex = anchorIndex;   // 保存了每次點擊的錨點
                this._scrollTo(anchorIndex);
              },
              // 監聽的是TouchMove事件
              onShortcutTouchMove(e) {
                let firstTouch = e.touches[0];
                this.touch.y2 = firstTouch.pageY;
                // 滾動的兩個差值 也就是y軸上的偏移
                // 除以每個高度,這樣子的話,就知道偏移了幾個錨點
                let delta = ((this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT) | 0;
                let anchorIndex = parseInt(this.touch.anchorIndex) + delta;
          
                this._scrollTo(anchorIndex);
              },
              
          復制代碼

          效果怎么樣呢,基本上點擊和手勢移動都較為完美的實現了。

          處理滾動列表-實現手勢移動右側跳轉相應位置

          左右聯動

          左右聯動的效果指的是左側點擊到某個區域,緊接著右側快速路口也跳轉到相應位置,這里其實指的就是高亮效果。

          效果就是滑動列表,右側的字母會相應的高亮,達到同步的作用,難點是什么呢?

          ListGroup計算高度

          從圖片上面看,我們發現每個listGroup分組里面的成員是不固定的,所以我們怎么去獲取到相應的currentIndex呢?

          「我們可以獲取到每次滾動的距離,那怎么樣去獲取相應的currentIndex呢,比如滑到K分組時,currentIndex是對應的下標?」

          有個不錯的思路:

          • 我們去維護一個height[i]數組,該數組含義就是第i個分組的范圍是height[i]~~heigth[i+1]
          • 那么我們獲取到滾動Y軸的距離,那么就可以確定它所在的范圍,如果滾動的距離在posY>height[i]&&posY<height[i+1],那么currentIndex就可以取值i,這樣子好像行。

          那么我們按照上面的思路來完善吧

           _calculateHeight() {
                // 這個方法就是計算每個listGroup高度
                this.listHeight = [];
                const list = this.$refs.listGroup;
                let height = 0;
                this.listHeight.push(height);
                for (let i = 0; i < list.length; i++) {
                  let item = list[i];
                  height += item.clientHeight;
                  this.listHeight.push(height);
                }
              },
          復制代碼

          這個listHeigth數據就是我們維護的第i個分組的clientHeight距離

          第二步,我們監控這個scrollY,這個變量表示的就是滾動的距離

          watch: {
              // 每次去watch這個滾動的距離,
              scrollY(newY) {
                const listHeight = this.listHeight;
                // 當滾動到頂部,newY>0
                if (newY > 0) {
                  this.currentIndex = 0;
                  return;
                }
                // 在中間部分滾動
                for (let i = 0; i < listHeight.length - 1; i++) {
                  let height1 = listHeight[i];
                  let height2 = listHeight[i + 1];
                  if (-newY >= height1 && -newY < height2) {
                    this.currentIndex = i;
                    this.diff = height2 + newY;
                    return;
                  }
                }
                // 當滾動到底部,且-newY大于最后一個元素的上限
                this.currentIndex = listHeight.length - 2;
              },
          復制代碼

          這里需要提醒的就是,我們怎么去拿到這個scrollY滾動距離呢?

          說到這個,我們得看到scroll組件中,閱讀它的API,會發現它提供了on方法,該方法可以去監聽該「實例的鉤子函數」,所以我們去「監聽鉤子函數scroll」

          「scroll鉤子函數」

          • 參數:{Object} {x, y} 滾動的實時坐標
          • 觸發時機:滾動過程中。

          所以我們可以通過這個鉤子來獲取滾動的實時坐標

          if (this.listenScroll) {
                    let me = this
                    this.scroll.on('scroll', (pos) => {
                      // me指的就是實例
                      // 通過監聽scroll事件,有一個回調,pos是一個對象,有x,y軸的具體距離
                      // 去派發一個scroll事件,這樣子外部也就是父組件可以拿到我們的pos
                      me.$emit('scroll', pos)
                    })
                  }
          復制代碼

          這樣子我們在子組件scroll中向外派發一個scroll事件,并且把「pos = {Object} {x, y} 滾動的實時坐標」向外傳遞,這樣子的話,父組件通過@scroll="scroll" 就可以拿到這個坐標pos

          這樣子我們這個難點就解決了。

          我們來看看效果

          這樣子基本上問題就解決了,但是呢還會遇到一個問題?


          probeType

          • 類型:Number
          • 默認值:0
          • 可選值:1、2、3
          • 作用:有時候我們需要知道滾動的位置。當 probeType 為 1 的時候,會非實時(屏幕滑動超過一定時間后)派發scroll 事件;當 probeType 為 2 的時候,會在屏幕滑動的過程中實時的派發 scroll 事件;當 probeType 為 3 的時候,不僅在屏幕滑動的過程中,而且在 momentum 滾動動畫運行過程中實時派發 scroll 事件。如果沒有設置該值,其默認值為 0,即不派發 scroll 事件。

          這個是文檔上面的內容,我們可以看到這個配置項還是很重要的,我們listview組件需要通過props向子組件傳遞probeType值,值為3,這樣子就可以「在滾動中實時派發 scroll 事件」

           <scroll :probe-type='3'></scroll>
           // 當然了,這個probeTyep會在data中拿到
          復制代碼

          總結

          • 解決難點一,獲取滾動的實時位置,通過子組件scroll實例on方法,去「監聽鉤子函數scroll」,然后向外去派發一個scroll函數,并且把滾動的距離傳給父組件listview
          • 解決難點二,獲取到滾動Y軸的距離,就可以進一步去判斷,它是落在哪一個listGroup中,也就是哪一個分組中,這樣子就確定currentIndex。
          • 通過watch監聽scrollY值,表示Y軸滾動距離,發生變化時,更新currentIndex。
          • 有了currentIndex,在判斷currentIndex === index,就可以實現高亮效果
          • 還有一些BetterScroll 提供的API 比如refresh(),重新計算 BetterScroll,當 DOM 結構發生變化的時候務必要調用確保滾動的效果正常scrollToElement(el, time, offsetX, offsetY, easing) 滾動到指定的目標元素on(type, fn, context) 監聽當前實例上的鉤子函數。如:scroll、scrollEnd 等
          • 還有一個收獲就是用第三方API,「有問題一定要查文檔」

          咱給小編:

          1. 點贊+評論

          2. 點頭像關注,轉發給有需要的朋友。

          您的支持是小編不斷輸出的動力,謝謝!!

          CSS 和 JS 的原生平滑滾動

          你想要一個平滑的滾動嗎? 忘記 JQuery,我們已經過去了。 讓我向您介紹我們的原生平滑滾動工具。


          CSS 滾動行為

          CSS scroll-behavior 屬性接受三個值之一 - 實際上是兩個值,因為其中一個已被棄用。

          1. scroll-behavior: auto 是我們已經習慣的默認即時滾動行為。
          2. scroll-behavior:instant 與 auto 相同,這就是它被棄用的原因。 如果你想要它,只需使用自動。
          3. scroll-behavior: smooth 在以編程方式觸發滾動事件時應用平滑過渡。

          我說“以編程方式觸發”是因為它不會平滑滾動鼠標滾輪。

          以編程方式觸發滾動事件的一些方法是:

          -    Window.scrollTo()
          -    Window.scrollBy()
          -    Element.scrollTo()
          -    Element.scrollBy()
          -    Element.scrollIntoView()
          -    Element.scrollLeft = x
          -    Element.scrollTop = y

          我們將分別探索這些方法。


          (注)Window.scroll() 和 Element.scroll()

          也許你已經注意到我沒有提到 scroll() 方法。

          這是因為 Window.scroll() 和 Element.scroll() 實際上是與 Window.scrollTo() 和 Element.scrollTo() 相同的方法。為避免重復內容,我將僅參考 scrollTo()。在實踐中,您可以使用任何一種,只需選擇一個并保持一致。


          Window.scrollTo() 和 Element.scrollTo()

          此方法非常適合滾動到絕對坐標。如果您有要將用戶滾動到的位置的 x 和 y 坐標,您可以簡單地調用 window.scrollTo(x, y) ,它會尊重頁面的 CSS 滾動行為。

          這同樣適用于可滾動元素。您只需調用 element.scrollTo(x, y) ,它就會尊重元素的 CSS 滾動行為。

          這個方法還有一個新的簽名,它使用一個對象而不是兩個數字參數,通過這個新的簽名,我們可以顯式地設置我們的滾動行為。

          // For window
          window.scrollTo({
            left: x,
            top: y,
            behavior: 'smooth'
          });
          // For element
          const el = document.querySelector(...);
          el.scrollTo({
            left: x,
            top: y,
            behavior: 'smooth'
          });


          Element.scrollLeft 和 Element.scrollTop

          設置元素 .scrollLeft 和 .scrollTop 屬性與使用坐標調用 Element.scrollTo() 相同。 它將尊重元素的 CSS 滾動行為。

          const el = document.querySelector(...);
          const x = 100;
          const y = 500;
          // Setting .scrollLeft and .scrollTop with smooth scroll
          el.style.scrollBehavior = 'smooth';
          el.scrollLeft = x;
          el.scrollTop = y;
          // Is the same as calling Element.scrollTo()
          el.scrollTo({ left: x, top: y, behavior: 'smooth' });


          (注)負元素.scrollLeft

          如果你的元素文本的方向是 rtl,scrollLeft = 0 表示水平滾動的最右邊位置,并且隨著你向左移動,值會減小。

          對于寬度為 100px,可滾動寬度為 500px,方向為 rtl 的可滾動元素,最左邊的位置是 scrollLeft = -400。

          <div id="scrollable" style="width: 100px; overflow: auto" dir="rtl">
            <div style="width: 500px; height: 100px; background: green"></div>
          </div>
          <p id="output"></p>
          const scrollable = document.querySelector('#scrollable');
          const output = document.querySelector('#output');
          const updateOutput = () => {
            output.textContent = `scrollLeft: ${scrollable.scrollLeft}`;
          };
          updateOutput();
          scrollable.addEventListener('scroll', updateOutput);


          Window.scrollBy() 和 Element.scrollBy()

          此方法與 Window.scrollTo() 或 Element.scrollTo() 具有完全相同的簽名。 它接受 x 和 y 作為兩個數字參數或作為具有可選 left、top 和 behavior 屬性的對象的單個參數。

          這里的區別是我們不是傳遞絕對坐標,而是相對值。 如果我們 scrollBy({ top: 10 }),我們從當前位置向下滾動 10 個像素,而不是從頁面開頭向下滾動 10 個像素。

          // For window
          window.scrollBy({ top: 10 }); // Scroll 10px down
          window.scrollBy({ left: 20 }); // Then 20px to the right
          window.scrollBy({ top: 50 }); // And then 50px down
          // For element
          const el = document.querySelector(...);
          el.scrollBy({ top: 10 }); // Scroll 10px down
          el.scrollBy({ left: 20 }); // Then 20px to the right
          el.scrollBy({ top: 50 }); // And then 50px down


          (注) Window.scrollByLines() 和 Window.scrollByPages()

          Firefox 更進一步,實現了滾動多行或多頁的方法。 這是一個僅適用于 Firefox 的非標準功能,因此您可能不想在生產中使用它。

          通過將 100vh(用于頁面)和 1rem(用于行)轉換為像素并將該值傳遞給 Window.scrollBy(),您可以在所有主要瀏覽器中實現類似的效果。

          const toPixels = require('to-px'); // From NPM
          const page = toPixels('100vh');
          window.scrollBy(0, page); // window.scrolByPages(1)
          const line = toPixels('1rem');
          window.scrollBy(0, line); // window.scrolByLines(1)


          Element.scrollIntoView()

          但大多數時候,我們并不關心任何硬編碼的坐標,我們只想將用戶滾動到屏幕上的特定元素。 這可以通過 Element.scrollIntoView() 輕松(更明確地)完成。

          可以不帶參數調用此方法,它將滾動頁面(尊重 CSS 滾動行為)直到元素在頂部對齊(除非元素在頁面底部,在這種情況下,它會 盡可能滾動)。

          <div style="height: 2000px">Some space</div>
          <div id="target" style="background: green">Our element</div>
          <div style="height: 500px">More space</div>
          const target = document.querySelector('#target');
          target.scrollIntoView();

          您可以通過傳遞布爾值或對象來進一步自定義元素在視圖中的位置。

          const el = document.querySelector(...);
          // Default, aligns at the top
          el.scrollIntoView(true);
          // Aligns at the bottom
          el.scrollIntoView(false);
          // Aligns vertically at the center
          el.scrollIntoView({ block: 'center' });
          // Vertical top and horizontal center
          el.scrollIntoView({ block: 'start', inline: 'center' });
          // Vertical bottom and horizontal right
          el.scrollIntoView({ block: 'end', inline: 'end' });
          // Vertical center and smooth
          el.scrollIntoView({ block: 'center', behavior: 'smooth' });
          // Vertical nearest
          el.scrollIntoView({ block: 'nearest' });

          當使用一個對象來定義元素的位置時,注意 block 是指垂直放置,而 inline 是指水平放置。此外,“最近”的位置可以是頂部/左側或底部/右側,以最近的為準,如果元素已經在視圖中,它也可以是空的。


          瀏覽器支持

          {關于瀏覽器支持} 在撰寫本文時,所有主流瀏覽器(Safari 除外)都支持平滑滾動和本文中描述的滾動方法。

          如果你需要更多的保證,我在我的項目中使用了一個非常好的平滑滾動 polyfill。它填充了 scroll()、scrollTo()、scrollBy()、scrollIntoView() 和 CSS 滾動行為。它不支持通過設置 scrollLeft/scrollTop 來平滑滾動,也不支持 scrollIntoView() 選項(它總是在頂部對齊元素)。

          如果你有更多的好奇心,我強烈建議你研究一下 polyfill 的源代碼,總共不到 500 行。我做了一個 PR 來稍微改進文檔,你可能會發現代碼更容易理解。


          祝你有美好的一天,很快見到你。

          extarea內容某個高度之內自適應,超過時候指定高度固定然后出現滾動條!

          效果如下:當你輸入超過設置的高度后,就會固定此高度不變。


          主站蜘蛛池模板: 国产在线视频一区二区三区| 夜色阁亚洲一区二区三区 | 国产精品一区电影| 国产精品无码一区二区三区电影 | 日本欧洲视频一区| 日韩综合无码一区二区| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 一区三区三区不卡| 国产精品综合一区二区三区| 精品国产AV一区二区三区| 国产精品无码一区二区三级| 91国在线啪精品一区| 亚洲日韩国产一区二区三区| 日韩精品一区二区三区中文字幕| 精品日韩一区二区| 无码中文字幕一区二区三区| 极品少妇一区二区三区四区| 国产福利一区二区精品秒拍| 一区二区不卡在线| 国产精品无码一区二区三区在| 国产一国产一区秋霞在线观看| 国产精品亚洲一区二区三区久久 | 波多野结衣一区二区免费视频| 无码精品久久一区二区三区| 亚洲AV日韩精品一区二区三区| 中文字幕亚洲一区| 人妻天天爽夜夜爽一区二区| 99精品一区二区三区无码吞精| 中文字幕日韩欧美一区二区三区 | 亚洲乱码日产一区三区| 一区高清大胆人体| 国产午夜精品一区二区三区极品 | 精品无码一区二区三区在线 | 精品日韩在线视频一区二区三区 | 日韩亚洲一区二区三区| 亚洲视频一区二区在线观看| 激情无码亚洲一区二区三区| 麻豆视传媒一区二区三区| 亚洲国产精品一区二区成人片国内 | 一区国产传媒国产精品| 一区二区在线免费视频|