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


Posted in Javascript onJune 29, 2020

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

效果图

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

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

原理分析

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

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

这个动画最重要的是在滚动中实时计算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组件学习教程进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS实多级联动下拉菜单类,简单实现省市区联动菜单!
May 03 Javascript
基于jQuery的仿flash的广告轮播
Nov 05 Javascript
jQuery控制图片的hover效果(smartRollover.js)
Mar 18 Javascript
JavaScript获取表单内所有元素值的方法
Apr 02 Javascript
jQuery的animate函数实现图文切换动画效果
May 03 Javascript
基于jQuery实现自动轮播旋转木马特效
Nov 02 Javascript
JS实现鼠标框选效果完整实例
Jun 20 Javascript
JS日期对象简单操作(获取当前年份、星期、时间)
Oct 26 Javascript
深入浅析JavaScript中的RegExp对象
Sep 18 Javascript
vue elementUI tree树形控件获取父节点ID的实例
Sep 12 Javascript
写一个Vue Popup组件
Feb 25 Javascript
浅谈JavaScript中this的指向更改
Jul 28 Javascript
element跨分页操作选择详解
Jun 29 #Javascript
vue实现数字滚动效果
Jun 29 #Javascript
js实现从右往左匀速显示图片(无缝轮播)
Jun 29 #Javascript
Vue实现可移动水平时间轴
Jun 29 #Javascript
uniapp与webview之间的相互传值的实现
Jun 29 #Javascript
基于Element封装一个表格组件tableList的使用方法
Jun 29 #Javascript
iview实现图片上传功能
Jun 29 #Javascript
You might like
动易数据转成dedecms的php程序
2007/04/07 PHP
安装PHP可能遇到的问题“无法载入mysql扩展” 的解决方法
2007/04/16 PHP
初学PHP的朋友 经常问的一些问题。不断更新
2011/08/11 PHP
php数组函数序列之end() - 移动数组内部指针到最后一个元素,并返回该元素的值
2011/10/31 PHP
php随机取mysql记录方法小结
2014/12/27 PHP
Symfony2安装第三方Bundles实例详解
2016/02/04 PHP
Jquery实现简单的动画效果代码
2012/03/18 Javascript
JS+CSS实现一个气泡提示框
2013/08/18 Javascript
asp.net刷新本页面的六种方法总结
2014/01/07 Javascript
JavaScript内存管理介绍
2015/03/13 Javascript
JavaScript仿flash遮罩动画效果
2016/06/15 Javascript
ajax分页效果(bootstrap模态框)
2017/01/23 Javascript
vue使用vue-cli快速创建工程
2017/07/28 Javascript
12条写出高质量JS代码的方法
2018/01/07 Javascript
基于vue,vue-router, vuex及addRoutes进行权限控制问题
2018/05/02 Javascript
vue3.0 搭建项目总结(详细步骤)
2019/05/20 Javascript
JS对象属性的检测与获取操作实例分析
2020/03/17 Javascript
[56:41]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 Newbee vs OG
2018/04/01 DOTA
Python for Informatics 第11章 正则表达式(一)
2016/04/21 Python
Python采用Django开发自己的博客系统
2020/09/29 Python
Python实现重建二叉树的三种方法详解
2018/06/23 Python
Python3 Post登录并且保存cookie登录其他页面的方法
2018/12/28 Python
在numpy矩阵中令小于0的元素改为0的实例
2019/01/26 Python
python基于plotly实现画饼状图代码实例
2019/12/16 Python
pycharm远程连接服务器并配置python interpreter的方法
2020/12/23 Python
JDO的含义
2012/11/17 面试题
Java工程师面试集锦之Spring框架
2013/06/16 面试题
心理学专业毕业生推荐信范文
2013/11/21 职场文书
后勤主管工作职责
2013/12/07 职场文书
八项规定整改措施
2014/02/12 职场文书
教学改革实施方案
2014/03/31 职场文书
高职教师先进事迹材料
2014/08/24 职场文书
2015年客房服务员工作总结
2015/05/15 职场文书
寻找成龙观后感
2015/06/12 职场文书
Pytorch 如何实现常用正则化
2021/05/27 Python
关于JavaScript轮播图的实现
2021/11/20 Javascript