Fork me on GitHub
余鸢

Vue音乐播放器开发(三):滚动列表开发和应用

歌手页面开发

歌手数据接口抓取

singer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import jsonp from 'common/js/jsonp'
import {commonParams, options} from './config'
export function getSingerList() {
const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg'
const data = Object.assign({}, commonParams, {
channel: 'singer',
page: 'list',
key: 'all_all_all',
pagesize: 100,
pagenum: 1,
hostUin: 0,
platform: 'yqq',
needNewCode: 0
})
return jsonp(url, data, options)
}

singer.vue获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/ecmascript-6">
import {getSingerList} from 'api/singer'
import {ERR_OK} from 'api/config'
export default{
created() {
this._getSingerList()
},
methods: {
_getSingerList() {
getSingerList().then((res) => {
if (res.code === ERR_OK) {
this.singers = this._normalizeSinger(res.data.list)
}
})
}
}
}
</script>

处理数据

获取到的数据发现并不是我想要的数据结构,那怎么处理请求获取到的数据?

思路:把歌手数据分做为两层数组,外层是ABCD为区分歌手的数组,另一层则是以字母为首相关的歌手(比如以A为首字母的歌手)的二级数组。Findex标识为ABCD,它是随机的,也没有热门的歌手数据。这里我把Findex做聚合,把相同的归类起来,其次把前10条数据作为热门数据提取出来,这样可以得到我想要的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
_normalizeSinger(list) {
let map = {
hot: {
title: HOT_NAME,
items: []
}
}
list.forEach((item, index) => {
if (index < HOT_SINGER_LEN) {
map.hot.items.push(new Singer({
name: item.Fsinger_name,
id: item.Fsinger_mid
}))
}
const key = item.Findex
if (!map[key]) {
map[key] = {
title: key,
items: []
}
}
map[key].items.push(new Singer({
name: item.Fsinger_name,
id: item.Fsinger_mid
}))
})
// 为了得到有序列表,我们需要处理 map
let ret = []
let hot = []
for (let key in map) {
let val = map[key]
if (val.title.match(/[a-zA-Z]/)) {
ret.push(val)
} else if (val.title === HOT_NAME) {
hot.push(val)
}
}
ret.sort((a, b) => {
return a.title.charCodeAt(0) - b.title.charCodeAt(0)
})
return hot.concat(ret)
}

歌手列表 — 滚动列表组件开发

列表组件也可以在其他地方使用,所以把它作为基础组件增加它的通用性。

listview.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<scroll class="listview" :data="data">
<ul>
<li class="list-group" v-for="group in data">
<h2 class="list-group-title">{{group.title}}</h2>
<ul>
<li class="list-group-item" v-for="item in group.items">
<img class="avatar" :src="item.avatar">
<span class="name">{{item.name}}</span>
</li>
</ul>
</li>
</ul>
</scroll>
</template>
<script type="text/ecmascript-6">
import Scroll from 'base/scroll/scroll'
export default{
props: {
data: {
type: Array,
default: []
}
},
components: {
Scroll
}
}
</script>

引入singer.vue组件并渲染到页面上,同时实现列表滚动。

v18

还有两个部分:右侧快速入口、滚动的时候会有固定的字母(title)显示列表上方

右侧快速入口

右侧快速入口实际上就是以字母开头的title。首先构造一个可以被遍历的数组数据。

获取右侧快速入口的列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<scroll class="listview" :data="data">
<ul>...</ul>
<div class="list-shortcut">
<ul>
<li class="item" v-for="item in shortcutList">{{item}}</li>
</ul>
</div>
</scroll>
</template>
<script type="text/ecmascript-6">
export default{
...
computed: {
shortcutList() {
return this.data.map((group) => {
return group.title.substr(0, 1)
})
}
}
}
</script>

实现左右侧的交互(右侧栏滚动事件)

@touchstart点击事件

显示右侧入口栏仅仅是个dom,并没有其他交互行为。新建@touchstart点击事件,点击这个事件后滚动到相应的元素,那就要先知道滚到第几个元素才能知道左侧第几个组(group),所以点的时候要获取到这个元素的索引,获取索引:data-index="index"

1
2
3
4
onShortcutTouchStart(e) {
let anchorIndex = getData(e.target, 'index')
this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0)
}
@touchMove事件

实现点击鼠标拖动右侧栏左侧歌手也跟着滚动到相应位置,监听@touchMove事件。要知道从touchstart到touchMove滚动的位置,计算当前位置和一开始滚动位置的差来算出滚动到第几个元素。在touchstart的时候记录当前的y值y1(firstTouch.pageY),然后在touchMove的时候又获取touch到的y值y2。

要有一个属性保存firstTouch的pageY位置,为了方便在两个函数之间相互获取这个数据,在created定义个touch对象(我们并不需要观测touch的变化,所以这里在created里定义)。

>

@touchmove.stop.prevent的stop是为阻止事件向上冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
onShortcutTouchStart(e) {
let anchorIndex = getData(e.target, 'index')
let firstTouch = e.touches[0]
// 当前pageY值
this.touch.y1 = firstTouch.pageY
// 当前索引
this.touch.anchorIndex = anchorIndex
this._scrollTo(anchorIndex)
},
onShortcutTouchMove(e) {
let firstTouch = e.touches[0]
this.touch.y2 = firstTouch.pageY
// y轴的偏移(偏移了几个锚点)
let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0
// 获取touchmove时候滚动到的位置
let anchorIndex = parseInt(this.touch.anchorIndex) + delta
this._scrollTo(anchorIndex)
},
_scrollTo (index) {
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
}

左右联动

根据左侧滚动右侧也滚动到相应的位置,同时滚动的位置字母显示高亮。比如滚动到C位置的时候C为高亮显示。

想要左右联动的话,首先必须知道当前滚动的时候右侧知道滚动的位置,要有一个变量记录滚动的位置也就是y轴的位置,根据实时滚动的位置计算我的位置落在哪个区间,所以要实时监听y轴的位置。这里要对scroll组件做一些拓展。

scroll.vue

1
2
3
4
5
// 监听滚动事件
listenScroll: {
type: Boolean,
default: false
},

这里不需要关心它的滚动位置,所以default设置为false,如果是true的话,在初始化scroll的时候要加个逻辑,监听scroll的滚动事件从而触发scroll事件,这样就可以拿到它的位置,pos是个对象,包含x轴和y轴的属性。

v19

在listview组件里实现滚动事件

v20

定义两个观测数据:

  • scrollY:记录实时滚动的位置

  • currentIndex:当前显示的位置(currentIndex对应哪个谁谁就高亮)

1
2
3
4
5
6
data() {
return {
scrollY: -1,
currentIndex: 0
}
},

要知道scroll落在哪个位置,首先这个列表的每个group都有高度,先计算每个group的高度。当data发生变化的时候要延迟计算它的高度,这里还要watch data的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
methods: {
_calculateHeight() {
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]
// item是个dom,用clientHeight获取它的高度
height += item.clientHeight
this.listHeight.push(height)
}
}
},
watch: {
data() {
setTimeout(() => {
this._calculateHeight()
}, 20)
}
}

有了listHeight,就方便观察scrollY变化时,scrollY和listHeight做对比就知道它落在第几个区间,从而就得到currentIndex。
观测scrollY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
watch: {
scrollY(newY) {
const listHeight = this.listHeight
for (let i = 0; i < listHeight.length; i++) {
let height1 = listHeight[i]
let height2 = listHeight[i + 1]
if (!height2 || (-newY > height1 && -newY < height2)) {
this.currentIndex = i
console.log(this.currentIndex)
return
}
}
this.currentIndex = 0
}
}

滚动时出现问题,发现监听不到在swipe的情况下的scroll事件,想要监听这种实时滚动的话就不截流这种方式,需要改变probeType的值,默认是1改成3。

probeType: 1 滚动的时候会派发scroll事件,会截流。2滚动的时候实时派发scroll事件,不会截流。 3除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件

v21

有了currentIndex,需要右侧栏有个active的效果,效果如图:

v22

当滚动到最底部的时候newY的值是大于0的,值就为负数它就永远不会落到这个区间。我们可以拆分为三种情况:歌手列表滚动到最顶部时、中间时和最底部时,最顶部时newY是大于0的,最底部时newY可能大于height2,也就是说newY值可以大于最后一个元素的上限。这样就可以在什么情况下应该怎么计算currentIndex,保证实时滚动currentIndex能计算出应该落到的位置。

对watch scrollY做些相应的改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
return
}
}
// 当滚动到底部,且-newY大于最后一个元素的上限
this.currentIndex = 0
}

当点击右侧快速入口时可以切换左侧的位置,但是高亮并没有随之改动,是因为高亮并不是根据点击的位置,而是根据scrollY计算而来的,scrollY的变化又是根据scroll事件实时更新的,调用_scrollTo(index)方法让它滚动到相应的位置,但是这个滚动并没有触发滚动事件,所以监听不到scrollY的变化,需要手动去改变scrollY的值。

1
2
3
4
_scrollTo(index) {
this.scrollY = -this.listHeight[index]
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
}

优化

解决右侧栏边界问题

为了页面好看右侧栏的上下两头分别多出一些边缘,但是这两个边缘点击是无意义的,不会触发任何滚动事件,做些逻辑处理。

1
2
3
4
5
6
7
8
9
_scrollTo (index) {
// 解决右侧栏边界问题
if (!index && index !== 0) {
return
}
this.scrollY = -this.listHeight[index]
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
}

解决右侧栏滑动到最顶部直接跳到底部的问题

console.log(index)发现滑动到最顶部是负值,最底部是无限大的值,是因为touchmove一直在执行,它的y值一直在变大,同样anchorIndex的值也变超。对于滑动顶部或底部做些边界条件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_scrollTo (index) {
// 解决右侧栏边界问题
if (!index && index !== 0) {
return
}
// 解决右侧栏滑动到最顶部直接跳到底部的问题
if (index < 0) {
index = 0
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2
}
this.scrollY = -this.listHeight[index]
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
},

滚动固定标题实现(fixed-title)

fixed-title:当我们滚动到某个区块,列表顶部会显示对应的名称,其实和右侧显示高亮有异曲同工之妙,不同的是dom实现不一样。

1
2
3
<div class="list-fixed" v-show="fixedTitle">
<h1 class="fixed-title">{{fixedTitle}}</h1>
</div>
1
2
3
4
5
6
7
8
computed: {
fixedTitle () {
if (this.scrollY > 0) {
return ''
}
return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
}
},

当滚动时左侧列表顶部需要有一个fixed-title,当两个title重合的时候要有一个title往上顶的效果,在data里定义一个diff。diff表示滚动到的区块的上限和当前滚动位置的滚动差。

观测scrollY做些改变:

v23

观测diff的变化来设置fixed-tit1e的偏移。

1
2
3
4
5
6
7
8
9
10
watch: {
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)`
}
}

歌手列表是异步获取的,这里也要使用loading效果,listview里引入loading组件

1
2
3
<div class="loading-container" v-show="!data.length">
<loading></loading>
</div>

总结:

左右联动思路:想要达到左右联动,首先需要实时知道滚动位置,根据滚动位置计算出当前位置是落到哪个group的区间,相应的算出右边哪个索引应该高亮,结合vue的watch观测数据变化。

具体源代码

参考:https://github.com/kakajing/vue-music