欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

libgdx中弹框组件如何阻止事件穿透到下层组件

程序员文章站 2022-03-26 08:48:37
...

libgdx中弹框组件如何阻止事件穿透到下层组件

Background-背景说明

项目组中反馈说,自己定制了一个libgdx的Dialog,但是出现事件会穿透到底层组件的问题;
借此稍微看了下,libgdx以及scene2d的事件机制

所以这篇文章的内容包括以下几点:

  • libgdx的事件简介
  • scene2d/stage事件处理机制
  • 阻止事件穿透的两个核心点
  • scene2d/window组件的实现举例

libgdx的事件简介

不同平台有不同的输入设备,以及不同设备之间支持的输入属性是不同的;
通常来说: 桌面用户通过键盘和鼠标;安卓用户通过触摸屏,还会有一些额外的硬件设备支持,如:陀螺仪等;

libgdx 抽象了上述的输入设备,鼠标和触摸被一致处理,通常这样做满足大多数的应用,不过也会造成一些问题: 如无法识别多指触摸

InputProcessor事件回调

libgdx会把系统的事件处理,统一回调 InputProcessor 接口

public class InputAdapter implements InputProcessor {
	public boolean keyDown (int keycode) {
		return false;
	}

	public boolean keyUp (int keycode) {
		return false;
	}

	public boolean keyTyped (char character) {
		return false;
	}

	public boolean touchDown (int screenX, int screenY, int pointer, int button) {
		return false;
	}

	public boolean touchUp (int screenX, int screenY, int pointer, int button) {
		return false;
	}

	public boolean touchDragged (int screenX, int screenY, int pointer) {
		return false;
	}

	@Override
	public boolean mouseMoved (int screenX, int screenY) {
		return false;
	}

	@Override
	public boolean scrolled (int amount) {
		return false;
	}
}

多个输入事件处理器

有时候应用会需要多个输入事件处理器,如:先分发给UI事件处理器,然后再给应用的世界处理器

可以通过 InputMultiplexer实现

InputMultiplexer multiplexer = new InputMultiplexer();
multiplexer.addProcessor(new MyUiInputProcessor());
multiplexer.addProcessor(new MyGameInputProcessor());
Gdx.input.setInputProcessor(multiplexer);

InputMultiplexer 维护一个 事件处理器列表,所有新的事件需要处理时,按照列表依次分发

重点: 如果当前事件处理器返回true时,表明该事件已处理完成,不需要分发给后续的事件处理器

scene2d/stage事件处理机制

scene2d/stage 在libgdx 事件上,另外再加了一层逻辑,主要是实现了事件在组件链上的传递
事件传播分为两个阶段:
首先是capture,从root 到 target ,在这阶段,父节点可以预处理事件或者取消事件等;
然后是normal,从 target 返回 root

主要在Actor的fire方法中实现

public boolean fire (Event event) {
		if (event.getStage() == null) event.setStage(getStage());
		event.setTarget(this);

		// 收集所有的父级组件
		Array<Group> ancestors = Pools.obtain(Array.class);
		Group parent = this.parent;
		while (parent != null) {
			ancestors.add(parent);
			parent = parent.parent;
		}

		try {
			// 所有父级,从root开始到 Actor 逐级处理 capture listeners
			Object[] ancestorsArray = ancestors.items;
			for (int i = ancestors.size - 1; i >= 0; i--) {
				Group currentTarget = (Group)ancestorsArray[i];
				currentTarget.notify(event, true);
				if (event.isStopped()) return event.isCancelled();
			}

			// Notify the target capture listeners.
			notify(event, true);
			if (event.isStopped()) return event.isCancelled();

			// Notify the target listeners.
			notify(event, false);
			if (!event.getBubbles()) return event.isCancelled();
			if (event.isStopped()) return event.isCancelled();

			// 第二阶段,逐级冒泡到root, 处理 所有 的listeners
			for (int i = 0, n = ancestors.size; i < n; i++) {
				((Group)ancestorsArray[i]).notify(event, false);
				if (event.isStopped()) return event.isCancelled();
			}

			return event.isCancelled();
		} finally {
			ancestors.clear();
			Pools.free(ancestors);
		}
	}

阻止事件穿透的两个核心点

所以要阻止事件的传播,需要满足以下两个条件

  • 要能捕获到事件,也就是说作为Event的target或者作为父级组件的 capture listeners

  • 对应事件返回True 或者stop,阻止事件继续传递

作为Event的target就需要实现 Actor的hit检测:

	/** Returns the deepest {@link #isVisible() visible} (and optionally, {@link #getTouchable() touchable}) actor that contains
	 * the specified point, or null if no actor was hit. The point is specified in the actor's local coordinate system (0,0 is the
	 * bottom left of the actor and width,height is the upper right).
	 * <p>
	 * This method is used to delegate touchDown, mouse, and enter/exit events. If this method returns null, those events will not
	 * occur on this Actor.
	 * <p>
	 * The default implementation returns this actor if the point is within this actor's bounds and this actor is visible.
	 * @param touchable If true, hit detection will respect the {@link #setTouchable(Touchable) touchability}.
	 * @see Touchable */
	public Actor hit (float x, float y, boolean touchable) {
		if (touchable && this.touchable != Touchable.enabled) return null;
		if (!isVisible()) return null;
		return x >= 0 && x < width && y >= 0 && y < height ? this : null;
	}

scene2d/window组件的实现举例

window 是table子类,实现了拖拽和模态弹框
当设置了 isModal 属性时,就能够防止弹框底层的事件穿透

主要是两块内容:

  • hit函数
	public Actor hit (float x, float y, boolean touchable) {
		if (!isVisible()) return null;
		Actor hit = super.hit(x, y, touchable);
		if (hit == null && isModal && (!touchable || getTouchable() == Touchable.enabled)) return this;
		float height = getHeight();
		if (hit == null || hit == this) return hit;
		if (y <= height && y >= height - getPadTop() && x >= 0 && x <= getWidth()) {
			// Hit the title bar, don't use the hit child if it is in the Window's table.
			Actor current = hit;
			while (current.getParent() != this)
				current = current.getParent();
			if (getCell(current) != null) return this;
		}
		return hit;
	}
  • 返回相应事件为True
		addListener(new InputListener() {
			float startX, startY, lastX, lastY;

			public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
        ...
				return edge != 0 || isModal;
			}

			public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
				dragging = false;
			}

			public void touchDragged (InputEvent event, float x, float y, int pointer) {
				if (!dragging) return;
				...
				setBounds(Math.round(windowX), Math.round(windowY), Math.round(width), Math.round(height));
			}

			public boolean mouseMoved (InputEvent event, float x, float y) {
				updateEdge(x, y);
				return isModal;
			}

			public boolean scrolled (InputEvent event, float x, float y, int amount) {
				return isModal;
			}

			public boolean keyDown (InputEvent event, int keycode) {
				return isModal;
			}

			public boolean keyUp (InputEvent event, int keycode) {
				return isModal;
			}

			public boolean keyTyped (InputEvent event, char character) {
				return isModal;
			}
		});