Initial Commit
This commit is contained in:
commit
1d71a02738
237 changed files with 64293 additions and 0 deletions
564
utils/animation-manager.js
Normal file
564
utils/animation-manager.js
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
// 动画管理器 - 微信小程序专用
|
||||
// 提供统一的动画效果和页面过渡管理
|
||||
|
||||
/**
|
||||
* 动画管理器
|
||||
* 功能:
|
||||
* 1. 页面过渡动画
|
||||
* 2. 组件动画效果
|
||||
* 3. 交互反馈动画
|
||||
* 4. 加载状态动画
|
||||
* 5. 手势动画
|
||||
* 6. 性能优化动画
|
||||
*/
|
||||
class AnimationManager {
|
||||
constructor() {
|
||||
this.isInitialized = false;
|
||||
|
||||
// 动画配置
|
||||
this.config = {
|
||||
// 动画开关
|
||||
enabled: true,
|
||||
|
||||
// 性能模式
|
||||
performanceMode: 'auto', // auto, high, low
|
||||
|
||||
// 动画时长配置
|
||||
durations: {
|
||||
fast: 200,
|
||||
normal: 300,
|
||||
slow: 500,
|
||||
page: 400
|
||||
},
|
||||
|
||||
// 缓动函数配置
|
||||
easings: {
|
||||
ease: 'ease',
|
||||
easeIn: 'ease-in',
|
||||
easeOut: 'ease-out',
|
||||
easeInOut: 'ease-in-out',
|
||||
linear: 'linear',
|
||||
spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)'
|
||||
},
|
||||
|
||||
// 页面过渡配置
|
||||
pageTransitions: {
|
||||
slideLeft: {
|
||||
enter: { transform: 'translateX(100%)', opacity: 0 },
|
||||
active: { transform: 'translateX(0)', opacity: 1 },
|
||||
leave: { transform: 'translateX(-100%)', opacity: 0 }
|
||||
},
|
||||
slideRight: {
|
||||
enter: { transform: 'translateX(-100%)', opacity: 0 },
|
||||
active: { transform: 'translateX(0)', opacity: 1 },
|
||||
leave: { transform: 'translateX(100%)', opacity: 0 }
|
||||
},
|
||||
slideUp: {
|
||||
enter: { transform: 'translateY(100%)', opacity: 0 },
|
||||
active: { transform: 'translateY(0)', opacity: 1 },
|
||||
leave: { transform: 'translateY(-100%)', opacity: 0 }
|
||||
},
|
||||
slideDown: {
|
||||
enter: { transform: 'translateY(-100%)', opacity: 0 },
|
||||
active: { transform: 'translateY(0)', opacity: 1 },
|
||||
leave: { transform: 'translateY(100%)', opacity: 0 }
|
||||
},
|
||||
fade: {
|
||||
enter: { opacity: 0 },
|
||||
active: { opacity: 1 },
|
||||
leave: { opacity: 0 }
|
||||
},
|
||||
scale: {
|
||||
enter: { transform: 'scale(0.8)', opacity: 0 },
|
||||
active: { transform: 'scale(1)', opacity: 1 },
|
||||
leave: { transform: 'scale(1.2)', opacity: 0 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 动画实例缓存
|
||||
this.animationCache = new Map();
|
||||
|
||||
// 性能监控
|
||||
this.performanceStats = {
|
||||
animationCount: 0,
|
||||
averageDuration: 0,
|
||||
droppedFrames: 0
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化动画管理器
|
||||
init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
console.log('🎭 初始化动画管理器...');
|
||||
|
||||
try {
|
||||
// 检测设备性能
|
||||
this.detectPerformance();
|
||||
|
||||
// 设置全局动画样式
|
||||
this.setupGlobalStyles();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('✅ 动画管理器初始化完成');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 动画管理器初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎭 ===== 基础动画 =====
|
||||
|
||||
// 创建动画实例
|
||||
createAnimation(options = {}) {
|
||||
const defaultOptions = {
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
delay: 0,
|
||||
transformOrigin: '50% 50% 0'
|
||||
};
|
||||
|
||||
const animationOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// 根据性能模式调整动画
|
||||
if (this.config.performanceMode === 'low') {
|
||||
animationOptions.duration = Math.min(animationOptions.duration, 200);
|
||||
}
|
||||
|
||||
const animation = wx.createAnimation(animationOptions);
|
||||
|
||||
// 缓存动画实例
|
||||
const animationId = this.generateAnimationId();
|
||||
this.animationCache.set(animationId, {
|
||||
animation: animation,
|
||||
options: animationOptions,
|
||||
createTime: Date.now()
|
||||
});
|
||||
|
||||
return { animation, animationId };
|
||||
}
|
||||
|
||||
// 淡入动画
|
||||
fadeIn(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation.opacity(1).step();
|
||||
}
|
||||
|
||||
// 淡出动画
|
||||
fadeOut(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeIn,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation.opacity(0).step();
|
||||
}
|
||||
|
||||
// 滑入动画
|
||||
slideIn(direction = 'left', options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.spring,
|
||||
...options
|
||||
});
|
||||
|
||||
const transforms = {
|
||||
left: () => animation.translateX(0),
|
||||
right: () => animation.translateX(0),
|
||||
up: () => animation.translateY(0),
|
||||
down: () => animation.translateY(0)
|
||||
};
|
||||
|
||||
return transforms[direction]().opacity(1).step();
|
||||
}
|
||||
|
||||
// 滑出动画
|
||||
slideOut(direction = 'left', distance = '100%', options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeIn,
|
||||
...options
|
||||
});
|
||||
|
||||
const transforms = {
|
||||
left: () => animation.translateX(`-${distance}`),
|
||||
right: () => animation.translateX(distance),
|
||||
up: () => animation.translateY(`-${distance}`),
|
||||
down: () => animation.translateY(distance)
|
||||
};
|
||||
|
||||
return transforms[direction]().opacity(0).step();
|
||||
}
|
||||
|
||||
// 缩放动画
|
||||
scale(scale = 1, options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.spring,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation.scale(scale).step();
|
||||
}
|
||||
|
||||
// 旋转动画
|
||||
rotate(angle = 360, options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.slow,
|
||||
timingFunction: this.config.easings.linear,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation.rotate(angle).step();
|
||||
}
|
||||
|
||||
// 🎪 ===== 组合动画 =====
|
||||
|
||||
// 弹跳进入动画
|
||||
bounceIn(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.slow,
|
||||
timingFunction: this.config.easings.bounce,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.scale(0.3).opacity(0).step({ duration: 0 })
|
||||
.scale(1.05).opacity(1).step({ duration: 200 })
|
||||
.scale(0.95).step({ duration: 100 })
|
||||
.scale(1).step({ duration: 100 });
|
||||
}
|
||||
|
||||
// 弹跳退出动画
|
||||
bounceOut(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeIn,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.scale(1.1).step({ duration: 100 })
|
||||
.scale(0).opacity(0).step({ duration: 200 });
|
||||
}
|
||||
|
||||
// 摇摆动画
|
||||
shake(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.fast,
|
||||
timingFunction: this.config.easings.linear,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.translateX(-10).step({ duration: 50 })
|
||||
.translateX(10).step({ duration: 50 })
|
||||
.translateX(-10).step({ duration: 50 })
|
||||
.translateX(10).step({ duration: 50 })
|
||||
.translateX(0).step({ duration: 50 });
|
||||
}
|
||||
|
||||
// 脉冲动画
|
||||
pulse(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeInOut,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.scale(1).step({ duration: 0 })
|
||||
.scale(1.05).step({ duration: 150 })
|
||||
.scale(1).step({ duration: 150 });
|
||||
}
|
||||
|
||||
// 🎬 ===== 页面过渡 =====
|
||||
|
||||
// 页面进入动画
|
||||
pageEnter(transitionType = 'slideLeft', options = {}) {
|
||||
const transition = this.config.pageTransitions[transitionType];
|
||||
if (!transition) {
|
||||
console.warn('未知的页面过渡类型:', transitionType);
|
||||
return this.fadeIn(options);
|
||||
}
|
||||
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.page,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
|
||||
// 设置初始状态
|
||||
this.applyTransform(animation, transition.enter);
|
||||
animation.step({ duration: 0 });
|
||||
|
||||
// 执行进入动画
|
||||
this.applyTransform(animation, transition.active);
|
||||
return animation.step();
|
||||
}
|
||||
|
||||
// 页面退出动画
|
||||
pageLeave(transitionType = 'slideLeft', options = {}) {
|
||||
const transition = this.config.pageTransitions[transitionType];
|
||||
if (!transition) {
|
||||
console.warn('未知的页面过渡类型:', transitionType);
|
||||
return this.fadeOut(options);
|
||||
}
|
||||
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.page,
|
||||
timingFunction: this.config.easings.easeIn,
|
||||
...options
|
||||
});
|
||||
|
||||
// 执行退出动画
|
||||
this.applyTransform(animation, transition.leave);
|
||||
return animation.step();
|
||||
}
|
||||
|
||||
// 🎯 ===== 交互动画 =====
|
||||
|
||||
// 按钮点击动画
|
||||
buttonPress(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.fast,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.scale(0.95).step({ duration: 100 })
|
||||
.scale(1).step({ duration: 100 });
|
||||
}
|
||||
|
||||
// 卡片悬停动画
|
||||
cardHover(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.scale(1.02)
|
||||
.translateY(-2)
|
||||
.step();
|
||||
}
|
||||
|
||||
// 列表项滑动动画
|
||||
listItemSlide(direction = 'left', options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: this.config.durations.normal,
|
||||
timingFunction: this.config.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
|
||||
const distance = direction === 'left' ? -80 : 80;
|
||||
return animation.translateX(distance).step();
|
||||
}
|
||||
|
||||
// 🔄 ===== 加载动画 =====
|
||||
|
||||
// 旋转加载动画
|
||||
loadingSpinner(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: 1000,
|
||||
timingFunction: this.config.easings.linear,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation.rotate(360).step();
|
||||
}
|
||||
|
||||
// 脉冲加载动画
|
||||
loadingPulse(options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: 1000,
|
||||
timingFunction: this.config.easings.easeInOut,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.opacity(0.3).step({ duration: 500 })
|
||||
.opacity(1).step({ duration: 500 });
|
||||
}
|
||||
|
||||
// 波浪加载动画
|
||||
loadingWave(delay = 0, options = {}) {
|
||||
const { animation } = this.createAnimation({
|
||||
duration: 600,
|
||||
timingFunction: this.config.easings.easeInOut,
|
||||
delay: delay,
|
||||
...options
|
||||
});
|
||||
|
||||
return animation
|
||||
.translateY(-10).step({ duration: 300 })
|
||||
.translateY(0).step({ duration: 300 });
|
||||
}
|
||||
|
||||
// 🎨 ===== 工具方法 =====
|
||||
|
||||
// 应用变换
|
||||
applyTransform(animation, transform) {
|
||||
Object.keys(transform).forEach(property => {
|
||||
const value = transform[property];
|
||||
|
||||
switch (property) {
|
||||
case 'opacity':
|
||||
animation.opacity(parseFloat(value));
|
||||
break;
|
||||
case 'transform':
|
||||
// 解析transform值
|
||||
this.parseTransform(animation, value);
|
||||
break;
|
||||
default:
|
||||
console.warn('不支持的动画属性:', property);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 解析transform值
|
||||
parseTransform(animation, transformValue) {
|
||||
const transforms = transformValue.split(' ');
|
||||
|
||||
transforms.forEach(transform => {
|
||||
if (transform.includes('translateX')) {
|
||||
const value = this.extractValue(transform);
|
||||
animation.translateX(value);
|
||||
} else if (transform.includes('translateY')) {
|
||||
const value = this.extractValue(transform);
|
||||
animation.translateY(value);
|
||||
} else if (transform.includes('scale')) {
|
||||
const value = parseFloat(this.extractValue(transform));
|
||||
animation.scale(value);
|
||||
} else if (transform.includes('rotate')) {
|
||||
const value = this.extractValue(transform);
|
||||
animation.rotate(parseFloat(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 提取变换值
|
||||
extractValue(transform) {
|
||||
const match = transform.match(/\(([^)]+)\)/);
|
||||
return match ? match[1] : '0';
|
||||
}
|
||||
|
||||
// 检测设备性能
|
||||
detectPerformance() {
|
||||
try {
|
||||
// 使用新的API替代已弃用的wx.getSystemInfoSync
|
||||
const deviceInfo = wx.getDeviceInfo();
|
||||
const { platform, system, model } = deviceInfo;
|
||||
|
||||
// 简单的性能评估
|
||||
let performanceScore = 100;
|
||||
|
||||
// 根据平台调整
|
||||
if (platform === 'android') {
|
||||
performanceScore -= 10;
|
||||
}
|
||||
|
||||
// 根据系统版本调整
|
||||
const systemVersion = parseFloat(system.match(/[\d.]+/)?.[0] || '0');
|
||||
if (systemVersion < 10) {
|
||||
performanceScore -= 20;
|
||||
}
|
||||
|
||||
// 根据机型调整(简化判断)
|
||||
if (model && model.toLowerCase().includes('redmi')) {
|
||||
performanceScore -= 15;
|
||||
}
|
||||
|
||||
// 设置性能模式
|
||||
if (performanceScore >= 80) {
|
||||
this.config.performanceMode = 'high';
|
||||
} else if (performanceScore >= 60) {
|
||||
this.config.performanceMode = 'auto';
|
||||
} else {
|
||||
this.config.performanceMode = 'low';
|
||||
// 低性能模式下禁用复杂动画
|
||||
this.config.durations.normal = 200;
|
||||
this.config.durations.slow = 300;
|
||||
}
|
||||
|
||||
console.log('🎭 设备性能评估:', {
|
||||
score: performanceScore,
|
||||
mode: this.config.performanceMode
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 性能检测失败:', error);
|
||||
this.config.performanceMode = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置全局动画样式
|
||||
setupGlobalStyles() {
|
||||
// 这里可以设置一些全局的CSS动画类
|
||||
// 微信小程序中主要通过WXSS实现
|
||||
}
|
||||
|
||||
// 生成动画ID
|
||||
generateAnimationId() {
|
||||
return `anim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// 清理动画缓存
|
||||
cleanupAnimationCache() {
|
||||
const now = Date.now();
|
||||
const expireTime = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
for (const [id, cache] of this.animationCache) {
|
||||
if (now - cache.createTime > expireTime) {
|
||||
this.animationCache.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取性能统计
|
||||
getPerformanceStats() {
|
||||
return {
|
||||
...this.performanceStats,
|
||||
cacheSize: this.animationCache.size,
|
||||
performanceMode: this.config.performanceMode
|
||||
};
|
||||
}
|
||||
|
||||
// 设置动画开关
|
||||
setEnabled(enabled) {
|
||||
this.config.enabled = enabled;
|
||||
console.log('🎭 动画开关:', enabled ? '开启' : '关闭');
|
||||
}
|
||||
|
||||
// 设置性能模式
|
||||
setPerformanceMode(mode) {
|
||||
if (['auto', 'high', 'low'].includes(mode)) {
|
||||
this.config.performanceMode = mode;
|
||||
console.log('🎭 性能模式:', mode);
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁动画管理器
|
||||
destroy() {
|
||||
this.animationCache.clear();
|
||||
this.isInitialized = false;
|
||||
console.log('🎭 动画管理器已销毁');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const animationManager = new AnimationManager();
|
||||
|
||||
module.exports = animationManager;
|
||||
Loading…
Add table
Add a link
Reference in a new issue