vue实现列表滚动的过渡动画

本文实例为大家分享了Vue实现列表滚动过渡动画的具体代码,供大家参考,具体内容如下

效果图

失帧比较严重,在手机上效果更佳。

原理分析

这个滚动页面由两个部分布局(底部固定的Tab页面除外)。一个是顶部的banner轮播,一个是下面的列表。这里的重点是做列表的动画,banner轮播的网上资料很多,请自行查找。

这个动画最重要的是在滚动中实时计算startIndex和endIndex,动画比较简单,就是scale和opacity的变化。向下滚动时,startIndex变小;向上滚动时,endIndex变大时,新露脸的项做该动画。当滚动连起来,就是一个完整的动画了。

涉及的技术

使用better-scroll做滚动以及轮播图

使用create-keyframe-animation做动画控制

实现步骤

1、vue的template部分

注意:由于IOS渲染速度比较快, 必须把没有展现在首屏的页面上的item隐藏掉,即index比startIndex小、比endIndex大的item都应该隐藏,避免页面动画混乱。

<div class="area-wrapper" ref="areaWrapper">

<div v-for="(item, index) in areaList" :key="index"

@click="clickAreaItem(item.id)"

:ref="'area-' + index" class="area"

:style="{ backgroundImage: 'url('+item.thumbUrl+')', 'opacity': (index < startIndex || index > endIndex) ? 0 : 1}">

<div class="content">

<h2 class="num">{{item.num}}</h2>

<div style="vertical-align:text-bottom">

<p class="name">{{item.name}}</p>

<p class="desc">{{item.desc}}</p>

</div>

</div>

</div>

</div>

高度预设。用于计算startIndex、endIndex

const AreaItemHeight = 119 // 每一项的高度(这里默认一致,如果不一致请自行修改startIndex、endIndex的计算方式)

const MarginBottom = 15 // 列表项的底部边距

const TopHeight = 160 // banner的高度

const BottomHeight = 50 // 底部Tab的高度

监听滚动。并实时计算startIndex、endIndex

scroll (position) {

const scrollY = position.y

if (scrollY < 0) {

// startIndex计算

const currentStartIndex = Math.abs(scrollY) <= TopHeight ? 0 : parseInt((Math.abs(scrollY) - TopHeight) / (AreaItemHeight + MarginBottom))

// endIndex计算

let currentEndIndex = Math.floor((window.innerHeight - (TopHeight + scrollY) - BottomHeight) / (AreaItemHeight + MarginBottom))

if (currentEndIndex > this.areaList.length - 1) {

currentEndIndex = this.areaList.length - 1

}

// 这里使用vue的watch属性监听更好

if (currentStartIndex !== this.startIndex) {

if (currentStartIndex < this.startIndex) {

// 运行动画

this.runAnimation(currentStartIndex)

}

this.startIndex = currentStartIndex

}

// 这里使用vue的watch属性监听更好

if (currentEndIndex !== this.endIndex) {

if (currentEndIndex > this.endIndex) {

this.runAnimation(currentEndIndex)

}

this.endIndex = currentEndIndex

}

}

}

运行动画

runAnimation (index) {

animations.registerAnimation({

name: 'scale',

animation: [

{

scale: 0.5,

opacity: 0

},

{

scale: 1,

opacity: 1

}

],

presets: {

duration: 300,

resetWhenDone: true

}

})

animations.runAnimation(this.$refs['area-' + index], 'scale')

}

完整代码

.vue文件

<template>

<div class="address-wrapper" style="height: 100%;">

<scroll ref="scroll" class="address-content" :data="areaList" @scroll="scroll" :listen-scroll="listenScroll" :probe-type="probeType" :bounce="false">

<div>

<div v-if="bannerList.length" style="position: relative;">

<slider :list="bannerList">

<div v-for="item in bannerList" :key="item.id" :style="{height: sliderHeight + 'px'}">

<img class="needsclick" :src="item.thumbUrl" width="100%" height="100%" />

</div>

</slider>

<div class="banner-bg"></div>

<div class="banner-bg-1"></div>

</div>

<div class="area-wrapper" ref="areaWrapper">

<div v-for="(item, index) in areaList" :key="index"

@click="clickAreaItem(item.id)"

:ref="'area-' + index" class="area"

:style="{ backgroundImage: 'url('+item.thumbUrl+')', 'opacity': (index < startIndex || index > endIndex) ? 0 : 1}">

<div class="content">

<h2 class="num">{{item.num}}</h2>

<div style="vertical-align:text-bottom">

<p class="name">{{item.name}}</p>

<p class="desc">{{item.desc}}</p>

</div>

<!-- <div></div> -->

</div>

</div>

</div>

</div>

</scroll>

<router-view />

</div>

</template>

<script>

import Slider from '@/components/slider/slider'

import Scroll from '@/components/scroll/scroll'

import { isIphoneX } from '@/assets/js/brower'

import animations from 'create-keyframe-animation'

import axios from '@/api/axiosApi'

import areaList from '@/assets/json/areaList.json'

import bannerList from '@/assets/json/bannerAddress.json'

// 每一个的Area的高度,都是一样的

const AreaItemHeight = 119

const MarginBottom = 15

const TopHeight = 160

const BottomHeight = 50

export default {

data () {

return {

startIndex: 0,

endIndex: 3,

bannerList,

areaList

}

},

components: {

Slider, Scroll

},

created () {

this.probeType = 3

this.listenScroll = true

this.sliderHeight = 210 + 20

if (isIphoneX()) {

this.sliderHeight += 34

}

this._getBanner()

this._getAddressList()

},

mounted () {

this.endIndex = Math.floor((window.innerHeight - TopHeight - BottomHeight) / (AreaItemHeight + MarginBottom))

},

methods: {

_getBanner () {

axios.get(this, '/v1/banner/1', null, (data) => {

data.forEach(item => {

item.thumbUrl += '-banner'

})

this.bannerList = data

}, null, false)

},

_getAddressList () {

axios.get(this, '/v1/address/1', {

pageSize: 30

}, (data) => {

// data.forEach(item => {

// item.thumbUrl += '-tiaomu'

// })

this.areaList = data

}, null, false)

},

scroll (position) {

const scrollY = position.y

if (scrollY < 0) {

const currentStartIndex = Math.abs(scrollY) <= TopHeight ? 0 : parseInt((Math.abs(scrollY) - TopHeight) / (AreaItemHeight + MarginBottom))

let currentEndIndex = Math.floor((window.innerHeight - (TopHeight + scrollY) - BottomHeight) / (AreaItemHeight + MarginBottom))

if (currentEndIndex > this.areaList.length - 1) {

currentEndIndex = this.areaList.length - 1

}

if (currentStartIndex !== this.startIndex) {

if (currentStartIndex < this.startIndex) {

this.runAnimation(currentStartIndex)

}

this.startIndex = currentStartIndex

}

if (currentEndIndex !== this.endIndex) {

if (currentEndIndex > this.endIndex) {

this.runAnimation(currentEndIndex)

}

this.endIndex = currentEndIndex

}

}

},

runAnimation (index) {

animations.registerAnimation({

name: 'scale',

animation: [

{

scale: 0.5,

opacity: 0

},

{

scale: 1,

opacity: 1

}

],

presets: {

duration: 300,

resetWhenDone: true

}

})

animations.runAnimation(this.$refs['area-' + index], 'scale')

},

clickAreaItem (id) {

this.$router.push(`address/addressDetail/${id}`)

}

}

}

</script>

<style lang="stylus" scoped>

.address-wrapper {

.address-content {

height: 100%;

overflow: hidden;

.banner-bg {

height: 50px;

width: 100%;

position: absolute;

bottom: -1px;

background:-moz-linear-gradient(top, rgba(249, 250, 252, 0.3), rgba(249, 250, 252, 1));/*火狐*/

background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(249, 250, 252, 0.3)), to(rgba(249, 250, 252, 1))); /*谷歌*/

background-image: -webkit-gradient(linear,left bottom,left top,color-start(0, rgba(249, 250, 252, 0.3)),color-stop(1, rgba(249, 250, 252, 1)));/* Safari & Chrome*/

}

.banner-bg-1 {

height: 20px;

width: 100%;

position: absolute;

bottom: 49px;

background:-moz-linear-gradient(top, rgba(249, 250, 252, 0), rgba(249, 250, 252, 0.3));/*火狐*/

background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(249, 250, 252, 0)), to(rgba(249, 250, 252, 0.3))); /*谷歌*/

background-image: -webkit-gradient(linear,left bottom,left top,color-start(0, rgba(249, 250, 252, 0)),color-stop(1, rgba(249, 250, 252, 0.3)));/* Safari & Chrome*/

}

.area-wrapper {

transform: translateY(-45px)

padding: 0 15px;

z-index: 1;

.area {

margin-bottom: 15px;

height: 119px;

width: 100%;

border-radius: 10px;

background-repeat: no-repeat;

background-size: cover;

box-shadow: 0 0 10px #a4a3a3;

display: flex;

align-items: flex-end;

.content {

color: #fff;

display: flex;

padding-right: 60px;

padding-bottom: 15px;

line-height: 1.2;

.num {

bottom: 35px;

font-size: 48px;

font-weight: 100;

padding: 0 15px;

display:table-cell;

vertical-align:bottom;

}

.name {

font-size: 21px;

font-weight: 600;

line-height: 1.7;

}

.desc {

font-size: 14px;

}

}

}

}

}

}

</style>

本地json文件,请自行修改图片路径

bannerAddress.json

[

{

"id": 1,

"contentId": 111111,

"type": 1,

"thumbUrl": "./static/img/banner/banner_address_1.jpg"

},

{

"id": 2,

"contentId": 111111,

"type": 1,

"thumbUrl": "./static/img/banner/banner_address_2.jpg"

},

{

"id": 3,

"contentId": 111111,

"type": 1,

"thumbUrl": "./static/img/banner/banner_address_3.jpg"

}

]

areaList.json

[

{

"id": "ba062c32fdf611e7ba2d00163e0c27f8",

"name": "凯里",

"desc": "这是凯里哟~",

"num": 17,

"thumbUrl": "./static/img/area/kaili.png"

}, {

"id": "ba5287a7fdf611e7ba2d00163e0c27f8",

"name": "丹寨",

"desc": "这是丹寨哟~",

"num": 8,

"thumbUrl": "./static/img/area/danzai.png"

}, {

"id": "ba9da079fdf611e7ba2d00163e0c27f8",

"name": "麻江",

"desc": "这是麻江哟~",

"num": 12,

"thumbUrl": "./static/img/area/majiang.png"

}, {

"id": "baeb0926fdf611e7ba2d00163e0c27f8",

"name": "黄平",

"desc": "这是黄平哟~",

"num": 7,

"thumbUrl": "./static/img/area/huangping.png"

}, {

"id": "bb357191fdf611e7ba2d00163e0c27f8",

"name": "施秉",

"desc": "这是施秉哟~",

"num": 6,

"thumbUrl": "./static/img/area/shibing.png"

}, {

"id": "bb842d8ffdf611e7ba2d00163e0c27f8",

"name": "镇远",

"desc": "这是镇远哟~",

"num": 3,

"thumbUrl": "./static/img/area/zhenyuan.png"

}, {

"id": "bbce67dffdf611e7ba2d00163e0c27f8",

"name": "岑巩",

"desc": "这是岑巩哟~",

"num": 23,

"thumbUrl": "./static/img/area/cengong.png"

}, {

"id": "bc198ca9fdf611e7ba2d00163e0c27f8",

"name": "三穗",

"desc": "这是三穗哟~",

"num": 66,

"thumbUrl": "./static/img/area/sansui.png"

}, {

"id": "bc64498bfdf611e7ba2d00163e0c27f8",

"name": "天柱",

"desc": "这是天柱哟~",

"num": 128,

"thumbUrl": "./static/img/area/tianzhu.png"

}, {

"id": "bcaf466bfdf611e7ba2d00163e0c27f8",

"name": "锦屏",

"desc": "这是锦屏哟~",

"num": 107,

"thumbUrl": "./static/img/area/jinping.png"

}, {

"id": "bcfa6f1bfdf611e7ba2d00163e0c27f8",

"name": "黎平",

"desc": "这是黎平哟~",

"num": 211,

"thumbUrl": "./static/img/area/liping.png"

}, {

"id": "bd44cca9fdf611e7ba2d00163e0c27f8",

"name": "从江",

"desc": "这是从江哟~",

"num": 17,

"thumbUrl": "./static/img/area/congjiang.png"

}, {

"id": "bd8f5cd4fdf611e7ba2d00163e0c27f8",

"name": "榕江",

"desc": "这是榕江哟~",

"num": 17,

"thumbUrl": "./static/img/area/rongjiang.png"

}, {

"id": "bdda2928fdf611e7ba2d00163e0c27f8",

"name": "雷山",

"desc": "这是雷山哟~",

"num": 17,

"thumbUrl": "./static/img/area/leishan.png"

}, {

"id": "be25afc0fdf611e7ba2d00163e0c27f8",

"name": "台江",

"desc": "这是台江哟~",

"num": 17,

"thumbUrl": "./static/img/area/taijiang.png"

}, {

"id": "be702db5fdf611e7ba2d00163e0c27f8",

"name": "剑河",

"desc": "这是剑河哟~",

"num": 17,

"thumbUrl": "./static/img/area/jianhe.png"

}

]

关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。

以上是 vue实现列表滚动的过渡动画 的全部内容, 来源链接: utcz.com/z/347776.html

回到顶部