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;
			}
		});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章