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 相关文章推荐
javascript 读取图片文件的大小
Jun 25 Javascript
JavaScript插入动态样式实现代码
Feb 22 Javascript
使用js正则控制input标签只允许输入的值
Jul 29 Javascript
jQuery中获取checkbox选中项等操作及注意事项
Nov 24 Javascript
jQuery实现带动画效果的二级下拉导航方法
Mar 11 Javascript
利用JavaScript脚本实现滚屏效果的方法
Jul 07 Javascript
js实现异步循环实现代码
Feb 16 Javascript
Angular.js中用ng-repeat-start实现自定义显示
Oct 18 Javascript
BootStrap中jQuery插件Carousel实现轮播广告效果
Mar 27 jQuery
创建简单的node服务器实例(分享)
Jun 23 Javascript
8个有意思的JavaScript面试题
Jul 30 Javascript
JavaScript ES 模块的使用
Nov 12 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
PHP simple_html_dom.php+正则 采集文章代码
2009/12/24 PHP
PHP在特殊字符前加斜杠的实现代码
2011/07/17 PHP
PHP表单提交表单名称含有点号(.)则会被转化为下划线(_)
2011/12/14 PHP
php读取flash文件高宽帧数背景颜色的方法
2015/01/06 PHP
PHP使用pdo连接access数据库并循环显示数据操作示例
2018/06/05 PHP
PHP实现批量修改文件名的方法示例
2019/09/18 PHP
php文件上传原理与实现方法详解
2019/12/20 PHP
使用jQuery同时控制四张图片的伸缩实现代码
2013/04/19 Javascript
jquery ajax传递中文参数乱码问题及解决方法说明
2014/02/07 Javascript
js实现的GridView即表头固定表体有滚动条且可滚动
2014/02/19 Javascript
js左右弹性滚动对联广告代码分享
2014/02/19 Javascript
js判断一个字符串是否包含一个子串的方法
2015/01/26 Javascript
Bootstrap每天必学之下拉菜单
2015/11/25 Javascript
JS组件Bootstrap实现弹出框和提示框效果代码
2015/12/08 Javascript
layui表格实现代码
2017/05/20 Javascript
微信小程序之电影影评小程序制作代码
2017/08/03 Javascript
ES6小技巧之代替lodash
2019/06/07 Javascript
环形加载进度条封装(Vue插件版和原生js版)
2019/12/04 Javascript
Node.js API详解之 module模块用法实例分析
2020/05/13 Javascript
vue3弹出层V3Popup实例详解
2021/01/04 Vue.js
python下调用pytesseract识别某网站验证码的实现方法
2016/06/06 Python
Python中操作mysql的pymysql模块详解
2016/09/13 Python
Python中单例模式总结
2018/02/20 Python
Django框架 Pagination分页实现代码实例
2019/09/04 Python
IRO美国官网:法国服装品牌
2018/03/06 全球购物
MIXIT官网:俄罗斯最大的化妆品公司之一
2020/01/25 全球购物
造价工程师个人求职信
2013/09/21 职场文书
《充气雨衣》教学反思
2014/04/07 职场文书
三八红旗集体先进事迹材料
2014/05/22 职场文书
应届生求职自荐信
2014/07/04 职场文书
死亡赔偿协议书
2015/01/28 职场文书
邀请函样本
2015/02/02 职场文书
聘任书格式及范文
2015/09/21 职场文书
2016寒假假期总结
2015/10/10 职场文书
CocosCreator ScrollView优化系列之分帧加载
2021/04/14 Python
Python Matplotlib绘制两个Y轴图像
2022/04/13 Python