1. 程式人生 > >不可不知的開發技巧之View.Post()

不可不知的開發技巧之View.Post()

稍微有點經驗的安卓開發人員應該都知道View類的post和postDelayed方法。我們知道呼叫這個方法可以保證在UI執行緒中進行需要的操作,方便地進行非同步通訊。以下是官方文件對該方法的註釋及原始碼。(postDelayed類似,不再贅述)

Causes the Runnable to be added to the message queue.The runnable will be run on the user interface thread.
public boolean post(Runnable action) {    
  final AttachInfo attachInfo =
mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; }

意思是將任務交由attachInfo中的Handler處理,保證在UI執行緒執行。從本質上說,它還是依賴於以Handler、Looper、MessageQueue、Message為基礎的非同步訊息處理機制。相對於新建Handler進行處理更加便捷。因為attachInfo中的Handler其實是由該View的ViewRootImpl提供的,所以post方法相當於把這個事件新增到了UI 事件佇列中。下面舉一個常用的例子,比如在onCreate方法中獲取某個view的寬高,而直接View#getWidth獲取到的值是0。要知道View顯示到介面上需要經歷onMeasure、onLayout和onDraw三個過程,而View的寬高是在onLayout階段才能最終確定的,而在Activity#onCreate中並不能保證View已經執行到了onLayout方法,也就是說Activity的宣告週期與View的繪製流程並不是一一繫結。那為什麼呼叫post方法就能起作用呢?首先MessageQueue是按順序處理訊息的,而在setContentView()後佇列中會包含一條詢問是否完成佈局的訊息,而我們的任務通過View#post方法被新增到佇列尾部,保證了在layout結束以後才執行。

下面我舉一個例子,加深大家的理解。假如你有一個需求,需要在某個頁面中加入一個點選事件:從AppBar(ActionBar)下滑出一個控制元件,並且在該控制元件出現的同時依然可以點選AppBar上的MenuItem。因此不能使用popupWindow,只能將該控制元件放置於佈局內,同時為了不阻礙其他控制元件的互動,需要將該view的可見性設定為GONE。於是你寫下下面的程式碼

private void show(){
    if(myView.getVisibility()==View.GONE){
     myView.setVisibility(View.VISIBLE);
     }
    //對view執行動畫
}

但執行後卻發現view是一瞬間顯現的,並沒有執行動畫,我想你也猜到了,問題在於動畫開始時該View還未成功被設定為可見,導致動畫失效。
而把動畫執行加入到View#post中後可以達到理想的效果

private void show(){
  if(myView.getVisibility()==View.GONE){
     myView.setVisibility(View.VISIBLE);
  }
     myView.post(new Runnable() {
            @Override
            public void run() {
                //對view執行動畫
            }
        });
  }

原因在於將view的可見性從GONE設定為VISIBLE時會申請requestLayout,而layout是非同步執行的,所以setVisibility方法返回了但是該操作還未完成。前面說了post方法可以保證新任務是在layout呼叫過後執行。下面是setVisibility方法中比較關鍵的程式碼:

if (newVisibility == VISIBLE) {
        if ((changed & VISIBILITY_MASK) != 0) {
            /*
             * If this view is becoming visible, invalidate it in case it changed while
             * it was not visible. Marking it drawn ensures that the invalidation will
             * go through.
             */
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(true);

            needGlobalAttributesUpdate(true);

            // a view becoming visible is worth notifying the parent
            // about in case nothing has focus.  even if this specific view
            // isn't focusable, it may contain something that is, so let
            // the root view try to give this focus if nothing else does.
            if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) {
                mParent.focusableViewAvailable(this);
            }
        }
    }

    /* Check if the GONE bit has changed */
    if ((changed & GONE) != 0) {
        needGlobalAttributesUpdate(false);
        // 這裡申請了重新佈局
        requestLayout();

        if (((mViewFlags & VISIBILITY_MASK) == GONE)) {
            if (hasFocus()) clearFocus();
            clearAccessibilityFocus();
            destroyDrawingCache();
            if (mParent instanceof View) {
                // GONE views noop invalidation, so invalidate the parent
                ((View) mParent).invalidate(true);
            }
            // Mark the view drawn to ensure that it gets invalidated properly the next
            // time it is visible and gets invalidated
            mPrivateFlags |= PFLAG_DRAWN;
        }
        if (mAttachInfo != null) {
            mAttachInfo.mViewVisibilityChanged = true;
        }
    }

總結

呼叫View.post()既方便又可以保證指定的任務在檢視操作中順序執行。