共计 10519 个字符,预计需要花费 27 分钟才能阅读完成。
我已经完成了需要的 scroll 组件:
HTML 代码:
<template>
<bscroll class="singer-list"
:data="singers"
ref="listview"
:listenScroll="listenScroll"
@scroll="scroll"
>
<!-- 歌手列表区域 -->
<ul class="singers-wrapper">
<li class="singers-item"
v-for="(item,index) in singers"
:key="index"
ref="listGroup"
>
<h2 class="title">{{item.title}}</h2>
<div class="singer-wrapper border-bottom"
v-for="(des,index) in item.items"
:key="des.id">
<div class="singer-imgUrl">
<div class="bg-imgUrl">
<img v-lazy="des.avatar" alt="">
</div>
</div>
<div class="singer-name">{{des.name}}</div>
</div>
</li>
</ul>
<!-- 首先姓氏首字母列表,里面存储的是“热门”+(A-Z)-->
<div class="list-shortcut">
<ul class="shortcut-wrapper">
<li class="item"
v-for="(item, index) in shortcutList"
:key="index"
:data-index="index"
@touchstart="onShortcutTouchStart"
@touchmove.stop.prevent="onShortcutTouchMove">
<!-- 变量 data-index 要进行 v -bind-->
{{item}}
</li>
</ul>
</div>
</bscroll>
</template>
js 代码:
<script>
import Bscroll from 'components/bsroll/bsroll'
import {getData} from 'common/js/dom'
const ANCHOR_HEIGHT = 18 // 通过样式设置计算得到
const TITLE_HEIGHT = 30
export default {
name: "listview",
data(){
return{probeType:1,}
},
props:{
singers:{
type:Array,
defalut: []}
},
computed:{
// 展示字母导航的数据
shortcutList(){
return this.singers.map(group =>{return group.title.substr(0, 1)
})
}
},
created(){this.touch = {}
this.listenScroll = true
this.listHeight = []},
methods:{onShortcutTouchStart(e){
// 实现当手指触摸手机时触发该方法
//getData(), 该方法的作用是能够获取到姓氏字母中自定义的 data-index 的值
let dom = e.target; // 获取到了当前触发姓氏字母的元素
getData(dom, 'index'); // 这个方法我们获取了,当前点击的字母在 shortcutList 中的下标
},
onShortcutTouchMove(){},
scroll(pos){ },
_scrollTo(index) {},},
components:{Bscroll}
}
</script>
完成类似通讯录列表功能
- 当我们点击右侧的姓氏列表字母的时候,左侧歌手列表就自动定位到相应的歌手字母;
- 当我们在右侧的姓氏列表字母上滑动的时候,左侧歌手列表也会随着滑动;
- 当我们在左侧歌手列表中滑动的时候,右侧的姓氏列表字母会出现高光现象;
右侧的姓氏列表字母事件
onShortcutTouchStart(e){
// 实现当手指触摸手机时触发该方法
//getData(), 该方法的作用是能够获取到姓氏字母中自定义的 data-index 的值
let dom = e.target; // 获取到了当前触发姓氏字母的元素
// 点击的时候要先获取元素的索引
let anchorIndex = getData(dom, 'index'); // 当前点击的字母在 shortcutList 中的下标
this._scrollTo(anchorIndex)
},
_scrollTo(anchorIndex) {//scrollToElement()是 bscroll 组件中的方法,// 滚动到相应的位置
//scrollToElement(el, time, offsetX, offsetY, easing)
// el 滚动到的目标元素, 如果是字符串,则内部会尝试调用 querySelector 转换成 DOM 对象;
//this.$refs.listGroup 代表所以歌手的对象数据数组
//this.$refs.listGroup[anchorIndex]中 anchorIndex 是 this.$refs.listGroup 数组的下标
this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0)
},
总结:
从代码中可以知道姓氏字母列表数组是从歌手列表数组中重新组合的,所以姓氏字母列表数组的元素字母 item 与歌手列表数组元素中的 item.title 是一一对应的;也就是说,姓氏字母列表数组 shortcutList[index] === singers[index].title;
-
第一步,必须找到当前点击的字母的下标 anchorIndex;
let dom = e.target; // 获取到了当前触发姓氏字母的元素 // 点击的时候要先获取元素的索引 let anchorIndex = getData(dom, 'index'); // 当前点击的字母在 shortcutList 中的下标
- 第二步,可以通过使用 bscroll 组件中的 scrollToElement()方法滚动到相应的位置 singers[index].title;
// 然后利用 BScroll 将 $refs.listview 滚动到 $refs.listGroup 相应的位置,
this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0)
这里的 0 代表的是 滚动动画执行的时长
手指滑动字母列表时,右侧也能相应的滚动
首先我们要阻止右侧列表的 touchmove 事件的冒泡,因为如果事件冒泡到上层会在右侧列表滚动之后出现整个歌手列表区的滚动
这个功能实现的思路:
-
在 created()中定义一个参数,不在 data 中定义的原因是 data 中有 getter 和 setter 方法,这些都会有 dom 有关联,现在需要的参数与 dom 没有什么关联,所以不用放到 data 中
this.touch = {} this.listenScroll = true // 允许派发滚动事件 this.listHeight = []
- 在 onShortcutTouchMove 事件中,要先找到 Y 轴上的偏移,结合 onShortcutTouchStart 中的 this.touch.y1 数据和锚点(ANCHOR_HEIGHT)的高度,计算一共走过了几个锚点,就在_scrollTo()中将 ref 移动到指定位置;
- console.log(e.touches)的结果:
TouchList {0: Touch, length: 1}
0: Touch
clientX: 348
clientY: 276
force: 1
identifier: 0
pageX: 348
pageY: 276
radiusX: 11.5
radiusY: 11.5
rotationAngle: 0
screenX: 497
screenY: 414
target: li.item
__proto__: Touch
length: 1
__proto__: TouchList
-
在 onShortcutTouchStart 方法中获取最开始滑动时候的数据:
let firstTouch = e.touches[0] // 手指第一次触碰的位置 // 当前的 y 值, 这是滑动的开始位置 this.touch.y1 = firstTouch.pageY // 当前的锚点的索引 this.touch.anchorIndex = anchorIndex;
-
this.touch 是在 onShortcutTouchMove 和 onShortcutTouchStart 共享的函数,
this.touch={ y1:num, // 滑动的开始位置 anchorIndex:num // 当前的锚点 ...... }
- data 和 props 中的数据都会被自动的添加一个 getter 和 setter,所以 vue 会观测到 data,props 和 computed 中数值的变化,只要是为 dom 做数据绑定用的,因为我们并不需要观测 touch 的变化,我们只是单纯的为了两个函数都能获取到这个数据,所以我们将 touch 定义到 created 中;
-
onShortcutTouchMove 和 onShortcutTouchStart 两个时间手指第一次触屏的高度差 delta 然后除以锚点的高度为 18
const ANCHOR_HEIGHT = 18 // 通过 css 样式计算出来一个锚点的高度为 18
这样我们在 onShortcutTouchMove 就知道移动了多少个锚点,并更新滚动之后锚点的 index,然后将歌手列表滚动到当前锚点的位置
onShortcutTouchMove(e){
// touchstart 的时候要获取当前滚动的一个 Y 值,// touchmove 的时候也要获取当前滚动的一个 Y 值
let againTouch = e.touches[0];
this.touch.y2 = againTouch.pageY
// 计算出偏移了几个锚点,this.touch.y1 的值始终不变,一直都是第一次触碰时的值
let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0;
// 滚动之后的锚点, 更新 anchorIndex 的值
let anchorIndex = parseInt(this.touch.anchorIndex) + delta; // 字符串转化为 int 类型
this._scrollTo(anchorIndex)
}
总结这个方法中的要点
- 当我们第一次触摸屏幕的时候会获取一个 pageY 值【既 this.touch.y1】
- 当我们在姓氏字母列表中滑动到最后的时候也会获得一个 pageY 值【this.touch.y2】;
- 通过对样式的计算我们知道了一个锚点的高度是 ANCHOR_HEIGHT,这样通过对 (this.touch.y2 – this.touch.y1) / ANCHOR_HEIGHT | 0 的计算我们知道了偏移了几个锚点;
- 滚动之后的锚点, 更新 anchorIndex 的值:let anchorIndex = parseInt(this.touch.anchorIndex) + delta; // 字符串转化为 int 类型
- 最后通过 this._scrollTo(anchorIndex)将歌手列表滚动到当前锚点的位置
-
在 onShortcutTouchStart 中我们知道如果想将歌手列表滚动到当前锚点的位置,只需要获取到 anchorIndex 就行了
let anchorIndex = getData(dom, ‘index’); 为什么在 onShortcutTouchMove 不用 getData 方法而是要用更麻烦的计算方法呢?
原因:onShortcutTouchMove 方法是一个连续的动作,有可能 onShortcutTouchMove 触发的时候,这个时候都没有元素也就得不到 el, 这样就会造成下面的错误;vue.esm.js?efeb:628 [Vue warn]: Error in v-on handler: "TypeError: el.getAttribute is not a function" found in ---> <Listview> at src/components/listview/listview.vue <Singer> at src/pages/singer/singer.vue <App> at src/App.vue <Root> warn @ vue.esm.js?efeb:628 logError @ vue.esm.js?efeb:1893 globalHandleError @ vue.esm.js?efeb:1888 handleError @ vue.esm.js?efeb:1848 invokeWithErrorHandling @ vue.esm.js?efeb:1871 invoker @ vue.esm.js?efeb:2188 original._wrapper @ vue.esm.js?efeb:7559 vue.esm.js?efeb:1897 TypeError: el.getAttribute is not a function at getData (dom.js?a929:10) at VueComponent.onShortcutTouchMove (listview.vue?1565:101) at touchmove (eval at ./node_modules/vue-loader/lib/template-compiler/index.js?{"id":"data-v-437d5d2f","hasScoped":true,"transformToRequire":{"video":["src","poster"],"source":"src","img":"src","image":"xlink:href"},"buble":{"transforms":{}}}!./node_modules/vue-loader/lib/selector.js?type=template&index=0!./src/components/listview/listview.vue (app.js:1993), <anonymous>:79:32) at invokeWithErrorHandling (vue.esm.js?efeb:1863) at HTMLLIElement.invoker (vue.esm.js?efeb:2188) at HTMLLIElement.original._wrapper (vue.esm.js?efeb:7559)
重点是计算出两次触屏 index 的差值 delta 和 sart 首次触屏时的 index,从而计算出 move 之后的 index 值,让 ref:listview 移动到 listview[index];
出现了滚动的时候产生联动效果
解决方法:需要有一个变量计算位置落在哪个区间
在 bscroll.vue 中,添加 listenScroll,是否要监听滚动事件进滚动,在 bscroll.vue 中
listenScroll: {// 要不要监听滚动事件
type: Boolean,
default: false
}
如果要监听滚动事件,在初始化 BScroll 的时候要设置滚动事件,并派发函数将 pos 传出去
// 如果要监听滚动事件,在初始化列 BScroll 之后要派发一个监听事件
if (this.listenScroll) {
// BScroll 中的 this 是默认指向 scroll 的,所以要在 me 中保留 vue 实例的 this
let that = this
// 监听 scroll, 拿到 pos 后,派发一个函数将 pos 传出去
this.scroll.on('scroll', (pos) => {that.$emit('scroll', pos)
})
}
将 listenScroll 传递给组件
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll">
<!– 将 listenScroll 的值传给 bscroll, 并监听子组件 bscroll 传过来的事件 scroll–>
在 bscroll 组件中监听 @scroll=”scroll”,获取滚动的高度,定义为 scrollY
data() {
return {
scrollY: -1,
currentIndex: 0, // 默认第一个是高亮的
diff: -1
}
}
scroll(pos) {this.scrollY = pos.y // 实时获取到 bscroll 滚动到 Y 轴的网距离, 这个值是负数},
拿到滚动条的高度 scrollY 之后,还要计算每个 title 块的位置,确定 scrollY 落在哪两个 title 块之间
title 块:<li v-for="group in data" :key="group.id" class="list-group" ref="listGroup">
用_calculateHeight()来计算每个 title 块的 clientHeight 值
_calculateHeight(){
//clientHeight 为内部可视区高度,样式的 height+ 上下 padding
this.listHeight = [] // 每次滚动时高度计算都重新开始, 这里存放每个 title 块的 clientHeight 的累加
const list = this.$refs.listGroup; // 所有 li.singers-item 元素的集合
let height = 0 // 初始位置的 height 为 0,'热门' 的高度为 0,第一个元素
this.listHeight.push(height)
for(let i=0;i<list.length;i++){const item = list[i] // 得到每一个 li.singers-item 的元素
//item.clientHeight 就得到了每个 li.singers-item 的元素的 clientHeight 高
height+=item.clientHeight; // 通过累加的方式可以将每个 li.singers-item 的元素距离第一个 li.singers-item 的元素 [热门] 的距离算出来
this.listHeight.push(height) // 得到每一个元素对应的 height
}
console.log(this.listHeight);
}
一旦数据 singers 发生变化,dom 元素也会发生变化,那么 this.listHeight 数组也会发生变化,这样我们就必须时刻去更新 this.listHeight
watch: {singers(){this.$nextTick(()=>{this._calculateHeight();
})
}
}
现在每个 title 块的高度已经获取,此时我们需要对滑动的 scrollY 和 title 块做对象,确定此时滑动的字母是落到了那个 title 块中了
scrollY(newY){console.log(newY);
const listHeight = this.listHeight
if (newY > 0) { // 当滚动到顶部,newY>0
this.currentIndex = 0
return
}
for (let i = 0; i < listHeight.length - 1; i++) {
// 在中间部分滚动,遍历到最后一个元素,保证一定有下限,// listHeight 中的 height 比元素多一个
const height1 = listHeight[i]
const height2 = listHeight[i + 1]
if (-newY >= height1 && -newY < height2) {
this.currentIndex = i
// this.diff = height2 + newY // 得到 fixed title 上边界距顶部的偏移距离
return
}
}
// 当滚动到底部,且 -newY 大于最后一个元素的上限
// currentIndex 比 listHeight 中的 height 多一个, 比元素多 2 个
this.currentIndex = listHeight.length - 2
}
注意的点:
- newY 一共有三种情况,在顶部的时候,newY 的值是大于零的,中间部分 -newY 是在 height1 和 height2 之间,最后是 -newY 有可能大于 height2
- 在_calculateHeight()方法中我们自己给 this.listHeight 数组添加了一个为 0 的高度的‘热’的高度,而 0 到 728 都是‘热’这个滑动的区块,而后又循环了 his.$refs.listGroup,这样最终 listHeight 中的 height 比元素多一个;所以在 scrollY 循环中要减去一
- scrollY 初始化为 -1,代表了‘热门的’的 pageY 的值,而在姓氏字母中‘热’的高度定义给 0;所以‘-newY’>0 就一下子把‘热门’这个 title 块与字母‘热’关联起来了,在左侧滑动的时候,scrollY 值为 -1~-728 的话,这都是在‘热’这个字母中
点击字母显示高亮
滚动的时候可以产生高亮,但是点击右侧的时候不能产生高亮,因为点击时的滚动是通过 refs 获取相应的 DOM 来滚动的,没有用到 scrollY,但是我们定义高亮是通过 watch scrollY 落到的区间判断的,所以在_scrollTo(index)中,我们要手动更新 scrollY 的值
_scrollTo(index){// console.log('_scrollTo'); null
console.log(index);
// 处理点击的情况,onShortcutTouchStart()if(index == null){ // 点击姓氏字母的最顶部和最下部,就是点击‘热上面’和‘Z 的下面的情况
return
}
// 处理滑动的情况 onShortcutTouchMove()if (index < 0) { // 处理滑动时的边界情况
// 滑动到’热 ' 上面的情况
index = 0
} else if (index > this.listHeight.length - 2) {
// 滑动到’Z' 下面的情况
index = this.listHeight.length - 2
}
// 点击时更新 scrollY 的值才会出现高亮, 定义为每一个 listHeight 的上限位置,是一个负值
this.scrollY = -this.listHeight[index]
this.$refs.listview.scrollToElement(this.$refs.listGroup[index],0)
}
单击‘热门’上边的那一部分时,Z 会高亮,输出 index,测试到输出结果是 null,所以在_scrollTo 中做好限制,滑动到最顶部的时候,Z 也会高亮,那是因为 movetouch 事件一直没有停止,所以在_scrollTo 中添加
if (!index && index !== 0) { // 点击最顶部的情况
return
}
if (index < 0) { // 处理滑动时的边界情况
index = 0
} else if (index > this.listHeight.length - 2) {index = this.listHeight.length - 2}
固定 titile,并利用 css 将其规定在顶部
HTML 代码:
<div class="list-fixed" v-show="fixedTitle" ref="fixed"> <!-- fixedTitle 为空时不显示 -->
<h1 class="fixed-title">{{fixedTitle}}</h1>
</div>
CSS 代码:
.list-fixed {
position: absolute; // 绝对定位到顶部
top: 0;
left: 0;
width: 100%;
z-index 20
.fixed-title {
font-weight: bold;
font-size: 14px
padding: 10px;
color: $color-text-l;
background: $color-background;
}
}
js 代码:
滚动时出现上层 title【歌手的名字首字母】被下层的 title【歌手的名字首字母】一点一点的推上去而不见的情况,watch diff 的变化
data() {
return {
scrollY: -1,
currentIndex: 0, // 默认第一个是高亮的
diff: -1
}
}
当 title 区块的上限减去 scrollY 的差,判断这个差值和 title 的高度,这个差值大于 title 时,title 是不用变的
在中间滚动的时候定义 diff 的值,上限加上差值,newY 是负值,实际上是减去
if (-newY >= height1 && -newY < height2) { // !height2 表示列表的最后一项
this.currentIndex = i
this.diff = height2 + newY
// console.log(this.currentIndex)
return
}
title 的高度为 34px
const TITLE_HEIGHT = 34
然后在 watch diff
diff(newVal) {
let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
if (this.fixedTop === fixedTop) {return}
this.fixedTop = fixedTop
this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)` // 向上偏移的动画效果
}
newVal > 0 && newVal < TITLE_HEIGHT【表示现在的滑动还在 title 区块中,还不能将上一个 title 推上去】