import baseComponent from '../helpers/baseComponent' import styleToCssString from '../helpers/styleToCssString' const ENTER = 'enter' const ENTERING = 'entering' const ENTERED = 'entered' const EXIT = 'exit' const EXITING = 'exiting' const EXITED = 'exited' const UNMOUNTED = 'unmounted' const TRANSITION = 'transition' const ANIMATION = 'animation' const TIMEOUT = 1000 / 60 const defaultClassNames = { enter: '', // 进入过渡的开始状态,在过渡过程完成之后移除 enterActive: '', // 进入过渡的结束状态,在过渡过程完成之后移除 enterDone: '', // 进入过渡的完成状态 exit: '', // 离开过渡的开始状态,在过渡过程完成之后移除 exitActive: '', // 离开过渡的结束状态,在过渡过程完成之后移除 exitDone: '', // 离开过渡的完成状态 } baseComponent({ properties: { // 触发组件进入或离开过渡的状态 in: { type: Boolean, value: false, observer(newVal) { if (this.data.isMounting) { this.updated(newVal) } }, }, // 过渡的类名 classNames: { type: null, value: defaultClassNames, }, // 过渡持续时间 duration: { type: null, value: null, }, // 过渡动效的类型 type: { type: String, value: TRANSITION, }, // 首次挂载时是否触发进入过渡 appear: { type: Boolean, value: false, }, // 是否启用进入过渡 enter: { type: Boolean, value: true, }, // 是否启用离开过渡 exit: { type: Boolean, value: true, }, // 首次进入过渡时是否懒挂载组件 mountOnEnter: { type: Boolean, value: true, }, // 离开过渡完成时是否卸载组件 unmountOnExit: { type: Boolean, value: true, }, // 自定义类名 wrapCls: { type: String, value: '', }, // 自定义样式 wrapStyle: { type: [String, Object], value: '', observer(newVal) { this.setData({ extStyle: styleToCssString(newVal), }) }, }, disableScroll: { type: Boolean, value: false, }, }, data: { animateCss: '', // 动画样式 animateStatus: EXITED, // 动画状态,可选值 entering、entered、exiting、exited isMounting: false, // 是否首次挂载 extStyle: '', // 组件样式 }, methods: { /** * 监听过渡或动画的回调函数 */ addEventListener() { const { animateStatus } = this.data const { enter, exit } = this.getTimeouts() if (animateStatus === ENTERING && !enter && this.data.enter) { this.performEntered() } if (animateStatus === EXITING && !exit && this.data.exit) { this.performExited() } }, /** * 会在 WXSS transition 或 wx.createAnimation 动画结束后触发 */ onTransitionEnd() { if (this.data.type === TRANSITION) { this.addEventListener() } }, /** * 会在一个 WXSS animation 动画完成时触发 */ onAnimationEnd() { if (this.data.type === ANIMATION) { this.addEventListener() } }, /** * 更新组件状态 * @param {String} nextStatus 下一状态,ENTERING 或 EXITING * @param {Boolean} mounting 是否首次挂载 */ updateStatus(nextStatus, mounting = false) { if (nextStatus !== null) { this.cancelNextCallback() this.isAppearing = mounting if (nextStatus === ENTERING) { this.performEnter() } else { this.performExit() } } }, /** * 进入过渡 */ performEnter() { const { className, activeClassName } = this.getClassNames(ENTER) const { enter } = this.getTimeouts() const enterParams = { animateStatus: ENTER, animateCss: className, } const enteringParams = { animateStatus: ENTERING, animateCss: `${className} ${activeClassName}`, } // 若已禁用进入过渡,则更新状态至 ENTERED if (!this.isAppearing && !this.data.enter) { return this.performEntered() } // 第一阶段:设置进入过渡的开始状态,并触发 ENTER 事件 // 第二阶段:延迟一帧后,设置进入过渡的结束状态,并触发 ENTERING 事件 // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发进入过渡完成 performEntered,否则等待触发 onTransitionEnd 或 onAnimationEnd this.safeSetData(enterParams, () => { this.triggerEvent('change', { animateStatus: ENTER }) this.triggerEvent(ENTER, { isAppearing: this.isAppearing }) // 由于有些时候不能正确的触发动画完成的回调,具体原因未知 // 所以采用延迟一帧的方式来确保可以触发回调 this.delayHandler(TIMEOUT, () => { this.safeSetData(enteringParams, () => { this.triggerEvent('change', { animateStatus: ENTERING }) this.triggerEvent(ENTERING, { isAppearing: this.isAppearing }) if (enter) { this.delayHandler(enter, this.performEntered) } }) }) }) }, /** * 进入过渡完成 */ performEntered() { const { doneClassName } = this.getClassNames(ENTER) const enteredParams = { animateStatus: ENTERED, animateCss: doneClassName, } // 第三阶段:设置进入过渡的完成状态,并触发 ENTERED 事件 this.safeSetData(enteredParams, () => { this.triggerEvent('change', { animateStatus: ENTERED }) this.triggerEvent(ENTERED, { isAppearing: this.isAppearing }) }) }, /** * 离开过渡 */ performExit() { const { className, activeClassName } = this.getClassNames(EXIT) const { exit } = this.getTimeouts() const exitParams = { animateStatus: EXIT, animateCss: className, } const exitingParams = { animateStatus: EXITING, animateCss: `${className} ${activeClassName}`, } // 若已禁用离开过渡,则更新状态至 EXITED if (!this.data.exit) { return this.performExited() } // 第一阶段:设置离开过渡的开始状态,并触发 EXIT 事件 // 第二阶段:延迟一帧后,设置离开过渡的结束状态,并触发 EXITING 事件 // 第三阶段:若已设置过渡的持续时间,则延迟指定时间后触发离开过渡完成 performExited,否则等待触发 onTransitionEnd 或 onAnimationEnd this.safeSetData(exitParams, () => { this.triggerEvent('change', { animateStatus: EXIT }) this.triggerEvent(EXIT) this.delayHandler(TIMEOUT, () => { this.safeSetData(exitingParams, () => { this.triggerEvent('change', { animateStatus: EXITING }) this.triggerEvent(EXITING) if (exit) { this.delayHandler(exit, this.performExited) } }) }) }) }, /** * 离开过渡完成 */ performExited() { const { doneClassName } = this.getClassNames(EXIT) const exitedParams = { animateStatus: EXITED, animateCss: doneClassName, } // 第三阶段:设置离开过渡的完成状态,并触发 EXITED 事件 this.safeSetData(exitedParams, () => { this.triggerEvent('change', { animateStatus: EXITED }) this.triggerEvent(EXITED) // 判断离开过渡完成时是否卸载组件 if (this.data.unmountOnExit) { this.setData({ animateStatus: UNMOUNTED }, () => { this.triggerEvent('change', { animateStatus: UNMOUNTED }) }) } }) }, /** * 获取指定状态下的类名 * @param {String} type 过渡类型,enter 或 exit */ getClassNames(type) { const { classNames } = this.data const className = typeof classNames !== 'string' ? classNames[type] : `${classNames}-${type}` const activeClassName = typeof classNames !== 'string' ? classNames[`${type}Active`] : `${classNames}-${type}-active` const doneClassName = typeof classNames !== 'string' ? classNames[`${type}Done`] : `${classNames}-${type}-done` return { className, activeClassName, doneClassName, } }, /** * 获取过渡持续时间 */ getTimeouts() { const { duration } = this.data if (duration !== null && typeof duration === 'object') { return { enter: duration.enter, exit: duration.exit, } } else if (typeof duration === 'number') { return { enter: duration, exit: duration, } } return {} }, /** * 属性值 in 被更改时的响应函数 * @param {Boolean} newVal 触发组件进入或离开过渡的状态 */ updated(newVal) { let { animateStatus } = this.pendingData || this.data let nextStatus = null if (newVal) { if (animateStatus === UNMOUNTED) { animateStatus = EXITED this.setData({ animateStatus: EXITED }, () => { this.triggerEvent('change', { animateStatus: EXITED }) }) } if (animateStatus !== ENTER && animateStatus !== ENTERING && animateStatus !== ENTERED) { nextStatus = ENTERING } } else { if (animateStatus === ENTER || animateStatus === ENTERING || animateStatus === ENTERED) { nextStatus = EXITING } } this.updateStatus(nextStatus) }, /** * 延迟一段时间触发回调 * @param {Number} timeout 延迟时间 * @param {Function} handler 回调函数 */ delayHandler(timeout, handler) { if (timeout) { this.setNextCallback(handler) setTimeout(this.nextCallback, timeout) } }, /** * 点击事件 */ onTap() { this.triggerEvent('click') }, /** * 阻止移动触摸 */ noop() {}, }, attached() { let animateStatus = null let appearStatus = null if (this.data.in) { if (this.data.appear) { animateStatus = EXITED appearStatus = ENTERING } else { animateStatus = ENTERED } } else { if (this.data.unmountOnExit || this.data.mountOnEnter) { animateStatus = UNMOUNTED } else { animateStatus = EXITED } } // 由于小程序组件首次挂载时 observer 事件总是优先于 attached 事件 // 所以使用 isMounting 来强制优先触发 attached 事件 this.safeSetData({ animateStatus, isMounting: true }, () => { this.triggerEvent('change', { animateStatus }) this.updateStatus(appearStatus, true) }) }, })