import { _decorator, Component, Node, UITransform, EventTouch, Touch, EventMouse, Vec2, Widget, Canvas, input, Input } from ‘cc’;
const { ccclass, property } = _decorator;
/**
-
拖动组件
-
支持PC端鼠标拖动和移动端触摸拖动
-
支持鼠标滚轮缩放
*/
@ccclass(‘Drag’)
export class Drag extends Component {
// 是否启用拖动
@property({ displayName: "启用拖动" })
public enableDrag: boolean = true;
// 是否限制在屏幕内
@property({ displayName: "限制在屏幕内" })
public limitInScreen: boolean = true;
// 是否启用鼠标缩放
@property({ displayName: "启用鼠标缩放" })
public enableScale: boolean = true;
// 最小缩放
@property({ displayName: "最小缩放", visible: function () { return this.enableScale } })
public minScale: number = 0.5;
// 最大缩放
@property({ displayName: "最大缩放", visible: function () { return this.enableScale } })
public maxScale: number = 3.0;
// 缩放速度
@property({ displayName: "缩放速度", visible: function () { return this.enableScale } })
public scaleSpeed: number = 0.1;
// 私有变量
private _isDragging: boolean = false;
private _offset: Vec2 = new Vec2();
private _widget: Widget = null;
private _widgetEnabled: boolean = false;
private _canvasNode: Node = null;
// 双指缩放相关
private _isScaling: boolean = false;
private _lastDistance: number = 0;
private _lastTouchCenter: Vec2 = new Vec2();
private _touches: Touch[] = [];
onLoad() {
// 检查是否有Widget组件
this._widget = this.node.getComponent(Widget);
// 查找Canvas节点
let current = this.node;
while (current) {
if (current.getComponent(Canvas)) {
this._canvasNode = current;
break;
}
current = current.parent;
}
}
start() {
// 注册触摸事件
this.node.on(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.node.on(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.node.on(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
// 注册鼠标滚轮事件
if (this.enableScale) {
input.on(Input.EventType.MOUSE_WHEEL, this._onMouseWheel, this);
}
}
onDestroy() {
// 注销触摸事件
this.node.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
this.node.off(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
this.node.off(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
// 注销鼠标滚轮事件
if (this.enableScale) {
input.off(Input.EventType.MOUSE_WHEEL, this._onMouseWheel, this);
}
}
private _onTouchStart(event: EventTouch) {
const touches = event.getTouches();
// 如果已经有两个触摸点,则不响应
if (this._touches.length === 2) return;
//取前两个点,若果已经有一个点,则取一个点
if (this._touches.length === 1) {
this._touches.push(touches[0]);
} else {
this._touches.push(...touches.slice(0, 2));
}
if (this._touches.length === 1) {
// 单指按下 - 开始或继续拖动
// 如果之前在缩放,不响应
if (this._isScaling) {
return;
}
if (this.enableDrag) {
// 设置拖动状态
this._isDragging = true;
this._isScaling = false;
// 禁用Widget(如果有)
if (this._widget && this._widget.enabled) {
this._widgetEnabled = this._widget.enabled;
this._widget.enabled = false;
}
// 重要:每次单指按下都重新计算偏移量
// 这样可以确保节点不会跳动
const touchPos = this._touches[0].getUILocation();
const nodeWorldPos = this.node.worldPosition;
this._offset.set(
touchPos.x - nodeWorldPos.x,
touchPos.y - nodeWorldPos.y
);
}
} else if (this._touches.length >= 2) {
// 双指按下 - 开始缩放
if (this.enableScale) {
// 停止拖动,开始缩放
this._isDragging = false;
this._isScaling = true;
// 禁用Widget(如果有)
if (this._widget && this._widget.enabled) {
if (!this._widgetEnabled) {
this._widgetEnabled = this._widget.enabled;
}
this._widget.enabled = false;
}
//重要:基于当前双指位置初始化缩放数据
const touch1 = this._touches[0].getUILocation();
const touch2 = this._touches[1].getUILocation();
this._lastDistance = this._getTouchDistance(touch1, touch2);
this._lastTouchCenter.set(
(touch1.x + touch2.x) / 2,
(touch1.y + touch2.y) / 2
);
}
}
}
private _onTouchMove(event: EventTouch) {
// 双指缩放
if (this._touches.length >= 2 && this._isScaling && this.enableScale) {
const touch1 = this._touches[0].getUILocation();
const touch2 = this._touches[1].getUILocation();
// 计算当前双指距离
const currentDistance = this._getTouchDistance(touch1, touch2);
// 计算距离变化比例
const distanceRatio = currentDistance / this._lastDistance;
// 如果距离变化太小,忽略(可能是手指抖动)
if (Math.abs(distanceRatio - 1.0) < 0.01) {
this._lastDistance = currentDistance;
return;
}
// 计算新的缩放值
const currentScale = this.node.scale.x;
let newScale = currentScale * distanceRatio;
// 限制在最小最大范围内
newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
// 如果缩放值没变化,只更新距离
if (newScale === currentScale) {
this._lastDistance = currentDistance;
return;
}
// 计算当前双指中心点
const currentCenter = new Vec2(
(touch1.x + touch2.x) / 2,
(touch1.y + touch2.y) / 2
);
// 获取节点当前的世界坐标
const nodeWorldPos = this.node.worldPosition;
// 计算中心点相对于节点中心的偏移
const offsetX = currentCenter.x - nodeWorldPos.x;
const offsetY = currentCenter.y - nodeWorldPos.y;
// 应用缩放
this.node.setScale(newScale, newScale, newScale);
// 计算缩放后,为了保持中心点位置不变,节点应该移动到的新位置
const scaleRatio = newScale / currentScale;
let newWorldX = currentCenter.x - offsetX * scaleRatio;
let newWorldY = currentCenter.y - offsetY * scaleRatio;
// 应用屏幕限制
if (this.limitInScreen && this._canvasNode) {
const limit = this._getScreenLimit();
if (limit) {
newWorldX = Math.max(limit.minX, Math.min(limit.maxX, newWorldX));
newWorldY = Math.max(limit.minY, Math.min(limit.maxY, newWorldY));
}
}
// 更新节点位置
this.node.setWorldPosition(newWorldX, newWorldY, nodeWorldPos.z);
// 更新上一次的距离
this._lastDistance = currentDistance;
return;
}
// 单指拖动
if (this._touches.length === 1 && this._isDragging && this.enableDrag && !this._isScaling) {
// 获取触摸点的世界坐标
const touchPos = this._touches[0].getUILocation();
// 计算新的世界坐标位置(减去偏移)
let newWorldX = touchPos.x - this._offset.x;
let newWorldY = touchPos.y - this._offset.y;
// 限制在屏幕范围内
if (this.limitInScreen && this._canvasNode) {
const limit = this._getScreenLimit();
if (limit) {
newWorldX = Math.max(limit.minX, Math.min(limit.maxX, newWorldX));
newWorldY = Math.max(limit.minY, Math.min(limit.maxY, newWorldY));
}
}
// 更新节点的世界坐标
this.node.setWorldPosition(newWorldX, newWorldY, this.node.worldPosition.z);
}
}
private _onTouchEnd(event: EventTouch) {
const touches = event.getTouches();
// 移除触摸点
touches.forEach(touch => {
this._touches = this._touches.filter(t => t.getID() !== touch.getID());
});
if (this._touches.length === 1) {
this._isDragging = true;
this._isScaling = false;
//设置单指偏移量
this._offset.set(
this._touches[0].getUILocation().x - this.node.worldPosition.x,
this._touches[0].getUILocation().y - this.node.worldPosition.y
);
}
if (this._touches.length === 2) {
this._isScaling = true;
this._isDragging = false;
//设置双指缩放相关数据
this._lastDistance = this._getTouchDistance(this._touches[0].getUILocation(), this._touches[1].getUILocation());
this._lastTouchCenter.set(
(this._touches[0].getUILocation().x + this._touches[1].getUILocation().x) / 2,
(this._touches[0].getUILocation().y + this._touches[1].getUILocation().y) / 2
);
}
// 所有手指都抬起 - 完全重置状态
if (this._touches.length === 0) {
// 清除所有状态
this._isDragging = false;
this._isScaling = false;
// 注意:不清除 _offset,它会在下次 TouchStart 时重新计算
// 清除缩放相关数据
this._lastDistance = 0;
this._lastTouchCenter.set(0, 0);
// 恢复Widget(如果有)
if (this._widget) {
this._widget.enabled = this._widgetEnabled;
}
}
}
private _onMouseWheel(event: EventMouse) {
if (!this.enableScale) return;
const uiTransform = this.node.getComponent(UITransform);
if (!uiTransform) return;
// 获取滚轮滚动方向
const scrollY = event.getScrollY();
// 计算新的缩放值
const currentScale = this.node.scale.x; // 假设xyz缩放一致
const scaleChange = scrollY > 0 ? this.scaleSpeed : -this.scaleSpeed;
let newScale = currentScale + scaleChange;
// 限制在最小最大范围内
newScale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
// 如果缩放值没变化,直接返回
if (newScale === currentScale) return;
// 获取鼠标在世界坐标系中的位置
const mouseWorldPos = event.getUILocation();
// 获取节点当前的世界坐标
const nodeWorldPos = this.node.worldPosition;
// 计算鼠标相对于节点中心的偏移(世界坐标)
const offsetX = mouseWorldPos.x - nodeWorldPos.x;
const offsetY = mouseWorldPos.y - nodeWorldPos.y;
// 应用缩放
this.node.setScale(newScale, newScale, newScale);
// 计算缩放后,为了保持鼠标位置不变,节点应该移动到的新位置
// 缩放会改变偏移量,所以需要重新计算节点位置
const scaleRatio = newScale / currentScale;
let newWorldX = mouseWorldPos.x - offsetX * scaleRatio;
let newWorldY = mouseWorldPos.y - offsetY * scaleRatio;
// 应用屏幕限制
if (this.limitInScreen && this._canvasNode) {
const limit = this._getScreenLimit();
if (limit) {
newWorldX = Math.max(limit.minX, Math.min(limit.maxX, newWorldX));
newWorldY = Math.max(limit.minY, Math.min(limit.maxY, newWorldY));
}
}
// 更新节点位置
this.node.setWorldPosition(newWorldX, newWorldY, nodeWorldPos.z);
}
/**
* 获取屏幕限制范围(世界坐标)
* 确保物体完全覆盖屏幕,不露出黑边
*/
private _getScreenLimit() {
const selfTransform = this.node.getComponent(UITransform);
const canvasTransform = this._canvasNode.getComponent(UITransform);
if (!selfTransform || !canvasTransform) {
return null;
}
// 获取Canvas的世界坐标和尺寸
const canvasWorldPos = this._canvasNode.worldPosition;
const canvasWidth = canvasTransform.width;
const canvasHeight = canvasTransform.height;
// 获取节点的尺寸(考虑缩放)
const selfWidth = selfTransform.width * Math.abs(this.node.scale.x);
const selfHeight = selfTransform.height * Math.abs(this.node.scale.y);
// 计算节点的半尺寸
const halfWidth = selfWidth / 2;
const halfHeight = selfHeight / 2;
// 计算Canvas的边界(世界坐标)
const canvasLeft = canvasWorldPos.x - canvasWidth / 2;
const canvasRight = canvasWorldPos.x + canvasWidth / 2;
const canvasBottom = canvasWorldPos.y - canvasHeight / 2;
const canvasTop = canvasWorldPos.y + canvasHeight / 2;
// 如果节点比屏幕小:限制节点不能离开屏幕
if (selfWidth <= canvasWidth && selfHeight <= canvasHeight) {
return {
minX: canvasLeft + halfWidth,
maxX: canvasRight - halfWidth,
minY: canvasBottom + halfHeight,
maxY: canvasTop - halfHeight
};
}
// 如果节点比屏幕大:限制节点必须覆盖整个屏幕(不能露出黑边)
return {
minX: canvasRight - halfWidth, // 中心不能太靠左,否则右边露出
maxX: canvasLeft + halfWidth, // 中心不能太靠右,否则左边露出
minY: canvasTop - halfHeight, // 中心不能太靠下,否则上边露出
maxY: canvasBottom + halfHeight // 中心不能太靠上,否则下边露出
};
}
/**
* 计算两个触摸点之间的距离
*/
private _getTouchDistance(touch1: Vec2, touch2: Vec2): number {
const dx = touch2.x - touch1.x;
const dy = touch2.y - touch1.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 设置是否启用拖动
*/
public setEnableDrag(enable: boolean) {
this.enableDrag = enable;
}
}