重現方式
直接上 SSCCE:
public class SpriteTestEP extends DrawComponent implements EntryPoint {
private RectangleSprite red = new RectangleSprite(200, 201, 100, 50);
private RectangleSprite none = new RectangleSprite(200, 201, 300, 50);
public SpriteTestEP() {
red.setFill(RGB.RED);
none.setFill(Color.NONE);
addSprite(red);
addSprite(none);
addSpriteOverHandler(new SpriteOverHandler() {
@Override
public void onSpriteOver(SpriteOverEvent event) {
log("Over : " + who(event.getSprite()));
}
});
addSpriteOutHandler(new SpriteOutHandler() {
@Override
public void onSpriteLeave(SpriteOutEvent event) {
log("Out : " + who(event.getSprite()));
}
});
}
private String who(Sprite sprite) {
return (sprite == red ? "red" : "none");
}
public static native void log(Object object) /*-{
console.log(
@java.lang.String::valueOf(Ljava/lang/Object;)(object)
);
}-*/;
@Override
public void onModuleLoad() {
//無關緊要,純粹 follow GXT 習慣 XD
Viewport vp = new Viewport();
vp.add(this);
RootPanel.get().add(vp);
}
}
操作步驟:
- 游標進入紅色區塊
- 慢慢水平移動滑鼠,直到離開紅色區塊
理論上 console 的輸出應該依序是:
- (N) Over : red
- Out : red
- (N) Over : none
(這裡可以先爆雷:只要 none.setFill()
給一個不是 Color.none
的顏色,就會看到這個正常的輸出結果)
但實際上的輸出依序是:
- (N) Over : red
- Over : none
- Out : none
- (N) Over : none
對,完全沒有出現 Out : red
… (所以 GF-Draw 就爆炸了),而且為什麼會有 Out : none
?滑鼠根本沒離開過 none
的範圍阿?
要招喚出這個靈異現象,需要滿足下列條件,缺一不可:
red
必須優先於none
做addSprite()
- 兩個 sprite 必須緊密相連。
- 在上頭的 case 當中,
none
在red
的右邊,所以none
的 x 座標(300)必須等於red
的 x 座標(100)加上red
的寬度(200)
- 在上頭的 case 當中,
none
的 fill 必須是Color.none
- 其實還有 stroke 的哏,不過有點複雜,這裡略過不談。
原因分析
那麼,source code 怎麼說呢?負責炸出 SpriteOverEvent
的 method 是 onMouseMove()
,裡頭長這樣:
public void onMouseMove(Event event) {
if (handlerManager != null && handlerManager.getHandlerCount(SpriteOverEvent.getType()) > 0) {
PrecisePoint point = getEventXY(event);
SpriteList<Sprite> sprites = surface.sprites;
for (int i = sprites.size() - 1; i >= 0; i--) {
Sprite sprite = sprites.get(i);
if (!sprite.isHidden() && sprite.getBBox().contains(point)) {
ensureHandler().fireEvent(new SpriteOverEvent(sprite, event));
lastSprite = sprite;
return;
}
}
}
if (lastSprite != null) {
ensureHandler().fireEvent(new SpriteOutEvent(lastSprite, event));
lastSprite = null;
}
}
從第一段 if
block 當中我們可以看到:
- 用
bbox
是否包含 event 發生點來判斷是哪個 sprite 要炸 event,而且找到、炸完就 return - 從
surface.sprites
的尾巴往前找,也就是說:如果有 n 個 sprite 的bbox
都符合,會判定給最後加到DrawComponent
上的 sprite- 以程式行為來看(aka 沒有追 source code… [逃]),越晚加到
DrawComponent
上的 sprite 會在越上面,這樣的寫法或許合理…嗎?
- 以程式行為來看(aka 沒有追 source code… [逃]),越晚加到
所以… 是的,source code 可以解釋頭兩個要件:當兩個 sprite 緊密相連時,就會發生炸 event 時的座標同時落在兩個 sprite 的 bbox
內。但因為 none
最後加入,所以就認為是 none
炸的(然後這時候 lastSprite
會是 none
)。
但是… 但是… 但是…
這完全無法解釋為什麼為什麼
none
有設fill
顏色就會沒事阿阿阿阿…
(至少 bbox
的生成過程找不到需要參考 fill
的部份)
另一個問題點是… 光看這段也無法解釋正常狀況下的 SpriteOutEvent
是誰炸的。能肯定的是,在上頭的情境當中,絕對不會是 ouMouseMove()
炸的,因為一定能在 surface.sprites
當中找到值,程式根本不會去跑第二個 if
block。
另一個會炸 SpriteOutEvent
的地方是 onMouseOut()
,裡頭長這樣:
public void onMouseOut(Event event) {
if (lastSprite != null) {
ensureHandler().fireEvent(new SpriteOutEvent(lastSprite, event));
lastSprite = null;
}
}
這樣有點眉目了,在滑鼠跨過兩個 sprite 的邊界時,DrawComponent
一定有收到 Event.ONMOUSEOUT
。那就透過 override 以及一些手段,來觀察:
- x:event 發生時在
DrawComponent
上的 x 座標 - type:browser event 是
ONMOUSEMOVE
或是ONMOUSEOUT
- element:產生 event 的 DOM element(製表時是註記為程式內的對應變數名)
- event:GXT 產生
SpriteOverEvent
或是SpriteOutEvent
如果 none.setFill()
給正常顏色的正常狀況下:
x | type | element | event |
---|---|---|---|
299 | move | red | over |
300 | out | red | out |
300 | move | none | over |
301 | move | none | over |
嗯… 一切看起來頗合理。
如果 none.setFill()
設定為 Color.NONE
,那情況是:
x | type | element | event |
---|---|---|---|
299 | move | red | over |
300 | move | red | over |
301 | out | red | out |
301 | move | bg | over |
browser 一直要到 x = 301 的時候才由 red
炸 ONMOUSEOUT
?然後那個 bg
是什麼鬼???
答:bg
是 DrawComponent
的 background… [眼神死]
到底發生什麼事情?
簡單地說,就是視覺上看不到的 element(例如 none
),就無法讓 browser 觸發任何 event。DrawComponent
有個預設白色的 background,所以實際觸發(browser)event 的是它。那麼如果再搞個 DrawComponent.setBackground(Color.NONE)
呢?還是會炸出 browser event,因為底下還有一個 API 完全無法控制、寬度高度都給 100%
的 <rect>
,GXT 大概就是為了避免沒東西可以觸發 event 所以而寫死、確保一定有的 DOM element?很有趣的事,這個 <rect>
根本沒有給 fill
attribute… #$%&*@!!!
回頭解釋 GXT 層級的 event,一切變得很合理。x = 300 時,對 browser 而言是 red
的 ONMOUSEMOVE
,但是對於 GXT 的機制來說卻會被判別是 none
的 SpriteOverEvent
(理由前段已詳述)。x = 301 時,GXT 要用 lastSprite
炸 SpriteOutEvent
,但是 x = 300 時lastSprite
指定為 none
,所以才會產生帳面上十分矛盾的結果。
結語
又再次證實了寫 SSCCE 的重要。
一開始有寫一個 GF 層級的 SSCCE。但是為了排除 GF-Draw 有 bug 的可能性(因為 GF-Draw 有再蓋一層 event handling 機制,而且 GF-Draw 基礎之一的 LayerSprite
預設有個 fill = Color.NONE
的 RectangleSprite
當 background,所以很容易就會湊滿觸發條件),所以只好再寫一個 GXT 版本,寫完發現程式行為跟程式碼對不起來,只好這麼一路追到 browser 行為上頭… [眼神死]
其實一直都知道「看不到的 element 不會炸 event」這個 browser 哏,因為大概十年前有被雷到,但是寫 GWT 不太需要理會這件事情,而且 DrawComponent
產生的 DOM 是 SVG,說不定 SVG 有 SVG 的玩法(看起來並沒有 XD),也就完全沒有往這方向去想。
結果就是十幾個小時的青春歲月就這樣沒了… [淚目]
所以這也依然是一個「如果在職場上一定直接避開觸發條件不深究」的 case。
好了,現在可以(暫時地)安心睡覺,不會擔心這個偽靈異現象在哪天具現化還逃不掉…
沒有留言:
張貼留言