mint-ui swipe组件源码解析
来源:互联网 发布:美国生活水平知乎 编辑:程序博客网 时间:2024/06/10 20:27
前叙
mint-ui组件库中swipe组件,实现的是常见的轮播图效果。但是它的实现方式,和常见的实现有所不同。
常见的实现方式: 通过移动轮播图的wrapper来实现item的切换效果(也就是修改wrapper的translate3d属性来实现)。如果支持循环播放,需要在首部插入一个最后一个轮播图item的clone版,以及在尾部插入一个第一个轮播图item的clone版。
swipe组件实现的方式: 只显示当前显示的轮播图item,当切换的时候,显示出当前item的前后相邻的两个item;通过设置三个item的translate3d来实现切换的效果。
两个实现方式的对比:
- 第一种方式,初始会渲染出所有的item,通过translate3d来实现切换和滑动,这种方式会启动硬件加速提升性能。但是毕竟是在所有轮播图的基础上的渲染。
- 第二种方式,通过切换item的display属性来实现对应item的显示和隐藏,虽然会引起回流和重绘,但是每个item的position为absolute,脱离文档流,所以并不会引起其他dom的回流和重绘。每个item的translate3d引发的渲染只是在当前item的基础上。
- 通过上面分析,可以得出: 如果轮播图的数量不多,第一种方式不会引起回流和重绘,并且translate引发渲染的item不多,性能相对好;但是轮播图的数量比较多的话,第二种性能相对比较好。
swipe接入示例
html代码
<div id="app"> <div class="swipe-wrapper"> <mt-swipe :auto="0" ref="swipeWrapper"> <mt-swipe-item class="swip-item-1 item">1</mt-swipe-item> <mt-swipe-item class="swip-item-2 item">2</mt-swipe-item> <mt-swipe-item class="swip-item-3 item">3</mt-swipe-item> </mt-swipe> </div> <div class="button-wrapper"> <button class="prev-button flex-item" @click="prev">prev</button> <button class="next-button flex-item" @click="next">next</button> </div></div>
css代码
<!-- 引入组件库css --><link rel="stylesheet" href="../css/mint-style.css"><style> html,body{ width: 100%; height: 100%; margin: 0; } #app{ width: 100%; height: 100%; } .swipe-wrapper{ width: 100%; height: 300px; } .swip-item-1{ background: red; } .swip-item-2{ background: blue; } .swip-item-3{ background: green; } .item{ text-align: center; font-size: 40px; color: white; } .button-wrapper{ display: flex; height: 100px; } .flex-item{ flex: 1; display: inline-block; text-align: center; height: 100%; line-height: 100%; font-size: 40px; } .prev-button{ background: darkorange; } .next-button{ background: green; }</style>
js代码
<!-- 先引入 Vue --><script src="../js/vue.js"></script><!-- 引入组件库 --><script src="../js/index.js"></script><script> new Vue({ el: '#app', methods: { prev: function () { this.$refs.swipeWrapper.prev(); console.log(this.$children); }, next: function () { this.$refs.swipeWrapper.next(); } } });</script>
原理解析
- 初始只显示选中index的item,将其他item都隐藏
- 当拖动开始的时候,显示当前index的相邻两个item
- 当拖动的时候,计算出手指滑动的距离,通过设置当前item和其相关两个item的translate3d来改变他们的位置的方式,来实现切换的效果
- 自动播放:通过设置定时器,触发上面拖动相同的切换代码,来实现切换。
源码解析
首先看子组件swipe-item组件,代码很简单,如下:
<template> <div class="mint-swipe-item"> <slot></slot> </div></template><script> export default { name: 'mt-swipe-item', mounted() { // 页面显示的时候触发 this.$parent && this.$parent.swipeItemCreated(this); // 内部实现:调用父组件的reinit()业务 }, destroyed() { // item隐藏的时候触发 this.$parent && this.$parent.swipeItemDestroyed(this); // 内部实现同上 } };</script>
上面的代码很简单,mounted和destoryed都是调用的父组件的实现。实现如下:
swipeItemCreated() { if (!this.ready) return; clearTimeout(this.reInitTimer); this.reInitTimer = setTimeout(() => { this.reInitPages(); }, 100); }, swipeItemDestroyed() { if (!this.ready) return; clearTimeout(this.reInitTimer); this.reInitTimer = setTimeout(() => { this.reInitPages(); }, 100); },
父组件swipe的props
这些数据都是允许外部传入的,这是数据的含义说明可以参考mint-ui的官网的说明mint-ui swipe组件swipe组件的data说明
data() { return { ready: false, // 当前组件是否 mounted dragging: false, // 当前是否正在拖动 userScrolling: false, // 判定当前用户在上下滚动,就不执行drag动作 animating: false, // 当前是否在执行动画(也就是自动切换页面) index: 0, // 当前所在的item的index pages: [], // 存储当前child 的dom timer: null, // 自动播放的定时器 timerid reInitTimer: null, // item组件触发reInit触发 定时器id noDrag: false, // 存储是否运行拖动的标识 isDone: false // 当前动画是否执行完成 };}
上面的注释都是我通过分析源码得出的,有了说明,下面看代码就更容易了。
swipe组件的入口函数mounted回调的实现
mounted() { this.ready = true; this.initTimer();// 初始化自动播放的timer this.reInitPages(); // 初始化drag状态, 以及dom节点的样式信息 var element = this.$el; // 为当前组件的dom节点 注册touch时间 element.addEventListener('touchstart', (event) => { if (this.prevent) event.preventDefault(); if (this.stopPropagation) event.stopPropagation(); if (this.animating) return; // 如果当前在执行移动动画, 直接返回 this.dragging = true; // 设置dragging状态标识 this.userScrolling = false; // 重置 this.doOnTouchStart(event); }); element.addEventListener('touchmove', (event) => { if (!this.dragging) return; if (this.timer) this.clearTimer(); // 将当前自动播放停止 this.doOnTouchMove(event); }); element.addEventListener('touchend', (event) => { if (this.userScrolling) { // 纵向滚动,重置状态并返回 this.dragging = false; this.dragState = {}; return; } if (!this.dragging) return; this.initTimer(); // 启动自动播放定时器 this.doOnTouchEnd(event); this.dragging = false; // 重置拖动状态 });}
关于初始化自动播放的定时器的代码,最后在分析。现在来看初始化dom样式的reInitPages函数实现如下:
reInitPages() { var children = this.$children; // 设置拖动状态 this.noDrag = children.length === 1 && this.noDragWhenSingle; // 当前只有一个item,并且设置了只有一个不支持拖动 var pages = []; var intDefaultIndex = Math.floor(this.defaultIndex); var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) ? intDefaultIndex : 0; this.index = defaultIndex; // 设置当前显示的索引值 //初始化显示样式, 将当前index的item显示出来,其他的都隐藏 children.forEach(function(child, index) { pages.push(child.$el); removeClass(child.$el, 'is-active'); if (index === defaultIndex) { addClass(child.$el, 'is-active'); } }); // 设置所有轮播图的item的dom this.pages = pages; },
swipe的touchstart事件回调的处理
上面已经有了回调的代码,主要看处理的核心函数doOnTouchStart的实现如下:doOnTouchStart(event) { // 创建dragState, 包括touch事件的信息,当前drag item以及它前后两个item,并将其显示出来 if (this.noDrag) return; // 不支持拖动 var element = this.$el; var dragState = this.dragState; var touch = event.touches[0]; // 设置dragstate的信息(也就是当前滑动的信息数据) dragState.startTime = new Date(); dragState.startLeft = touch.pageX; dragState.startTop = touch.pageY; dragState.startTopAbsolute = touch.clientY; dragState.pageWidth = element.offsetWidth; dragState.pageHeight = element.offsetHeight; var prevPage = this.$children[this.index - 1]; var dragPage = this.$children[this.index]; var nextPage = this.$children[this.index + 1]; if (this.continuous && this.pages.length > 1) { // 当前支持循环播放, 并且pages的长度大于1 if (!prevPage) { prevPage = this.$children[this.$children.length - 1]; } if (!nextPage) { nextPage = this.$children[0]; } } dragState.prevPage = prevPage ? prevPage.$el : null; dragState.dragPage = dragPage ? dragPage.$el : null; dragState.nextPage = nextPage ? nextPage.$el : null; // 将当前index下的前后两个item显示出来 if (dragState.prevPage) { dragState.prevPage.style.display = 'block'; } if (dragState.nextPage) { dragState.nextPage.style.display = 'block'; } }
获取当前touchstart状态下面的拖动的状态信息(包括touch的信息,页面宽高,prev、current、next三个item的dom)。同时将prev、next显示出来。
touchmove事件回调的处理
doOnTouchMove(event) { if (this.noDrag) return; var dragState = this.dragState; var touch = event.touches[0]; dragState.currentLeft = touch.pageX; dragState.currentTop = touch.pageY; dragState.currentTopAbsolute = touch.clientY; //计算滑动的距离 var offsetLeft = dragState.currentLeft - dragState.startLeft; var offsetTop = dragState.currentTopAbsolute - dragState.startTopAbsolute; var distanceX = Math.abs(offsetLeft); var distanceY = Math.abs(offsetTop); // 判断是 竖向滚动,还是横向滚动 if (distanceX < 5 || (distanceX >= 5 && distanceY >= 1.73 * distanceX)) { this.userScrolling = true; // 判定当前用户在上下滚动,就不执行drag动作 return; } else { this.userScrolling = false; event.preventDefault(); // 阻止默认事件的触发,也就是点击事件的触发 } // 设置最大的拖拽距离在当前dom里面 offsetLeft = Math.min(Math.max(-dragState.pageWidth + 1, offsetLeft), dragState.pageWidth - 1); var towards = offsetLeft < 0 ? 'next' : 'prev'; // 拖动的方向的确定 //prev方向: prev dom移动到指定的位置 if (dragState.prevPage && towards === 'prev') { this.translate(dragState.prevPage, offsetLeft - dragState.pageWidth); } // current dom移动到指定的位置 this.translate(dragState.dragPage, offsetLeft); // next方向: next dom 移动到指定的位置 if (dragState.nextPage && towards === 'next') { this.translate(dragState.nextPage, offsetLeft + dragState.pageWidth); } }
主要确定当前滚动不是竖向滚动,并确定滚动的方向以确定移动prev还是next。
下面看translate移动dom的核心函数实现:/** * @param element 要移动的dom节点 * @param offset // dom移动的距离 * @param speed 如果传递, 执行动画的移动; 没有,则直接translate执行的距离 * @param callback 处理完成的回调函数 */ translate(element, offset, speed, callback) { if (speed) { this.animating = true; // 当前正在执行动画,此时不能拖拽 element.style.webkitTransition = '-webkit-transform ' + speed + 'ms ease-in-out'; // transition过渡状态 setTimeout(() => { element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`; }, 50); var called = false; var transitionEndCallback = () => { if (called) return; called = true; this.animating = false; // 停止动画 element.style.webkitTransition = ''; element.style.webkitTransform = ''; if (callback) { callback.apply(this, arguments); // 调用回调 } }; once(element, 'webkitTransitionEnd', transitionEndCallback); // 此事件只执行一次 // 防止低版本android, 无法触发此事件 setTimeout(transitionEndCallback, speed + 100); // webkitTransitionEnd maybe not fire on lower version android. } else { element.style.webkitTransition = ''; element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`; } }
如果设置了speed,就会执行平滑的动画切换(speed是动画执行的时间);如果没有设置,直接移动到指定的位置,没有过渡效果。
touchend事件回调的实现
分析其中核心代码doTouchEnd函数:doOnTouchEnd() { if (this.noDrag) return; var dragState = this.dragState; var dragDuration = new Date() - dragState.startTime; var towards = null; // 决定下面进入哪个页面, null: 当前页面, prev: 前一个页面, next: 下一个页面 var offsetLeft = dragState.currentLeft - dragState.startLeft; var offsetTop = dragState.currentTop - dragState.startTop; var pageWidth = dragState.pageWidth; var index = this.index; var pageCount = this.pages.length; // 判断当前是否是 tap事件(轻触事件) if (dragDuration < 300) { let fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop) < 5; if (isNaN(offsetLeft) || isNaN(offsetTop)) { fireTap = true; } if (fireTap) { this.$children[this.index].$emit('tap'); // 当前轮播图item发送给外部的tab事件 } } // 触发时长小于300ms,并且没有执行touchmove事件, 不处理 if (dragDuration < 300 && dragState.currentLeft === undefined) return; if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) { towards = offsetLeft < 0 ? 'next' : 'prev'; } if (!this.continuous) { // 当前不支持循环, 向前或向后 都回到当前页面 if ((index === 0 && towards === 'prev') || (index === pageCount - 1 && towards === 'next')) { towards = null; } } if (this.$children.length < 2) { towards = null; } // 动画的方式切换到指定的item this.doAnimate(towards, { offsetLeft: offsetLeft, pageWidth: dragState.pageWidth, prevPage: dragState.prevPage, currentPage: dragState.dragPage, nextPage: dragState.nextPage }); this.dragState = {};// 清空dragState }
判断当前是否是tap事件,并且确定下面要切换到哪个item。
下面来看doAnimate动画的方式切换的实现:doAnimate(towards, options) { if (this.$children.length === 0) return; if (!options && this.$children.length < 2) return; var prevPage, nextPage, currentPage, pageWidth, offsetLeft; var speed = this.speed || 300; var index = this.index; var pages = this.pages; var pageCount = pages.length; if (!options) { // 没有options,是 自动播放或手动触发切换页面的处理 pageWidth = this.$el.clientWidth; currentPage = pages[index]; prevPage = pages[index - 1]; nextPage = pages[index + 1]; if (this.continuous && pages.length > 1) { if (!prevPage) { prevPage = pages[pages.length - 1]; } if (!nextPage) { nextPage = pages[0]; } } // 将 prevPage 和 nextPage 定位到应该的位置(也就是开始执行切换页面的位置) if (prevPage) { prevPage.style.display = 'block'; // 显示出来 this.translate(prevPage, -pageWidth); // 移到当前index的前面 } if (nextPage) { nextPage.style.display = 'block'; this.translate(nextPage, pageWidth); // 移到当前index的后面 } } else { prevPage = options.prevPage; currentPage = options.currentPage; nextPage = options.nextPage; pageWidth = options.pageWidth; offsetLeft = options.offsetLeft; } // 确定 要切换的item的索引 var newIndex; var oldPage = this.$children[index].$el; if (towards === 'prev') { if (index > 0) { newIndex = index - 1; } if (this.continuous && index === 0) { newIndex = pageCount - 1; } } else if (towards === 'next') { if (index < pageCount - 1) { newIndex = index + 1; } if (this.continuous && index === pageCount - 1) { newIndex = 0; } } var callback = () => { // 动画完成的回调: 重置dom的样式信息 if (newIndex !== undefined) { // 重置dom的样式信息 var newPage = this.$children[newIndex].$el; removeClass(oldPage, 'is-active'); // is-active 设置当前item的display:block addClass(newPage, 'is-active'); this.index = newIndex; } if (this.isDone) { // 切换了页面,向外部发送切换页面完成的事件 this.end(); } // 在touchStart 时设置的style中的display清空, 也就是使用class里面的display:none隐藏属性 if (prevPage) { prevPage.style.display = ''; } if (nextPage) { nextPage.style.display = ''; } }; setTimeout(() => { if (towards === 'next') { // 切换到下一页 this.isDone = true; this.before(currentPage); // 执行切换页面之前,向外部发送事件 this.translate(currentPage, -pageWidth, speed, callback); if (nextPage) { this.translate(nextPage, 0, speed); } } else if (towards === 'prev') { // 切换到上一页 this.isDone = true; this.before(currentPage); this.translate(currentPage, pageWidth, speed, callback); if (prevPage) { this.translate(prevPage, 0, speed); } } else { // 回到当前页面,不切换页面 this.isDone = false; // 当前没有进入到前一个页面和后一个页面, 还是回到当前页面 this.translate(currentPage, 0, speed, callback); if (typeof offsetLeft !== 'undefined') { if (prevPage && offsetLeft > 0) { this.translate(prevPage, pageWidth * -1, speed); } if (nextPage && offsetLeft < 0) { this.translate(nextPage, pageWidth, speed); } } else { if (prevPage) { this.translate(prevPage, pageWidth * -1, speed); } if (nextPage) { this.translate(nextPage, pageWidth, speed); } } } }, 10); }
此函数代码比较,但是不难,结合上面的注释,应该很容易读懂。实现:如果没有options(手动触发切换页面),会生成options中的信息(也就是下面处理需要用到的数据),并把prev和next两个dom定位到指定的位置;然后执行切换页面的操作,并且在结束的回调中重置相应dom的样式以及当前选中的index。
下面来看 手动触发的切换页面的代码
next() { // 切换到下一个页面 this.doAnimate('next'); }, prev() { // 切换到上一个页面 this.doAnimate('prev'); }
最后,来看自动播放的代码:
initTimer() { if (this.auto > 0) { this.timer = setInterval(() => { // 如果不支持循环播放,并且当前播放到了 末尾位置, 停止定时器 if (!this.continuous && (this.index >= this.pages.length - 1)) { return this.clearTimer(); } if (!this.dragging && !this.animating) { // 没有在拖动, 也没有执行动画 this.next(); // 播放下一个item } }, this.auto); } }
读懂了doAnimate函数了,就很简单了。
总结
- 了解了一种新的轮播图的实现方式
- 两个轮播图的实现方式的差别以及性能的比较
- touch事件边界条件的处理,比如tap事件的判断,横纵向滚动的判断
- mint-ui swipe组件源码解析
- mint-ui swipe组件解析
- vue mint-ui源码解析之loadmore组件
- mint-ui —— cell swipe的使用
- 【vue 组件 mint-ui】 看了一下源码,给轮播图Swiper封装自定义跳转的函数
- vue mint-ui tabbar变组件使用
- Mint UI -- 基于Vue.js的移动端组件库
- 基于 Vue.js 的移动端组件库mint-ui
- UI组件——SwipeRefrshLayout最详细的源码解析——UI绘制
- swipe源码循环索引
- UI-Router源码解析
- ui.router 源码解析
- ui.router源码解析
- UI组件——SwipeRefreshLayout源码解析——MaterialProgressDrawable的绘制
- Vue搭配mint-ui
- Mint-ui MessageBox.confirm
- vue引入Mint-UI
- vue mint-ui 使用手册
- python-class__dict__setattr__getattr__
- 在github上创建和删除新分支
- git的使用
- MySQL索引类型 btree索引和hash索引的区别
- Java学习——Servlet 本质是什么 servlet运行原理
- mint-ui swipe组件源码解析
- 网络iso七层协议含义
- Android 实现两次点击返回键 提示退出
- 207. Course Schedule
- Vue SPA + Nodejs项目实战
- bugku Reverse Easy_vb wirteup
- hibernate查询结果映射到实体和map的方法
- 原型模式(Prototype)(创建型模式)
- Jquery.flotX轴日期对应不上