在自定義 GalleryView 的 startSlideShow 方法中,開啟新線程調用 galleryView.this.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null),拋出 ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views。
Android UI 工具包非線程安全,所有視圖操作(包括調用 onKeyDown 觸發的內部視圖更新、聲音播放等)必須在主線程(UI 線程)執行。新創建的 Thread 是工作線程,直接操作視圖導致異常。
public class GalleryView extends Gallery {
private Handler mHandler = new Handler(Looper.getMainLooper()); // 綁定主線程
private Runnable mSlideRunnable;
private boolean isTouched = false;
public void startSlideShow(final int periodTime) {
mSlideRunnable = new Runnable() {
@Override
public void run() {
if (!isTouched) {
// 關鍵:使用 Handler post 到主線程執行 onKeyDown
mHandler.post(new Runnable() {
@Override
public void run() {
// 在主線程中安全調用
GalleryView.this.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null);
}
});
}
// 繼續下一次循環
mHandler.postDelayed(mSlideRunnable, periodTime);
}
};
// 首次啟動
mHandler.post(mSlideRunnable);
// 觸摸監聽控制暫停/恢復
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
isTouched = true;
mHandler.removeCallbacks(mSlideRunnable); // 暫停
} else {
isTouched = false;
mHandler.postDelayed(mSlideRunnable, periodTime); // 恢復
}
return false;
}
});
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacksAndMessages(null); // 防止內存洩漏
}
}
| 錯誤寫法 | 正確寫法 | 說明 |
|---|---|---|
new Thread(runnable).start() | mHandler.post(runnable) | Handler 綁定主線程 Looper,確保 UI 操作在主線程 |
子線程直接調用 onKeyDown | 在 mHandler.post 的 Runnable 中調用 | 視圖操作必須在主線程 |
使用 Thread.sleep 延遲 | 使用 mHandler.postDelayed | 非阻塞延遲,不佔用線程資源 |
// 若在 Activity 中
runOnUiThread(new Runnable() {
@Override
public void run() {
galleryView.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null);
}
});
// Kotlin 協程 (推薦)
lifecycleScope.launch {
while (!isTouched) {
withContext(Dispatchers.Main) {
galleryView.onKeyDown(KeyEvent.KEYCODE_DPAD_RIGHT, null)
}
delay(periodTime)
}
}
onKeyDown)必須在主線程。Handler(Looper.getMainLooper()) 或 runOnUiThread 切換線程。postDelayed 替代 Thread.sleep。removeCallbacksAndMessages(null) 防洩漏。