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 相关文章推荐
JQuery的Validation插件中Remote验证的中文问题
Jul 26 Javascript
File, FileReader 和 Ajax 文件上传实例分析(php)
Apr 27 Javascript
解析js如何获取当前url中的参数值并复制给input
Jun 23 Javascript
jquery 利用show和hidden实现级联菜单示例代码
Aug 09 Javascript
jquery操作select详解(取值,设置选中)
Feb 07 Javascript
JavaScript sub方法入门实例(把字符串显示为下标)
Oct 17 Javascript
快速掌握Node.js环境的安装与运行方法
Feb 16 Javascript
JS中对Cookie的操作详解
Aug 05 Javascript
浅谈jquery.form.js的ajaxSubmit和ajaxForm的使用
Sep 09 Javascript
详解angular2封装material2对话框组件
Mar 03 Javascript
d3.js实现立体柱图的方法详解
Apr 28 Javascript
jQuery常见的遍历DOM操作详解
Sep 05 jQuery
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+MySql编写聊天室
2006/10/09 PHP
ie6 动态缩略图不显示的原因
2009/06/21 PHP
php 浮点数比较方法详解
2017/05/05 PHP
thinkPHP中钩子的使用方法实例分析
2017/11/16 PHP
使用PHP反射机制来构造&quot;CREATE TABLE&quot;的sql语句
2019/03/21 PHP
php中钩子(hook)的原理与简单应用demo示例
2019/09/03 PHP
JS中简单的实现像C#中using功能(有源码下载)
2007/01/09 Javascript
指定js可访问其它域名的cookie的方法
2007/09/18 Javascript
nodejs实用示例 缩址还原
2010/12/28 NodeJs
JS判断元素为数字的奇异写法分享
2012/08/01 Javascript
一个简单的动态加载js和css的jquery代码
2014/09/01 Javascript
JavaScript DOM进阶方法
2015/04/13 Javascript
jQuery插件EnPlaceholder实现输入框提示文字
2015/06/05 Javascript
兼容各大浏览器的JavaScript阻止事件冒泡代码
2015/07/09 Javascript
利用Angularjs和原生JS分别实现动态效果的输入框
2016/09/01 Javascript
touch.js 拖动、缩放、旋转 (鼠标手势)功能代码
2017/02/04 Javascript
vue获取input输入值的问题解决办法
2017/10/17 Javascript
jQuery实现当拉动滚动条到底部加载数据的方法分析
2019/01/24 jQuery
layer弹出框确定前验证:弹出消息框的方法(弹出两个layer)
2019/09/21 Javascript
微信公众号中的JSSDK接入及invalid signature等常见错误问题分析(全面解析)
2020/04/11 Javascript
[01:01:35]Optic vs paiN 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
Python列表list数组array用法实例解析
2014/10/28 Python
Tensorflow 实现修改张量特定元素的值方法
2018/07/30 Python
python opencv 简单阈值算法的实现
2019/08/04 Python
python 获取当前目录下的文件目录和文件名实例代码详解
2020/03/10 Python
解决redis与Python交互取出来的是bytes类型的问题
2020/07/16 Python
Pycharm如何自动生成头文件注释
2020/11/14 Python
MONNIER Frères英国官网:源自巴黎女士奢侈品配饰电商平台
2018/12/06 全球购物
医学检验专业个人求职信范文
2013/12/04 职场文书
财政专业求职信范文
2014/02/19 职场文书
黄金搭档广告词
2014/03/21 职场文书
传播学专业毕业生自荐书
2014/07/01 职场文书
动物科学专业求职信
2014/07/27 职场文书
社区党员群众路线教育实践活动心得体会
2014/11/03 职场文书
解决Goland 同一个package中函数互相调用的问题
2021/05/06 Golang
Android开发 使用文件储存的方式保存QQ密码
2022/04/24 Java/Android