言
最近的一個項目做的是vue組件中的一個應用,「處理滾動列表」,這個應該是很常見的需求了,在項目中遇到的痛點,難點,如何一步步解決的,以及小細節一些優化。
借鑒某課的思路,仿QQ音樂效果,記錄一下,自己字母解決這個難題,分享給你們,「希望對你們做移動端滾動列表問題有所幫助」
GitHub倉庫
處理滾動列表最終效果
從最終效果來看,實現了三個我的難點
接下來就是一步步去實現,優化上面的效果
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,{})
//{}配置一些信息
復制代碼
點這里有文檔
接下來就開始把
這個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"
// 兩個修飾符阻止冒泡以及默認的事件
復制代碼
思路
看代碼
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是對應的下標?」
有個不錯的思路:
那么我們按照上面的思路來完善吧
_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鉤子函數」
所以我們可以通過這個鉤子來獲取滾動的實時坐標
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
這樣子我們這個難點就解決了。
我們來看看效果
這樣子基本上問題就解決了,但是呢還會遇到一個問題?
這個是文檔上面的內容,我們可以看到這個配置項還是很重要的,我們listview組件需要通過props向子組件傳遞probeType值,值為3,這樣子就可以「在滾動中實時派發 scroll 事件」。
<scroll :probe-type='3'></scroll>
// 當然了,這個probeTyep會在data中拿到
復制代碼
咱給小編:
1. 點贊+評論
2. 點頭像關注,轉發給有需要的朋友。
您的支持是小編不斷輸出的動力,謝謝!!
CSS 和 JS 的原生平滑滾動
你想要一個平滑的滾動嗎? 忘記 JQuery,我們已經過去了。 讓我向您介紹我們的原生平滑滾動工具。
CSS 滾動行為
CSS scroll-behavior 屬性接受三個值之一 - 實際上是兩個值,因為其中一個已被棄用。
我說“以編程方式觸發”是因為它不會平滑滾動鼠標滾輪。
以編程方式觸發滾動事件的一些方法是:
- 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內容某個高度之內自適應,超過時候指定高度固定然后出現滾動條!
效果如下:當你輸入超過設置的高度后,就會固定此高度不變。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。