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();
|
|||
|
},
|
|||
|
};
|