如题,要实现这个功能,首先我们需要先了解下的CocosCreator中的触摸事件传递的机制。
触摸事件冒泡
触摸事件支持节点树的事件冒泡,以下图为例:
在图中的场景里,假设 A 节点拥有一个子节点 B,B 拥有一个子节点 C。开发者对 A、B、C 都监听了触摸事件(以下的举例都默认节点监听了触摸事件)。
当鼠标或手指在 C 节点区域内按下时,事件将首先在 C 节点触发,C 节点监听器接收到事件。接着 C 节点会将事件向其父节点传递这个事件,B 节点的监听器将会接收到事件。同理 B 节点会将事件传递给 A 父节点。这就是最基本的事件冒泡过程。需要强调的是,在触摸事件冒泡的过程中不会有触摸检测,这意味着即使触点不在 A B 节点区域内,A B 节点也会通过触摸事件冒泡的机制接收到这个事件。
触摸事件的冒泡过程与普通事件的冒泡过程并没有区别。所以,调用 event.stopPropagation()
可以主动停止冒泡过程。
同级节点间的触点归属问题
假设上图中 B、C 为同级节点,C 节点部分覆盖在 B 节点之上。这时候如果 C 节点接收到触摸事件后,就宣布了触点归属于 C 节点,这意味着同级节点的 B 就不会再接收到触摸事件了,即使触点同时也在 B 节点内。同级节点间,触点归属于处于顶层的节点。
此时如果 C 节点还存在父节点,则还可以通过事件冒泡的机制传递触摸事件给父节点。
这一点就是我们所需要注意的,由于牌组中的牌都处于同一层,因此不能直接监听每一张牌的触摸事件,这样触摸点归属一张牌之后,其他牌就无法收到事件传递,因此不能监听每张牌的触摸事件,需要一个触摸层,用来接受触摸事件,通过判断触摸点是否落在卡牌上。
原理
如上图,由于每张牌都要按照一个固定位移差排列着,因此检查触摸范围时,只有最后一张卡牌的触摸范围是整张牌范围,其他牌的触摸范围只要检查显示的部分。
不管触摸点起始位置时在触摸终点的左边还是右边,只要判断卡牌的显示部分处于起始点和终点之间就是被选中的状态
实现
1,创建一个卡牌视图类,用来控制卡牌被选中,挂起和复位效果,代码如下:
typescript
export default class CardView {
...
/** 原始位置 */
private _orginPos: cc.Vec3 = cc.v3();
/** 是否被选中 */
private _isSelect: boolean = false;
public get isSelect (): boolean {
return this._isSelect;
}
public set isSelect (isSelect: boolean) {
this._isSelect = isSelect;
this.selectStateImg.active = isSelect;
}
/** 是否挂起 */
private _isUp: boolean = false;
public isUp (): boolean {
return this._isUp;
}
/** 牌之间的间隔 */
private _interval: number = 0;
public set interval (interval: number) {
this._interval = interval;
}
public get interval (): number {
return this._interval;
}
/** 被选中后,挂起的牌复位,原位的牌挂起 */
public checkUp () {
if (this._isUp) {
this.unselectAction();
} else {
this.selectAction();
}
}
/** 被选中后挂起动画 */
public selectAction () {
if (this._isUp) {
return;
}
cc.tween(this.node)
.to(0.1, { position: cc.v3(this.node.x, this._orginPos.y + 20, 0) }, { easing: 'sineIn'})
.call(() => {
this._isUp = true;
})
.start();
}
/** 复位动画 */
public unselectAction () {
if (!this._isUp) {
return;
}
cc.tween(this.node)
.to(0.1, { position: cc.v3(this.node.x, this._orginPos.y, 0) }, { easing: 'sineIn'})
.call(() => {
this._isUp = false;
})
.start();
}
}
2, 创建触摸层, 用来监听触摸事件
typescript
xport default class CardPanelView extends EventComponent {
@inject("playerPanel1", cc.Node)
playerPanel1: cc.Node = null;
/** 根视图 */
private _gameView: UIView = null;
public set gameView (view: UIView) {
this._gameView = view;
}
/** 创建的牌组 */
private _cardViews: cc.Node[] = [];
/** 触摸点起始位置 */
private _startPos: cc.Vec2 = null;
/** 触摸到牌外之前的坐标 */
private _moveEndPos: cc.Vec2 = null;
/** 触摸起始位置是否触摸到牌 */
private _isTouchedInCard: boolean = false;
onLoad(): void {
super.onLoad();
}
/** 监听触摸事件 */
addEvents(): void {
this.onN(this.node, cc.Node.EventType.TOUCH_START, this._touchStart);
this.onN(this.node, cc.Node.EventType.TOUCH_MOVE, this._touchMove);
this.onN(this.node, cc.Node.EventType.TOUCH_END, this._touchEnd);
}
init() {
this.initCards();
}
/** 初始化牌组 */
initCards () {
const cards = [0x1A, 0x1A, 0x1B, 0x1B, 0x1E, 0x1E, 0x1E, 0x1F, 0x1F, 0x01, 0x02];
createPrefab({
url: DDZ_PREFAB_URL[0],
view: this._gameView,
complete: (node) => {
for (let i = 0; i < cards.length; i++) {
const node1 = cc.instantiate(node);
this.node.addChild(node1);
node1.x = setCardPositionX(cards.length-1, i, node1.width, 60);
node1.y = this.playerPanel1.y;
const cardView1 = node1.addComponent(CardView);
cardView1.gameView = this._gameView;
cardView1.interval = 60;
cardView1.value = cards[i];
this._cardViews.push(node1);
}
}
});
}
private _touchStart (ev : cc.Event.EventTouch) {
this._startPos = ev.getLocation();
this._moveEndPos = this._startPos;
this._isTouchedInCard = this._isTouchedCard(this._startPos);
}
private _touchMove (ev : cc.Event.EventTouch) {
/** 起始触摸点在牌组范围外,不进行触摸检测 */
if (!this._isTouchedInCard) return;
/** 触摸点移动到牌组范围外,就不继续进行新的碰撞检测 */
const currentPos = ev.getLocation();
const isCurTouchedCard = this._isTouchedCard(currentPos);
if (!isCurTouchedCard) {
return;
}
this._moveEndPos = currentPos;
/** 碰撞检测,检测到牌显示部分在起始和终点范围之间,则为选中状态,否则为选中 */
this._cardViews.forEach((card, index, arr) => {
const cardRect = card.getBoundingBoxToWorld();
const cardView = card.getComponent(CardView);
cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
if (this._isCardInSelectionRange(cardRect, this._startPos, this._moveEndPos)) {
cardView.isSelect = true;
} else {
cardView.isSelect = false;
}
});
}
private _touchEnd (ev : cc.Event.EventTouch) {
const currentPos = ev.getLocation();
// 选中牌意外的区域,所有牌复位
const _isEndTouchedInCard = this._isTouchedCard(currentPos);
if (!this._isTouchedInCard && !_isEndTouchedInCard) {
this._cardViews.forEach((card, index, arr) => {
const cardView = card.getComponent(CardView);
if (cardView.isSelect)
cardView.isSelect = false;
if (cardView.isUp) {
cardView.unselectAction();
}
});
return;
}
if (!this._isTouchedInCard) return;
// 选中牌区域,挂起的牌复位,原始位置的牌挂起
this._cardViews.forEach((card, index, arr) => {
const cardRect = card.getBoundingBoxToWorld();
const cardView = card.getComponent(CardView);
cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
if (this._isCardInSelectionRange(cardRect, this._startPos, this._moveEndPos)) {
cardView.checkUp();
}
if (cardView.isSelect)
cardView.isSelect = false;
});
}
/** 判断牌显示区域是否在两个点范围内 */
private _isCardInSelectionRange(cardRect: cc.Rect, startPos: cc.Vec2, currentPos: cc.Vec2): boolean {
const minX = Math.min(startPos.x, currentPos.x);
const maxX = Math.max(startPos.x, currentPos.x);
const minY = Math.min(startPos.y, currentPos.y);
const maxY = Math.max(startPos.y, currentPos.y);
if (cardRect.x + cardRect.width < minX || cardRect.x > maxX || cardRect.y + cardRect.height < minY || cardRect.y > maxY) {
return false;
}
return true;
}
/** 判断是否在牌组内 */
private _isTouchedCard (targetPos: cc.Vec2) {
let isTouchedCard = false;
this._cardViews.forEach((card, index, arr) => {
const cardRect = card.getBoundingBoxToWorld();
const cardView = card.getComponent(CardView);
cardRect.width = index === arr.length - 1 ? cardRect.width : cardView.interval;
if (this._isCardInSelectionRange(cardRect, targetPos, targetPos)) {
isTouchedCard = true;
}
});
return isTouchedCard;
}
}
代码就不多解释了,看注释。 这样就基本可以实现多选卡牌的功能,效果如下: