android-如何在子執行緒中更新ui
阿新 • • 發佈:2019-01-03
正如我們知道的,android是不讓在子執行緒中更新ui的。在子執行緒中更新ui會直接丟擲異常
- Only the original thread that created a view hierarchy can touch its views
- 那麼這種檢查機制在什麼時候發生的呢?
- 那麼真的不能在子執行緒中更新ui麼?我們帶著這個疑問來看一下系統程式碼
我們知道android中的view的更新(大小,位置,內容)全部都交給了WindowManager,那麼我們帶著疑問來看下WindowMagager介面的實現類WindowManagerImpl,中如何控制對view的更新的
- 我們知道WindowManager中有三個常用方法 addView(),removeView()和updateViewLayout();
接下來我們只分析updateViewLayout()方法。
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); mGlobal.updateViewLayout(view, params); }
- applyDefaultToken(params);方法和Window的層級有關係,這裡和我們探討的view的跟新沒有關係,因此跳過
mGlobal.updateViewLayout(view, params); 發現windowManager的更新其實是交給了mGlobal來操作了,那麼mGlobal是什麼呢?
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
- 發現mGlobal其實是WindowManaerImpl一個成員變數,而且還是單例。其實WindowManagerImpl的跟新委託給了WindowManagerGlobal
那麼WindowManagerGlobal的updateViewLayout()方法裡面完成了什麼功能呢?
public void updateViewLayout(View view, ViewGroup.LayoutParams params) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; view.setLayoutParams(wparams); synchronized (mLock) { int index = findViewLocked(view, true); ViewRootImpl root = mRoots.get(index); mParams.remove(index); mParams.add(index, wparams); root.setLayoutParams(wparams, false); }
}
- 前半部分是異常判斷,跳過
下面是給view設定佈局引數,新的佈局引數。
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; view.setLayoutParams(wparams);
下面是找到viewRootImpl,給root重新設定佈局引數。
int index = findViewLocked(view, true); ViewRootImpl root = mRoots.get(index); mParams.remove(index); mParams.add(index, wparams); root.setLayoutParams(wparams, false);
- 那麼ViewRootImpl是什麼呢?其實是android系統中view和WindowManager通訊的橋樑。比如測量 佈局 繪製 時間分發 都是在這裡傳遞給view的
接下來我們分析 root.setLayoutParams(wparams, false);這段程式碼。
if (newView) { mSoftInputMode = attrs.softInputMode; requestLayout(); }
- 程式碼比較長,這裡擷取部分程式碼 requestLayout();
那麼requestLayout中做了什麼操作呢?
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); }
}
- 終於到了重點 checkThread(),在這個方法中做了一個判斷,就是當前更新ui的執行緒是否和ViewRootImpl建立的執行緒是否是同一個,不是則丟擲異常
下面是checkThread程式碼
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); }
}
- 那麼mThread是什麼時候建立的呢?下面我們看下ViewRootImpl的構造方法
* 那麼viewRootImpl物件什麼時候建立的呢?其實在WindowManagerImpl的addview中呼叫了WindowManagerGlobal的addview。在WindowManagerGlobal的addView的時候建立了ViewRootImpl物件
- 現在我們終於理清楚了,不能在子執行緒中更新ui的原因。
- 如果ViewRootImpl是在更新ui的時候,做了一個判斷。判斷建立自己的執行緒和更新ui的執行緒是否是同一個,不是,直接異常。
- 那麼我們能否手動的建立一個子執行緒,在這個執行緒中建立一個viewRootImpl呢?
- 下面我們帶著疑問寫一個demo
- 先看效果圖
- 下面是我們點選之後。在子執行緒中更新ui的效果圖
- 程式碼的原理是,我們在子執行緒中通過WindowManager新增一個view,而這個window所有的層級是系統層級。因此有懸浮效果。而我們建立的這個view因為是在子執行緒中直接建立了一個window,這個window的級別比較高,所以能顯示在其他應用上面。而這個window又沒有父window,因此其會單獨建立ViewRootImpl物件,而這個物件又是在子執行緒中建立的,那麼我們更新ui的時候,在這個子執行緒中更新能夠成功。
下面是核心程式碼,我們將會一步一步對其進行分析
new Thread() { @Override public void run() { Looper.prepare(); wm = (WindowManager) MyApplication.ctx.getSystemService(WINDOW_SERVICE); view = View.inflate(MainActivity.this, R.layout.item, null); tv = (TextView) view.findViewById(R.id.tv); params = new WindowManager.LayoutParams(); params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;// 設定最大的層級 以便顯示在其他應用的上面 // 設定不攔截焦點 params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; params.width = (int) (60 * getResources().getDisplayMetrics().density); params.height = (int) (60 * getResources().getDisplayMetrics().density); params.gravity = Gravity.LEFT | Gravity.TOP;// 且設定座標系 左上角 params.format = PixelFormat.TRANSPARENT; width = wm.getDefaultDisplay().getWidth(); height = wm.getDefaultDisplay().getHeight(); params.y = height / 2 - params.height / 2; wm.addView(view, params); view.setOnTouchListener(new View.OnTouchListener() { private int y; private int x; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: x = (int) event.getRawX(); y = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int minX = (int) (event.getRawX() - x); int minY = (int) (event.getRawY() - y); params.x = Math.min(width - params.width, Math.max(0, minX + params.x)); params.y = Math.min(height - params.height, Math.max(0, minY + params.y)); wm.updateViewLayout(view, params); x = (int) event.getRawX(); y = (int) event.getRawY(); break; case MotionEvent.ACTION_UP: if (params.x > 0 && params.x < width - params.width) { int x = params.x; if (x > (width - params.width) / 2) { params.x = width - params.width; } else { params.x = 0; } wm.updateViewLayout(view, params); } else if (params.x == 0 || params.x == (width - params.width)) { Toast.makeText(MainActivity.this, "被電擊了", Toast.LENGTH_SHORT).show(); tv.setText("abcd"); } break; } return true; } }); Looper.loop(); } }.start();
- 首先準備Looper,之後loop。因為更新view的時候會在當前的子執行緒中使用handler。而使用handler必須要looper。
- 接下來拿到windowManager wm = (WindowManager) MyApplication.ctx.getSystemService(WINDOW_SERVICE);
- 填充view
- WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 設定type,將window的級別設定較大,能夠顯示在其他的window之上
- params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;這裡是設定window透傳,也就是當前view所在的window不阻礙底層的window獲得觸控事件。
- 接下來設定window的寬度和高度
- params.format = PixelFormat.TRANSPARENT;設定透明 否則的話 圓形view後面顯示一層黑色,預設效果是黑色。需要設定,才能體現出圓形。
- 接下來就是設定Gravity了,這裡比較簡單,因為想實現懸浮視窗的拖拽效果,因此需要修改WindowManager的LayoutParams的x,y值。因此需要和gravity配合使用
- 接下來就是將view新增到WindowManager中了
- 剩下的就是觸控事件了
- 在鬆手的時候判斷了,更新了view中顯示的ui
- 下面是更新效果圖
- 初始文字為Click