Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android-如何在子線程中更新ui

android-如何在子線程中更新ui

編輯:關於Android編程

正如我們知道的,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對象

<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="" src="/uploadfile/Collfiles/20160824/20160824092324623.png" title="\" />

現在我們終於理清楚了,不能在子線程中更新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

因此能否在子線程中更新ui,由ViewRootImpl在哪個線程中創建決定。因此我們更應該將能更新ui的線程成為ui線程而不是主線程。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved