You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
13 KiB
382 lines
13 KiB
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)
|
|
})
|
|
},
|
|
})
|
|
|