libgdx中弹框组件如何阻止事件穿透到下层组件
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;
}
});