426 lines
12 KiB
JavaScript
426 lines
12 KiB
JavaScript
// 兼容性处理:requestAnimationFrame polyfill
|
||
const getRequestAnimationFrame = () => {
|
||
// 检查全局对象是否存在(uni-app 中可能是 uni 而不是 window)
|
||
const global = typeof window !== 'undefined' ? window :
|
||
typeof uni !== 'undefined' ? uni :
|
||
typeof global !== 'undefined' ? global : {};
|
||
|
||
return global.requestAnimationFrame ||
|
||
global.webkitRequestAnimationFrame ||
|
||
global.mozRequestAnimationFrame ||
|
||
global.oRequestAnimationFrame ||
|
||
global.msRequestAnimationFrame ||
|
||
function(callback) {
|
||
return setTimeout(callback, 1000 / 60); // 60 FPS fallback
|
||
};
|
||
};
|
||
|
||
const getCancelAnimationFrame = () => {
|
||
const global = typeof window !== 'undefined' ? window :
|
||
typeof uni !== 'undefined' ? uni :
|
||
typeof global !== 'undefined' ? global : {};
|
||
|
||
return global.cancelAnimationFrame ||
|
||
global.webkitCancelAnimationFrame ||
|
||
global.mozCancelAnimationFrame ||
|
||
global.oCancelAnimationFrame ||
|
||
global.msCancelAnimationFrame ||
|
||
function(id) {
|
||
return clearTimeout(id);
|
||
};
|
||
};
|
||
|
||
const requestAnimationFrame = getRequestAnimationFrame();
|
||
const cancelAnimationFrame = getCancelAnimationFrame();
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// 触摸缩放相关状态
|
||
touchStartPosition1: {
|
||
x: 0,
|
||
y: 0,
|
||
},
|
||
touchStartPosition2: {
|
||
x: 0,
|
||
y: 0,
|
||
},
|
||
initialDistance: 0,
|
||
x: 0, // 元素的x坐标
|
||
y: 0, // 元素的y坐标
|
||
scale: 1, // 元素的缩放比例
|
||
initialX: 0, // 元素的初始x坐标
|
||
initialY: 0, // 元素的初始y坐标
|
||
minScale: 0.5, // 最小缩放比例
|
||
maxScale: 3, // 最大缩放比例
|
||
isScaling: false, // 是否正在缩放
|
||
lastTouchTime: 0, // 上次触摸时间
|
||
|
||
// 拖拽优化相关
|
||
isDragging: false, // 是否正在拖拽
|
||
dragStartTime: 0, // 拖拽开始时间
|
||
lastMoveTime: 0, // 上次移动时间
|
||
velocity: { x: 0, y: 0 }, // 移动速度
|
||
animationId: null, // 动画帧ID
|
||
pendingUpdate: false, // 是否有待处理的更新
|
||
|
||
// 边界控制
|
||
enableBoundary: false, // 是否启用边界控制
|
||
boundary: {
|
||
minX: -500,
|
||
maxX: 500,
|
||
minY: -500,
|
||
maxY: 500,
|
||
}, // 移动边界
|
||
|
||
// 兼容性相关
|
||
useTimer: false, // 是否使用定时器代替 RAF(兼容性降级)
|
||
};
|
||
},
|
||
methods: {
|
||
/**
|
||
* 触摸开始事件处理
|
||
* @param {TouchEvent} event 触摸事件
|
||
*/
|
||
handleTouchStart(event) {
|
||
event.preventDefault();
|
||
|
||
// 取消之前的动画
|
||
if (this.animationId) {
|
||
this.safeCancelAnimationFrame(this.animationId);
|
||
this.animationId = null;
|
||
}
|
||
|
||
const currentTime = Date.now();
|
||
this.lastTouchTime = currentTime;
|
||
|
||
if (event.touches.length === 1) {
|
||
// 单指拖拽初始化
|
||
this.isScaling = false;
|
||
this.isDragging = true;
|
||
this.dragStartTime = currentTime;
|
||
this.lastMoveTime = currentTime;
|
||
this.initialX = event.touches[0].clientX;
|
||
this.initialY = event.touches[0].clientY;
|
||
this.velocity = { x: 0, y: 0 };
|
||
this.pendingUpdate = false;
|
||
} else if (event.touches.length === 2) {
|
||
// 双指缩放初始化
|
||
this.isScaling = true;
|
||
this.isDragging = false;
|
||
this.touchStartPosition1.x = event.touches[0].clientX;
|
||
this.touchStartPosition1.y = event.touches[0].clientY;
|
||
this.touchStartPosition2.x = event.touches[1].clientX;
|
||
this.touchStartPosition2.y = event.touches[1].clientY;
|
||
|
||
// 计算初始两指间距离
|
||
this.initialDistance = this.getDistance(
|
||
this.touchStartPosition1,
|
||
this.touchStartPosition2
|
||
);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 触摸移动事件处理
|
||
* @param {TouchEvent} event 触摸事件
|
||
*/
|
||
handleTouchMove(event) {
|
||
event.preventDefault();
|
||
|
||
if (event.touches.length === 2 && this.isScaling) {
|
||
// 双指缩放逻辑
|
||
const currentDistance = this.getDistance(
|
||
{ x: event.touches[0].clientX, y: event.touches[0].clientY },
|
||
{ x: event.touches[1].clientX, y: event.touches[1].clientY }
|
||
);
|
||
|
||
if (this.initialDistance > 0) {
|
||
// 计算缩放比例变化
|
||
const scaleChange = currentDistance / this.initialDistance;
|
||
let newScale = this.scale * scaleChange;
|
||
|
||
// 限制缩放范围
|
||
newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
|
||
this.scale = newScale;
|
||
|
||
// 更新初始距离为当前距离,用于下次计算
|
||
this.initialDistance = currentDistance;
|
||
}
|
||
} else if (event.touches.length === 1 && this.isDragging) {
|
||
// 单指拖拽逻辑优化
|
||
const currentTime = Date.now();
|
||
const currentX = event.touches[0].clientX;
|
||
const currentY = event.touches[0].clientY;
|
||
|
||
// 计算移动距离
|
||
const deltaX = currentX - this.initialX;
|
||
const deltaY = currentY - this.initialY;
|
||
|
||
// 计算移动速度(用于惯性效果)
|
||
const timeDelta = currentTime - this.lastMoveTime;
|
||
if (timeDelta > 0) {
|
||
this.velocity.x = deltaX / timeDelta * 16; // 标准化到16ms(60fps)
|
||
this.velocity.y = deltaY / timeDelta * 16;
|
||
}
|
||
|
||
// 使用节流机制更新位置
|
||
this.schedulePositionUpdate(deltaX, deltaY);
|
||
|
||
// 更新初始位置和时间
|
||
this.initialX = currentX;
|
||
this.initialY = currentY;
|
||
this.lastMoveTime = currentTime;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 触摸结束事件处理
|
||
* @param {TouchEvent} event 触摸事件
|
||
*/
|
||
handleTouchEnd(event) {
|
||
if (event.touches.length === 0) {
|
||
// 所有手指都离开屏幕,重置状态
|
||
this.isScaling = false;
|
||
this.initialDistance = 0;
|
||
|
||
// 如果是拖拽结束,启动惯性效果
|
||
if (this.isDragging) {
|
||
this.isDragging = false;
|
||
this.startInertiaAnimation();
|
||
}
|
||
} else if (event.touches.length === 1 && this.isScaling) {
|
||
// 从双指变为单指,切换到拖拽模式
|
||
this.isScaling = false;
|
||
this.isDragging = true;
|
||
this.initialX = event.touches[0].clientX;
|
||
this.initialY = event.touches[0].clientY;
|
||
this.lastMoveTime = Date.now();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 计算两点间距离的辅助方法
|
||
* @param {Object} point1 第一个点 {x, y}
|
||
* @param {Object} point2 第二个点 {x, y}
|
||
* @return {Number} 两点间距离
|
||
*/
|
||
getDistance(point1, point2) {
|
||
const dx = point1.x - point2.x;
|
||
const dy = point1.y - point2.y;
|
||
return Math.sqrt(dx * dx + dy * dy);
|
||
},
|
||
|
||
/**
|
||
* 重置缩放和位置
|
||
*/
|
||
resetTransform() {
|
||
this.x = 0;
|
||
this.y = 0;
|
||
this.scale = 1;
|
||
this.isScaling = false;
|
||
this.initialDistance = 0;
|
||
},
|
||
|
||
/**
|
||
* 安全的 requestAnimationFrame 调用
|
||
* @param {Function} callback 回调函数
|
||
* @return {Number} 动画ID
|
||
*/
|
||
safeRequestAnimationFrame(callback) {
|
||
try {
|
||
if (this.useTimer) {
|
||
return setTimeout(callback, 16); // 约 60 FPS
|
||
}
|
||
return requestAnimationFrame(callback);
|
||
} catch (error) {
|
||
console.warn('requestAnimationFrame failed, fallback to setTimeout:', error);
|
||
this.useTimer = true;
|
||
return setTimeout(callback, 16);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 安全的 cancelAnimationFrame 调用
|
||
* @param {Number} id 动画ID
|
||
*/
|
||
safeCancelAnimationFrame(id) {
|
||
try {
|
||
if (this.useTimer || typeof id === 'number') {
|
||
clearTimeout(id);
|
||
} else {
|
||
cancelAnimationFrame(id);
|
||
}
|
||
} catch (error) {
|
||
console.warn('cancelAnimationFrame failed, fallback to clearTimeout:', error);
|
||
clearTimeout(id);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 节流位置更新机制
|
||
* @param {Number} deltaX x轴位移
|
||
* @param {Number} deltaY y轴位移
|
||
*/
|
||
schedulePositionUpdate(deltaX, deltaY) {
|
||
// 计算新位置
|
||
let newX = this.x + deltaX;
|
||
let newY = this.y + deltaY;
|
||
|
||
// 应用边界限制
|
||
if (this.enableBoundary) {
|
||
const pos = this.applyBoundaryConstraints(newX, newY);
|
||
newX = pos.x;
|
||
newY = pos.y;
|
||
}
|
||
|
||
// 更新位置
|
||
this.x = newX;
|
||
this.y = newY;
|
||
|
||
if (!this.pendingUpdate) {
|
||
this.pendingUpdate = true;
|
||
this.safeRequestAnimationFrame(() => {
|
||
this.pendingUpdate = false;
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 启动惯性动画
|
||
*/
|
||
startInertiaAnimation() {
|
||
const minVelocity = 0.1; // 最小速度阈值
|
||
const friction = 0.95; // 摩擦系数
|
||
|
||
// 如果速度太小,直接停止
|
||
if (Math.abs(this.velocity.x) < minVelocity && Math.abs(this.velocity.y) < minVelocity) {
|
||
return;
|
||
}
|
||
|
||
const animate = () => {
|
||
// 应用摩擦力
|
||
this.velocity.x *= friction;
|
||
this.velocity.y *= friction;
|
||
|
||
// 更新位置
|
||
let newX = this.x + this.velocity.x;
|
||
let newY = this.y + this.velocity.y;
|
||
|
||
// 应用边界限制
|
||
if (this.enableBoundary) {
|
||
const pos = this.applyBoundaryConstraints(newX, newY);
|
||
newX = pos.x;
|
||
newY = pos.y;
|
||
|
||
// 如果到达边界,减慢对应方向的速度
|
||
if (newX !== this.x + this.velocity.x) {
|
||
this.velocity.x *= 0.3;
|
||
}
|
||
if (newY !== this.y + this.velocity.y) {
|
||
this.velocity.y *= 0.3;
|
||
}
|
||
}
|
||
|
||
this.x = newX;
|
||
this.y = newY;
|
||
|
||
// 检查是否继续动画
|
||
if (Math.abs(this.velocity.x) > minVelocity || Math.abs(this.velocity.y) > minVelocity) {
|
||
this.animationId = this.safeRequestAnimationFrame(animate);
|
||
} else {
|
||
this.animationId = null;
|
||
this.velocity = { x: 0, y: 0 };
|
||
}
|
||
};
|
||
|
||
this.animationId = this.safeRequestAnimationFrame(animate);
|
||
},
|
||
|
||
/**
|
||
* 应用边界约束
|
||
* @param {Number} x x坐标
|
||
* @param {Number} y y坐标
|
||
* @return {Object} 约束后的坐标 {x, y}
|
||
*/
|
||
applyBoundaryConstraints(x, y) {
|
||
return {
|
||
x: Math.max(this.boundary.minX, Math.min(this.boundary.maxX, x)),
|
||
y: Math.max(this.boundary.minY, Math.min(this.boundary.maxY, y)),
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 设置缩放范围
|
||
* @param {Number} minScale 最小缩放比例
|
||
* @param {Number} maxScale 最大缩放比例
|
||
*/
|
||
setScaleRange(minScale, maxScale) {
|
||
this.minScale = minScale || 0.5;
|
||
this.maxScale = maxScale || 3;
|
||
},
|
||
|
||
/**
|
||
* 设置移动边界
|
||
* @param {Object} boundary 边界配置 {minX, maxX, minY, maxY}
|
||
* @param {Boolean} enable 是否启用边界控制
|
||
*/
|
||
setBoundary(boundary, enable = true) {
|
||
this.boundary = { ...this.boundary, ...boundary };
|
||
this.enableBoundary = enable;
|
||
},
|
||
|
||
/**
|
||
* 检测并设置兼容性模式
|
||
*/
|
||
detectCompatibility() {
|
||
try {
|
||
// 尝试调用 requestAnimationFrame
|
||
const testId = requestAnimationFrame(() => {});
|
||
cancelAnimationFrame(testId);
|
||
this.useTimer = false;
|
||
} catch (error) {
|
||
console.warn('requestAnimationFrame not supported, using timer fallback');
|
||
this.useTimer = true;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 强制使用定时器模式(用于调试或特殊需求)
|
||
* @param {Boolean} useTimer 是否使用定时器
|
||
*/
|
||
setTimerMode(useTimer = true) {
|
||
this.useTimer = useTimer;
|
||
if (useTimer) {
|
||
console.info('TouchScale: 已切换到定时器模式');
|
||
} else {
|
||
console.info('TouchScale: 已切换到 requestAnimationFrame 模式');
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清理动画和状态
|
||
*/
|
||
cleanupTouchScale() {
|
||
if (this.animationId) {
|
||
this.safeCancelAnimationFrame(this.animationId);
|
||
this.animationId = null;
|
||
}
|
||
this.isDragging = false;
|
||
this.isScaling = false;
|
||
this.velocity = { x: 0, y: 0 };
|
||
this.pendingUpdate = false;
|
||
},
|
||
},
|
||
|
||
// 组件创建时检测兼容性
|
||
mounted() {
|
||
this.detectCompatibility();
|
||
},
|
||
|
||
// 组件销毁时清理
|
||
beforeDestroy() {
|
||
this.cleanupTouchScale();
|
||
},
|
||
};
|