| | |
| | | <template> |
| | | <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view> |
| | | </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: '' |
| | | } |
| | | }, |
| | | 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 ? 1 : 0, |
| | | '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> |
| | | <template>
|
| | | <view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
|
| | | </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: ''
|
| | | }
|
| | | },
|
| | | 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 ? 1 : 0,
|
| | | '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>
|