项目背景:
游戏背包 需要手动 拖拽游戏装备到 装备卡槽中,看了下网上资料很少。手搓了一个下午搞定,现在来记录下实现步骤;
功能拆分:
一个完整需求,我们一般会把它拆分成 几个小步骤分别造零件。等都造好了我们就能 装配到一起 形成一个完成功能。以下是对上面功能的 步骤拆分:
- 背包详情的展示,点击背包中的物品 展示一个详情页面(因为 是在 ScrollView 中拖拽 ScrollView 的元素 会造成 ScrollView 滚动)所有我们直接拖拽 详情页面中的元素就能 避免对 ScrollView 的影响。(如果没有详情可以 先生成一个一样的元素放置的 ScrollView 外层)
- 实现 拖拽元素,本身是对 拖拽事件的监听 来完成对元素位置的设置。这里有个知识点 触点坐标 到 世界坐标的转换
- 能拖拽元素了 那么我们就需要把 元素放在对应的位置, 实际上就是在拖拽的时候 判断两个元素的位置,根据需求我们这里是直接判断的 元素的 x坐标;来判断拖拽元素 是否到了 对应的卡槽;
- 既然能判断到那个具体卡槽了,我们停止拖拽,那就装备到对应的卡槽,要是没有到卡槽位置,我们隐藏拖拽元素 返回 (假装什么也没发生)
1.背包装备详情
点击装备展示,对应详情信息,应为装备位置不固定,
- 展示的时候就需要 通过 背包元素的 位置 来计算详情页面展示的位置
- 并且如果是 在边缘位置还要修改要展示的位置,然后修正位置信息后 在做展示
这里有一个技术点就是 坐标位置的转换:
想要对比两个节点元素 或者 参照当前节点 设置另一个节点位置信息(不在同一个父节点的时候) 需要把他们转换在 同一个坐标系及(参照同一个父节点)
问题: 由于图中的 粉色ScrollView item 和 详情不是在用一个坐标参照中, 如果我们直接获取 item 位置 赋值给 详情元素 那肯定无法让他们两位置一直;
解决问题思路:
先计 算出 item 世界坐标,然后再把这个坐标转化到 详情页面的局部坐标(他们世界坐标一直,局部坐标不一致)
同为 cocos creator 坐标转换需要使用 使用节点父元素计算:
https://docs.cocos.com/creator/3.8/api/zh/class/UITransform?id=convertToWorldSpaceARhttp://convertToWorldSpaceAR
ItemworldPositon = item.parent.getComponent(UITransform).convertToWorldSpaceAR(item.getPosition()); 详情面板位置 = 详情面板.parent.getComponent(UITransform).convertToNodeSpaceAR( ItemworldPositon)
- convertToNodeSpaceAR
/** * @en * Converts a Point to node (local) space coordinates. * * @zh * 将一个 UI 节点世界坐标系下点转换到另一个 UI 节点 (局部) 空间坐标系,这个坐标系以锚点为原点。 * 非 UI 节点转换到 UI 节点(局部) 空间坐标系,请走 Camera 的 `convertToUINode`。 * * @param worldPoint @en Point in world space. * @zh 世界坐标点。 * @param out @en Point in local space. * @zh 转换后坐标。 * @returns @en Return the relative position to the target node. * @zh 返回与目标节点的相对位置。 * @example * ```ts * const newVec3 = uiTransform.convertToNodeSpaceAR(cc.v3(100, 100, 0)); * ``` */ convertToNodeSpaceAR(worldPoint: math.Vec3, out?: math.Vec3): math.Vec3;
- convertToWorldSpaceAR
/** * @en * Converts a Point in node coordinates to world space coordinates. * * @zh * 将距当前节点坐标系下的一个点转换到世界坐标系。 * * @param nodePoint @en Point in local space. * @zh 节点坐标。 * @param out @en Point in world space. * @zh 转换后坐标。 * @returns @en Returns the coordinates in the UI world coordinate system. * @zh 返回 UI 世界坐标系。 * @example * ```ts * const newVec3 = uiTransform.convertToWorldSpaceAR(3(100, 100, 0)); * ``` */ convertToWorldSpaceAR(nodePoint: math.Vec3, out?: math.Vec3): math.Vec3;
/** * 坐标计算 * @param target ScrollView 中被点击的 item 元素 * @param item 数据 */ public setData(target: Node, item: CoachCharacterItem) { //console.log('target.getPosition():', target.getPosition()); //当前节点本地坐标转世界坐标、 //世界坐标转到 对应的 节点坐标 let vec: Vec3 = this.rootParemt.getComponent(UITransform).convertToNodeSpaceAR(target.parent.getComponent(UITransform).convertToWorldSpaceAR(target.getPosition())); console.log(vec) //修正位置 if (vec.x < 400) { vec.x += 250; } else { vec.x -= 250; } if (vec.y > 140) { vec.y = 140; } else if (vec.y < -100) { vec.y = -100; } this.node.setPosition(vec); this.node.active = true; }
2.拖拽元素
我们给详情页面添加 拖拽事件的监听:
这里有个技术点: tuochMove 中获取到的坐标实际上是 触点坐标, 我们需要将这个坐标 转化到 UI坐标才能对 UI元素赋值
//添加监听事件 onLoad() { this.node.on(Node.EventType.TOUCH_START, this.touchStart, this); this.node.on(Node.EventType.TOUCH_MOVE, this.touchMove, this); this.node.on(Node.EventType.TOUCH_END, this.touchEnd, this) this.node.on(Node.EventType.TOUCH_CANCEL, this.touchCancel, this) } //展示只有 在拖拽情况下才显示的 元素 equipmentFly touchStart(event: EventTouch) { //Log.trace('touchStart'); //展示 拖拽 元素 equipmentFly this.equipmentFly.node.active = true; } //获取 触点坐标位置 touchMove(event: EventTouch) { //更新 拖拽元素位置 this.equipmentFly.updatePosition(event.getLocation()); } //结束拖拽 隐藏拖拽元素 touchCancel(event: EventTouch) { //console.log('touchCancel'); //隐藏 拖拽 元素 equipmentFly this.equipmentFly.node.active = false; //执行 拖拽完毕的判断逻辑 this.equipmentFly.onConfirm(); //重置拖拽元素的位置 this.equipmentFly.resetPostion(new Vec3(0, 50)); } //结束拖拽 touchEnd(event: EventTouch) { //console.log('touchEnd'); //隐藏 拖拽 元素 equipmentFly this.equipmentFly.node.active = false; //执行 拖拽完毕的判断逻辑 this.equipmentFly.onConfirm(); //重置拖拽元素的位置 this.equipmentFly.resetPostion(new Vec3(0, 50)); } //隐藏详情面板 public hideNoticePanel() { this.equipmentFly.node.active = false; this.equipmentFly.resetPostion(new Vec3(0, 50)); this.node.active = false; }
触点坐标转换UI坐标的实现:
触点坐标转 UI 坐标需要通过 camera.screenToWorld(触点坐标)
Cocos Creator APIDescriptionhttps://docs.cocos.com/creator/3.8/api/zh/class/renderer.scene.Camera?id=screenToWorld大概逻辑整理如下:
- 拿到 拖拽 事件传递的 触点坐标
- 触点坐标 通过 camera 转 世界坐标
- 世界坐标转 UI局部坐标赋值 拖拽元素坐标 这样就能实现 拖拽元素随着手指滑动而移动
//拿到摄像机 onLoad() { //获取摄像机 this.camera = find('Canvas/Camera').getComponent(Camera); //获取卡槽1 的 世界坐标 this.vec3WorldBattle = this.nodeWearBattle.parent.getComponent(UITransform).convertToWorldSpaceAR(this.nodeWearBattle.getPosition()); //获取卡槽1 的 世界坐标 this.vec3WorldReward = this.nodeWearReward.parent.getComponent(UITransform).convertToWorldSpaceAR(this.nodeWearReward.getPosition()); } /** * 触点坐标转换 * 最后使用UI坐标赋值 * @param vec */ updatePosition(vec: Vec2) { this.vec3Pos.x = vec.x; this.vec3Pos.y = vec.y; //触点坐标转 世界坐标 this.vec3WorldNode = this.camera.screenToWorld(this.vec3Pos); // 世界坐标转 UI局部坐标 this.vec3new = this.node.parent.getComponent(UITransform).convertToNodeSpaceAR(this.vec3WorldNode); this.node.position = this.vec3new; //计算 拖拽元素 和 两个卡槽的 x轴距离 这里 distance 为 50个单位 if (Math.abs(this.vec3WorldNode.x - this.vec3WorldBattle.x) < this.distance) { //距离到了 通知界面展示 卡槽1 选中框 this.coachCharacterEquipmentView.onPreview(this.equipment, 0); } else if (Math.abs(this.vec3WorldNode.x - this.vec3WorldReward.x) < this.distance) { //距离到了 通知界面展示 卡槽2 选中框 this.coachCharacterEquipmentView.onPreview(this.equipment, 1); } else { //这里处理 距离都没到的逻辑 移除选中框 以及 从 已经选中状态到 没选中状态的切换 this.coachCharacterEquipmentView.onPreview(null, -1); } }
3.卡槽判定
通过上面的代码我们已经可以实现,拖转元素到 卡槽函数的调用:
//计算 拖拽元素 和 两个卡槽的 x轴距离 这里 distance 为 50个单位 if (Math.abs(this.vec3WorldNode.x - this.vec3WorldBattle.x) < this.distance) { //距离到了 通知界面展示 卡槽1 选中框 this.coachCharacterEquipmentView.onPreview(this.equipment, 0); } else if (Math.abs(this.vec3WorldNode.x - this.vec3WorldReward.x) < this.distance) { //距离到了 通知界面展示 卡槽2 选中框 this.coachCharacterEquipmentView.onPreview(this.equipment, 1); } else { //这里处理 距离都没到的逻辑 移除选中框 以及 从 已经选中状态到 没选中状态的切换 this.coachCharacterEquipmentView.onPreview(null, -1); }
因为我们一个卡槽有多个 装备位置,需要响应的装备展示 对应的卡槽;
this.coachCharacterEquipmentView.onPreview(data,soltIndex);
函数onPreview 当到对应的卡槽就把数据传递过去,具体不够就传 null,过去
卡槽现实的我们就略过去;
卡槽数据的确认:
这个环节就对应的是 到对应的卡槽,并且松手:
上面的代码中我们已经加过监听了;通过实践发现 即便我同时监听了 TOUCH_END 和 TOUCH_CANCEL 他也只有一个会执行;所有我两个都加了
所以一来逻辑就清晰了:
- 当拖拽元素到卡槽中的时候,我们已经传递参数过去了,并且 我们当拖出 卡槽的时候还把数据 赋值为 null,
- 当我们停止拖拽的时候 判断下 传递过去的 数据是否为 null,
- 如果为 null 则说明 没有在卡槽中停止 拖拽,不为空则为 在卡槽中停止的拖拽
touchCancel(event: EventTouch) { //console.log('touchCancel'); this.equipmentFly.node.active = false; this.equipmentFly.onConfirm(); this.equipmentFly.resetPostion(new Vec3(0, 50)); } touchEnd(event: EventTouch) { //console.log('touchEnd'); this.equipmentFly.node.active = false; this.equipmentFly.onConfirm(); this.equipmentFly.resetPostion(new Vec3(0, 50)); }
到这里基本上级功能都实现了。而且我们的 距离判断逻辑只有在 拖拽时候才会执行,应该说性能还不错!