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组件学习教程进行学习。