<template> <!-- #ifndef APP-NVUE --> <view v-show="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view> <!-- #endif --> <!-- #ifdef APP-NVUE --> <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view> <!-- #endif --> </template> <script> import { createAnimation } from './createAnimation' /** * Transition 过渡动画 * @description 简单过渡动画组件 * @tutorial https://ext.dcloud.net.cn/plugin?id=985 * @property {Boolean} show = [false|true] 控制组件显示或隐藏 * @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型 * @value fade 渐隐渐出过渡 * @value slide-top 由上至下过渡 * @value slide-right 由右至左过渡 * @value slide-bottom 由下至上过渡 * @value slide-left 由左至右过渡 * @value zoom-in 由小到大过渡 * @value zoom-out 由大到小过渡 * @property {Number} duration 过渡动画持续时间 * @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red` */ export default { name: 'uniTransition', emits:['click','change'], props: { show: { type: Boolean, default: false }, modeClass: { type: [Array, String], default() { return 'fade' } }, duration: { type: Number, default: 300 }, styles: { type: Object, default() { return {} } }, customClass:{ type: String, default: '' }, onceRender:{ type:Boolean, default:false }, }, data() { return { isShow: false, transform: '', opacity: 1, animationData: {}, durationTime: 300, config: {} } }, watch: { show: { handler(newVal) { if (newVal) { this.open() } else { // 避免上来就执行 close,导致动画错乱 if (this.isShow) { this.close() } } }, immediate: true } }, computed: { // 生成样式数据 stylesObject() { let styles = { ...this.styles, 'transition-duration': this.duration / 1000 + 's' } let transform = '' for (let i in styles) { let line = this.toLine(i) transform += line + ':' + styles[i] + ';' } return transform }, // 初始化动画条件 transformStyles() { return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject } }, created() { // 动画默认配置 this.config = { duration: this.duration, timingFunction: 'ease', transformOrigin: '50% 50%', delay: 0 } this.durationTime = this.duration }, methods: { /** * ref 触发 初始化动画 */ init(obj = {}) { if (obj.duration) { this.durationTime = obj.duration } this.animation = createAnimation(Object.assign(this.config, obj),this) }, /** * 点击组件触发回调 */ onClick() { this.$emit('click', { detail: this.isShow }) }, /** * ref 触发 动画分组 * @param {Object} obj */ step(obj, config = {}) { if (!this.animation) return for (let i in obj) { try { if(typeof obj[i] === 'object'){ this.animation[i](...obj[i]) }else{ this.animation[i](obj[i]) } } catch (e) { console.error(`方法 ${i} 不存在`) } } this.animation.step(config) return this }, /** * ref 触发 执行动画 */ run(fn) { if (!this.animation) return this.animation.run(fn) }, // 开始过度动画 open() { clearTimeout(this.timer) this.transform = '' this.isShow = true let { opacity, transform } = this.styleInit(false) if (typeof opacity !== 'undefined') { this.opacity = opacity } this.transform = transform // 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常 this.$nextTick(() => { // TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器 this.timer = setTimeout(() => { this.animation = createAnimation(this.config, this) this.tranfromInit(false).step() this.animation.run() this.$emit('change', { detail: this.isShow }) }, 20) }) }, // 关闭过度动画 close(type) { if (!this.animation) return this.tranfromInit(true) .step() .run(() => { this.isShow = false this.animationData = null this.animation = null let { opacity, transform } = this.styleInit(false) this.opacity = opacity || 1 this.transform = transform this.$emit('change', { detail: this.isShow }) }) }, // 处理动画开始前的默认样式 styleInit(type) { let styles = { transform: '' } let buildStyle = (type, mode) => { if (mode === 'fade') { styles.opacity = this.animationType(type)[mode] } else { styles.transform += this.animationType(type)[mode] + ' ' } } if (typeof this.modeClass === 'string') { buildStyle(type, this.modeClass) } else { this.modeClass.forEach(mode => { buildStyle(type, mode) }) } return styles }, // 处理内置组合动画 tranfromInit(type) { let buildTranfrom = (type, mode) => { let aniNum = null if (mode === 'fade') { aniNum = type ? 0 : 1 } else { aniNum = type ? '-100%' : '0' if (mode === 'zoom-in') { aniNum = type ? 0.8 : 1 } if (mode === 'zoom-out') { aniNum = type ? 1.2 : 1 } if (mode === 'slide-right') { aniNum = type ? '100%' : '0' } if (mode === 'slide-bottom') { aniNum = type ? '100%' : '0' } } this.animation[this.animationMode()[mode]](aniNum) } if (typeof this.modeClass === 'string') { buildTranfrom(type, this.modeClass) } else { this.modeClass.forEach(mode => { buildTranfrom(type, mode) }) } return this.animation }, animationType(type) { return { fade: type ? 0 : 1, 'slide-top': `translateY(${type ? '0' : '-100%'})`, 'slide-right': `translateX(${type ? '0' : '100%'})`, 'slide-bottom': `translateY(${type ? '0' : '100%'})`, 'slide-left': `translateX(${type ? '0' : '-100%'})`, 'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`, 'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})` } }, // 内置动画类型与实际动画对应字典 animationMode() { return { fade: 'opacity', 'slide-top': 'translateY', 'slide-right': 'translateX', 'slide-bottom': 'translateY', 'slide-left': 'translateX', 'zoom-in': 'scale', 'zoom-out': 'scale' } }, // 驼峰转中横线 toLine(name) { return name.replace(/([A-Z])/g, '-$1').toLowerCase() } } } </script> <style></style>