我已经完成了需要的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推上去】
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。