interface ActivityPopupAnimationState { x: number; y: number; scaleX: number; scaleY: number; opacity: number; mask: cc.Node; maskOpacity: number; } const ANIMATION_STATE_KEY = "__activityPopupAnimationState"; export enum ActivityPopupAnimationType { "左侧滑入" = 0, "Q弹弹出" = 1 } /** * Common enter/exit animation for activity popups. * Only the content panel slides and scales; the full-screen mask fades separately. */ export default class ActivityPopupAnimator { public static open( content: cc.Node, mask?: cc.Node, animationType: ActivityPopupAnimationType = ActivityPopupAnimationType.左侧滑入, onComplete?: () => void ): void { if (!content || !cc.isValid(content, true)) { if (onComplete) onComplete(); return; } const state = this.getState(content, mask); content.stopAllActions(); content.active = true; this.openMask(mask, state.maskOpacity); if (animationType === ActivityPopupAnimationType.Q弹弹出) { this.openElastic(content, state, onComplete); } else { this.openFromLeft(content, state, onComplete); } } public static close( content: cc.Node, mask?: cc.Node, animationType: ActivityPopupAnimationType = ActivityPopupAnimationType.左侧滑入, onComplete?: () => void ): void { if (!content || !cc.isValid(content, true)) { if (onComplete) onComplete(); return; } const state = this.getState(content, mask); content.stopAllActions(); this.closeMask(mask); if (animationType === ActivityPopupAnimationType.Q弹弹出) { this.closeElastic(content, mask, state, onComplete); } else { this.closeToLeft(content, mask, state, onComplete); } } private static openFromLeft( content: cc.Node, state: ActivityPopupAnimationState, onComplete?: () => void ): void { const startX = state.x - this.getSlideDistance(content); content.setPosition(startX, state.y); content.scaleX = state.scaleX * 0.72; content.scaleY = state.scaleY * 0.72; content.opacity = 0; cc.tween(content) .to(0.24, { x: state.x, y: state.y, scaleX: state.scaleX * 0.94, scaleY: state.scaleY * 0.94, opacity: state.opacity }, { easing: "cubicOut" }) .to(0.10, { scaleX: state.scaleX * 1.06, scaleY: state.scaleY * 1.06 }, { easing: "backOut" }) .to(0.12, { scaleX: state.scaleX, scaleY: state.scaleY }, { easing: "sineOut" }) .call(() => { if (onComplete) onComplete(); }) .start(); } private static openElastic( content: cc.Node, state: ActivityPopupAnimationState, onComplete?: () => void ): void { content.setPosition(state.x, state.y); content.scaleX = state.scaleX * 0.36; content.scaleY = state.scaleY * 0.36; content.opacity = 0; cc.tween(content) .to(0.18, { scaleX: state.scaleX * 1.12, scaleY: state.scaleY * 0.88, opacity: state.opacity }, { easing: "backOut" }) .to(0.09, { scaleX: state.scaleX * 0.94, scaleY: state.scaleY * 1.08 }, { easing: "sineOut" }) .to(0.08, { scaleX: state.scaleX * 1.04, scaleY: state.scaleY * 0.97 }, { easing: "sineInOut" }) .to(0.08, { scaleX: state.scaleX, scaleY: state.scaleY }, { easing: "sineOut" }) .call(() => { if (onComplete) onComplete(); }) .start(); } private static closeToLeft( content: cc.Node, mask: cc.Node, state: ActivityPopupAnimationState, onComplete?: () => void ): void { const endX = state.x - this.getSlideDistance(content); cc.tween(content) .to(0.08, { scaleX: state.scaleX * 1.06, scaleY: state.scaleY * 1.06 }, { easing: "sineOut" }) .to(0.22, { x: endX, scaleX: state.scaleX * 0.72, scaleY: state.scaleY * 0.72, opacity: 0 }, { easing: "cubicIn" }) .call(() => this.finishClose(content, mask, state, onComplete)) .start(); } private static closeElastic( content: cc.Node, mask: cc.Node, state: ActivityPopupAnimationState, onComplete?: () => void ): void { cc.tween(content) .to(0.07, { scaleX: state.scaleX * 0.94, scaleY: state.scaleY * 1.06 }, { easing: "sineOut" }) .to(0.08, { scaleX: state.scaleX * 1.08, scaleY: state.scaleY * 0.90 }, { easing: "sineInOut" }) .to(0.15, { scaleX: state.scaleX * 0.36, scaleY: state.scaleY * 0.36, opacity: 0 }, { easing: "backIn" }) .call(() => this.finishClose(content, mask, state, onComplete)) .start(); } private static openMask(mask: cc.Node, opacity: number): void { if (!mask || !cc.isValid(mask, true)) return; mask.stopAllActions(); mask.active = true; mask.opacity = 0; cc.tween(mask) .to(0.24, { opacity }, { easing: "sineOut" }) .start(); } private static closeMask(mask: cc.Node): void { if (!mask || !cc.isValid(mask, true)) return; mask.stopAllActions(); cc.tween(mask) .to(0.30, { opacity: 0 }, { easing: "sineIn" }) .start(); } private static finishClose( content: cc.Node, mask: cc.Node, state: ActivityPopupAnimationState, onComplete?: () => void ): void { this.restore(content, state); content.active = false; if (mask && cc.isValid(mask, true)) { mask.stopAllActions(); mask.opacity = 0; mask.active = false; } if (onComplete) onComplete(); } private static getState(content: cc.Node, mask?: cc.Node): ActivityPopupAnimationState { let state = (content as any)[ANIMATION_STATE_KEY] as ActivityPopupAnimationState; if (!state) { state = { x: content.x, y: content.y, scaleX: content.scaleX, scaleY: content.scaleY, opacity: content.opacity, mask: mask || null, maskOpacity: mask ? mask.opacity : 0 }; (content as any)[ANIMATION_STATE_KEY] = state; } else if (mask && state.mask !== mask) { state.mask = mask; state.maskOpacity = mask.opacity; } return state; } private static getSlideDistance(content: cc.Node): number { const parentWidth = content.parent ? content.parent.width : 0; return Math.max(content.width, parentWidth * 0.92); } private static restore(content: cc.Node, state: ActivityPopupAnimationState): void { content.setPosition(state.x, state.y); content.scaleX = state.scaleX; content.scaleY = state.scaleY; content.opacity = state.opacity; } }