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;
}
});