247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
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;
|
|
}
|
|
}
|