Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 面試題總結之Android 進階(二)

Android 面試題總結之Android 進階(二)

編輯:關於Android編程

本章節將繼續深入理解View,關於View的繪制流程,View的事件分發。刷新機制等等。

掌握

Window是什麼? View的繪制流程 View的事件分發機制 View 與SurfaceView,GLSurfaceView

View的繪制流程

上一文章中我們已經自定義View以及View的三大過程,基本操作由三個函數完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法

Android 之美 從0到1

在理解View的繪制流程之前我們應該知道這幾個類:

View:最基本的UI組件,表示屏幕上的一個矩形區域。 Window: 是一個抽象基類,作用於外觀用戶界面和行為策略表示一個窗口,它包含一個View tree和窗口的layout 參數。View tree的root View可以通過getDecorView得到。還可以設置Window的Content View。其實現類是PhoneWindow。Activity,Dialog,Toast,都包含一個Window,該Window在Activity的attach()函數中mWindow = new PhoneWindow(this);創建。 DecorView:該類是PhoneWindow類的內部類,繼承自FrameLayout,它是所有應用窗口的根View,PhoneWindow設置DecorView為應用窗口的根視圖。 PhoneWindow:PhoneWindow對象幫我們創建了一個PhoneWindow內部類DecorView(父類為FrameLayout)窗口頂層視圖 ViewRootImpl:ViewRootImpl是連接WindowManager與DecorView的紐帶,View的整個繪制流程的三大步(measure、layout、draw)以及我們一些addView()的操作,都是通過ViewRootImpl完成的。

WindowManager:應用程序界面和窗口管理器
Android 之美 從0到1
在Activity onCreate使用的setContentView()就是設置的ContentView,通過LayoutInflater將xml內容布局解析成View樹形結構添加到DecorView頂層視圖中id為content的FrameLayout父容器上面。

那麼DecorView是如何繪制的呢?我們分兩個步驟來理解:

DecZ喎?/kf/ware/vc/" target="_blank" class="keylink">vclZpZXfM7bzTtb1XaW5kb3e1xLn9s8wgRGVjb3JWaWV3tcS75tbGuf2zzA0KPGgyIGlkPQ=="decorview添加到window的過程">DecorView添加到Window的過程

我們根據下圖步驟來解析DecorView添加到Window的過程,以便讓我們更容易的理解。

Android 之美 從0到1

**Activity初始化:**Activity 啟動,關於Activity的創建過程啊或者其他細節,因為不是本篇幅重點故不做詳細討論。我們盡量簡化理解View的繪制流程。

PhoneWindow的創建:
Activity對象創建完成後,初始化了PhoneWindow對象,該Window在Activity的attach()函數中mWindow = new PhoneWindow(this);創建,相關代碼塊如下:

final void attach(Context context, ActivityThread aThread..){
    ..........
   mFragments.attachHost(null /*parent*/);
            //創建PhoneWindow對象
        mWindow = new PhoneWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
            ..........
  }

DecorView添加Window:
ActivityThread.java類會調用handleResumeActivity方法將頂層視圖DecorView添加到PhoneWindow窗口,因此通過PhoneWindow的setContentView將Activity與Window進行關聯了。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
             //獲得當前Activity的PhoneWindow對象
              r.window = r.activity.getWindow();
               //獲得當前PhoneWindow內部類DecorView對象
                View decor = r.window.getDecorView();
                  //設置DecorView為可見
                decor.setVisibility(View.INVISIBLE);
                //獲取Activity的WindowManager
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                        //標記已添加至Window
                    a.mWindowAdded = true;
                    //添加DecorView到Window
                    wm.addView(decor, l);
                }

            }

接著DecorView通過WindowManager設置到ViewRootImpl中,然後就是下面DecorView的繪制流程了。

因此我們知道在Activity的onCreate和onResume方法中調用View.getWidth()和View.getMeasuredHeight()返回值是0,因為View 還沒有開始繪制。

View的繪制過程

ViewRootImpl是連接WindowManager與DecorView的紐帶,View的整個繪制流程的三大步(measure、layout、draw)都是通過ViewRootImpl完成的,
繪制是從根節點開始,對布局樹進行 measure 和 draw 。整個 View 樹的繪圖流程在 ViewRootImpl.java 類的 performTraversals() 函數展開,該函數所做 的工作可簡單概括為是否需要重新計算視圖大小(measure)、是否需要重新安置視圖的位置(layout)、以及是否需要重繪(draw),結合DecorView添加至Window過程,整體大概的流程圖如下:
這裡寫圖片描述
那麼我們圍繞圖上過程來分析View的繪制流程,首先我們進入ViewRootImpl.java中,查看performTraversals函數,這個函數非常長,View的繪制三大流程將在此展開。

“` private void performTraversals() {
// 緩存DecorView ,因為在下面用的比較多
final View host = mView;
…..
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(TAG,
“And hey let’s measure once more: width=” + width
+ ” height=” + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
…..
//獲得view寬高的測量規格,mWidth和mHeight表示窗口的寬高,lp.width和lp.height表示DecorView根布局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

    //執行測量操作
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    .....
    //執行布局操作
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ......
    //執行繪制操作
    performDraw();
}

“`

主要分下面三大步驟。

measure

measure操作主要用於計算視圖的大小
在前面文章 Android 之美 從0到1 Android 進階(一)中我們知道View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定,而對於DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同確定。
在ViewRootImpl的performTraversals方法中,完成了創建DecorView的MeasureSpec的過程,相應的代碼片段如下:

//獲得view寬高的測量規格,mWidth和mHeight表示窗口的寬高,lp.width和lp.height表示DecorView根布局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

我們知道Activity的根視圖總是全屏的,因為ViewRootImpl 在創建DecorView的MeasureSpec的過程 測量模式是EXACTLY,而Size是windowSize,相應的代碼片段如下:


private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//匹配父容器時,測量模式為MeasureSpec.EXACTLY,測量大小直接為屏幕的大小,也就是充滿真個屏幕
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
......
}
return measureSpec;
}

View 的measure過程

measure在performMeasure開始的,該函數在view中定義為final類型,要求子類不能修改。measure()函數中又會調用onMeasure()函數,相應的代碼片段如下:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      ...........
      //如果上一次的測量規格和這次不一樣,重新測量視圖View的大小
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }     ...........
      }

實際為整個View tree計算大小是onMeasure()函數,裡面直接調用setMeasuredDimension()提供一個默認模式View計算大小,相應的代碼片段如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

其中默認使用getDefaultSize() 獲取默認尺寸大小,如果自定義View不重寫onMesure(),在布局中使用wrap_content就相當於使用match_parent的效果相應的代碼片段如下:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //獲得測量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //獲得父親容器留給子視圖View的大小
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

普通View的measure()函數是由ViewGroup在measureChild方法中調用的,ViewGroup調用其子View的measure時即傳入了該子View的widthMeasureSpec和heightMeasureSpec,共同決定了View的大小。而DecorView是繼承自FrameLayout的,所以我們看下面ViewGroup的measure過程。

ViewGroup 的measure過程

ViewGroup需要先完成子View的measure過程,才能完成自身的measure過程,在ViewGroup的onMeasure()函數中,不同的布局(LinearLayout、RelativeLayout、FrameLayout等等)有不同的實現。FrameLayout的onMeasure()方法代碼如下:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //獲取子View的個數
        int count = getChildCount();
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //測量FrameLayout下每個子視圖View的寬和高
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        .............
 }       

歸納總結一張圖大致理解View 的 measure過程

這裡寫圖片描述

至此View的measure 過程大致清楚了,下面是View的layout過程。

layout

layout在view中定義為final類型,要求子類不能修改,用於設置子View的位置,因而是由父容器獲取子View的位置參數後,調用child.layout方法並傳入已獲取的位置參數,從而完成對子View的layout。相應的代碼片段如下:

public void layout(int l, int t, int r, int b) {
            //判斷是否需要重新測量
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
         //判斷布局是否發生改變,重新布局
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                //回調onLayout的方法,該方法由ViewGroup實現
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList listenersCopy =
                        (ArrayList)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

通過上面代碼我們知道layout主要完成兩個操作:setFrame(l,t,r,b),l,t,r,b即子視圖在父視圖中的具體位置,該函數用於將這些參數保存起來,onLayout() 是空方法由ViewGroup實現,在ViewGroup中,onLayout是一個抽象方法,因為對於不同的布局管理器類,對子元素的布局方式是不同的。而DecorView是繼承自FrameLayout的,所以我們看下面DecorView的onLayout代碼片段:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
    void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        final int count = getChildCount();
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
        //遍歷每一個子View 進行布局
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //當子視圖View可見度設置為GONE時,不進行當前子視圖View的布局
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft;
                int childTop;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //獲取子View的位置
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                //子View布局
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

歸納總結一張圖大致理解View 的 layout過程
這裡寫圖片描述

通過上面代碼我們知道主要是遍歷子View 獲取位置,進行布局,至此View的layout 過程大致清楚了,下面是View的draw過程

draw

View視圖繪制流程中的最後一步繪制draw是由ViewRootImpl中的performDraw成員方法開始的,用於繪制View內容到畫布上,每次發起繪圖時,並不會重新繪制每個View樹的視圖,而只會重新繪制那些“需要重繪”的視圖,View類內部變量包含了一個標志位DRAWN,當該視圖需要重繪時,就會為該View添加該標志位。(View不需要繪制任何內容,可通過這個方法將相應標記設為true,系統會進行相應優化。ViewGroup默認開啟這個標記,View默認不開啟)相應代碼片段如下:

    //設置是否需要重繪
   public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

繪制開始

private void performDraw() {
            ....
        if (!mAttachInfo.mScreenOn && !mReportNextDraw) {
            return;
        }

        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
        try {
                //調用下面draw方法
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        .....
}

接著會在ViewRootImpl類中的drawSoftware方法繪制View,然後調用View的成員方法draw開始繪制,相應代碼塊如下:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
            boolean scalingRequired, Rect dirty) {
            ............
                    try {
                canvas.translate(0, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;

                mView.draw(canvas);

        ............
        return true;
    }

接著我們看mView.draw() 開始繪制,主要做了以下6件事:

繪制該View的背景
如果要視圖顯示漸變框,這裡會做一些准備工作 調用onDraw()方法繪制視圖本身 ,每個View都需要override該方法,ViewGroup不需要實現該方法,因為ViewGroup沒有內容,但是ViewGroup需要通知View 調用onDraw函數,也就是下面的dispatchDraw(); 繪制子視圖的內容,dispatchDraw()函數。在View中這是個空函數,具體的視圖不需要實現該方法,ViewGroup類已經為我們重寫了dispatchDraw()的功能實現,該方法內部會遍歷每個子視圖,調用drawChild()去重新回調每個子視圖的draw()方法。 如果需要, 繪制當前視圖在滑動時的邊框漸變效果 繪制滾動條

理解相應代碼塊如下:

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
            // Step 4, draw the children
            dispatchDraw(canvas);
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
            // we're done...
            return;
        }
        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */
        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;
        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;
        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;
        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
     .........
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;
        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }
        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }
       .........
        saveCount = canvas.getSaveCount();
        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }
            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }
            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
       ........
        canvas.restoreToCount(saveCount);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

歸納總結一張圖大致理解View 的 draw過程
這裡寫圖片描述
至此View的繪制流程就大致清楚了,通過ViewRootImpl完成的,
繪制是從根節點開始,對布局樹進行 measure 和 layout、draw,接下來在最後面我們小結一下一些View比較重要的問題。

View的事件分發機制

理解View的事件機制有助於解決開發過程中經常會遇到滑動、點擊事件沖突問題、View事件機制已經是android開發者必不可少的知識。那麼
之前《Android 面試題總結之Android 基礎 (六)》一文中我們已經熟悉了View 和ViewGroup之間的關系,為我們理解View的事件分發機制奠定了基礎。

View 事件構成

在Android中,事件主要包括onClick、onLongClick、onScroll、onFling等,onClick又包括單擊和雙擊,另外還包括單指操作和多指操作。
用戶在手指與屏幕接觸過程中通過MotionEvent對象產生一系列事件,它有四種狀態:

按下(ACTION_DOWN) 移動(ACTION_MOVE) 抬起(ACTION_UP) 退出 (ACTION_CANCEL) 一般由程序產生,不會由用戶產生

所有這些都構成了Android中的事件響應,Touch事件由 Action_Down、Action_Move、Aciton_UP 組成,其中一次完整的Touch事件中,Down 和 Up 都只 有一個,Move 有若干個,可以為 0 個。

View的事件分發

我們先來了解三個常見的函數的作用。

public boolean dispatchTouchEvent(MotionEvent event)
進行事件分發 public boolean onTouchEvent(MotionEvent event)
進行事件處理 public boolean onInterceptTouchEvent(MotionEvent event)
進行事件攔截,注意該方法只有ViewGroup才會有,普通的View沒有子View也就沒有必要進行攔截了

View事件機制三個過程

事件分發的過程

在這個事件分發過程,我們分為ViewGroup的事件分發過程和View的事件分發過程這兩個方面。

public boolean dispatchTouchEvent(MotionEvent event)
Android中所有的事件都必須經過這個方法的分發,然後決定是自身消費當前事件還是繼續往下分發給子控件處理進行事件分發,dispatchTouchEvent 的事件分發邏輯如下:

如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞; 如果 return false,事件分發給父View的onTouchEvent 進行消費(如果在最外層是Activity 則是返回給Activity的onTouchEvent): 如果返回系統默認的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 ViewGroup 的 onInterceptTouchEvent方法 ,如果是View 就繼續往下分發。

ViewGroup分發過程

當事件分發到ViewGroup的dispatchTouchEvent方法,如果返回系統默認的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 ViewGroup 的 onInterceptTouchEvent方法,如果onInterceptTouchEvent 返回true,則調用onTouchEvent方法進行事件處理,否則繼續向child View.dispatchTouchEvent分發。

Android中事件傳遞按照從上到下進行層級傳遞,事件處理從Activity開始到ViewGroup再到View,舉個下面的例子:
Android 之美 從0到1
當一個 Touch 事件(觸摸事件為例)到達根節點,即 Acitivty 的 DecorView 時,它會依次下發,下發的過程是調用子 View(ViewGroup)的 dispatchTouchEvent 方法實現的。簡單來說,就是 ViewGroup 遍歷它包含著的子 View,再進行判斷當前的x,y坐標是否落在子View身上,如果在,那麼調用每個 View 的 dispatchTouchEvent 方法,而當子 View 為 ViewGroup 時,又會通過調用 ViwGroup 的dispatchTouchEvent 方法繼續調用其內部的 View的 dispatchTouchEvent 方法。上述例子中的消息下發順序是這樣的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent 方法只負責事件的分發,它擁有 boolean 類型的返回值,當返回為 true 時,順序下發會中斷。在上述例子中如果的 dispatchTouchEvent 返回結果為 true,那麼⑥-⑦-③-④將都接收不到本次 Touch 事件,標志著本次事件取消。

View的事件分發

下面通過點擊Activity上的一個Button小例子來分析View的事件流程:

先來一張簡化的流程圖
這裡寫圖片描述
當點擊這個Button,首先執行到的是MainActivity的dispatchTouchEvent方法,這將是事件分發的開始。

如果MainActivity 不進行攔截,那麼繼續分發給Button,詢問Button是否分發,如果不進行分發,回調onTouchEvent是否進行事件消費。 如果MainActivity 進行攔截,子View就分發不到。

總結一張圖理解事件分發流程(紅色箭頭流向):
這裡寫圖片描述

事件響應的過程

響應的過程是子View 回傳遞到父View的過程
還是用這張圖來理解事件響應流程(綠色箭頭):
這裡寫圖片描述

如果子View(Button)消費了事件,所以開始回傳,一層一層往上告訴父View他已經消費了事件。 如果子View(Button)沒有消費事件,也開始回傳,一層一層往上告訴父View他沒有消費事件,問ViewGroup2是否消費事件,如果ViewGroup1也不消費事件,繼續回傳到ViewGroup1,問ViewGroup1是否消費事件,如果也不消費事件,最終回傳到Activity,讓Activity去消費。

事件處理的過程

onTouchEvent方法用於事件的處理,

如果事件傳遞到當前 View(Btuuon) 的 onTouchEvent 方法,如果返回false,那麼這個事件會從當前 View(Bttuon) 向上ViewGroup1傳遞。 如果返回了 true 接收並消費該事件。 如果返回 super.onTouchEvent(ev) 默認處理事件的邏輯和返回 false 時相同。
最終當一個View接收到了觸碰事件時,會調用其onTouchEvent方法.相關代碼塊如下:
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {}

如果此view被禁用了 返回的是false;相關代碼塊如下:

// 如果View被禁用的話,則返回它是否可以點擊。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    return (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

如果此View有觸碰事件處理代理,那麼將此事件交給mTouchDelegate,相關代碼塊如下:

// 如果該View的mTouchDelegate不為null的話,將觸摸消息分發給mTouchDelegate。
// mTouchDelegate的默認值是null。
 if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

如果View不可點擊則直接返回false,如果可以點擊進入處理點擊,更新View狀態等。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
        if (!post(mPerformClick)) {
                        performClick();
                   }
        }

View 與SurfaceView,GLSurfaceView

SurfaceView是從View基類中派生出來的顯示類。android游戲開發中常用的三種視圖是:view、SurfaceView和GLSurfaceView

View:顯示視圖,內置畫布,提供圖形繪制函數、觸屏事件、按鍵事件函數等;必須在UI主線程內更新畫面,速度較慢。 SurfaceView:基於view視圖進行拓展的視圖類,更適合2D游戲的開發;是view的子類,類似使用雙緩機制,在新的線程中更新畫面所以刷新界面速度比view快,缺點 非常消耗cpu和內存的開銷 GLSurfaceView:基於SurfaceView視圖再次進行拓展的視圖類,專用於3D游戲開發的視圖;是SurfaceView的子類,openGL專用,。

問題總結

View的繪制流程分幾步,從哪開始?哪個過程結束以後能看到view?
ViewRootImplperformTraversals開始,經過measure,layout,draw 三個流程。draw流程結束以後就可以在屏幕上看到view了。

view的測量寬高和實際寬高有區別嗎?
基本上百分之99的情況下都是可以認為沒有區別的。有兩種情況,有區別。第一種 就是有的時候會因為某些原因 view會多次測量,那第一次測量的寬高 肯定和最後實際的寬高 是不一定相等的,但是在這種情況下

最後一次測量的寬高和實際寬高是一致的。此外,實際寬高是在layout流程裡確定的,我們可以在layout流程裡 將實際寬高寫死 寫成硬編碼,這樣測量的寬高和實際寬高就肯定不一樣了,雖然這麼做沒有意義 而且也不好。

view的measureSpec 由誰決定?頂級view呢?
View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定,而對於DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同確定。
在ViewRootImpl的performTraversals方法中,完成了創建DecorView的MeasureSpec的過程,一旦確定了spec,onMeasure中就可以確定view的寬高了。

對於普通view來說,他的measure過程中,與父view有關嗎?如果有關,這個父view也就是viewgroup扮演了什麼角色?

對於普通view的measure來說 是由這個view的 父view ,也就是viewgroup來觸發的。
通過前面歸納總結一張圖大致理解View 的 measure過程

這裡寫圖片描述

view的meaure和onMeasure有什麼關系?
view的measure是final 方法 我們子類無法修改的,是在measure方法裡調用了onMeasure方法。

自定義view中 如果onMeasure方法 沒有對wrap_content 做處理 會發生什麼?為什麼?怎麼解決?

如果沒有對wrap_content做處理 ,那即使你在xml裡設置為wrap_content.其效果也和match_parent相同。
解決方式就是在onMeasure裡 針對wrap 來做特殊處理 比如指定一個默認的寬高,當發現是wrap_content 就設置這個默認寬高即可。

ViewGroup有onMeasure方法嗎?為什麼?

沒有,這個方法是交給子類自己實現的。不同的viewgroup子類 肯定布局都不一樣,那onMeasure索性就全部交給他們自己實現好了。

為什麼在activity的生命周期裡無法獲得測量寬高?有什麼方法可以解決這個問題嗎?

因為measure的過程和activity的生命周期 沒有任何關系。你無法確定在哪個生命周期執行完畢以後 view的measure過程一定走完。可以嘗試如下幾種方法 獲取view的測量寬高。

    //重寫activity的這個方法
public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = tv.getMeasuredWidth();
            int height = tv.getMeasuredHeight();
            Log.v("burning", "width==" + width);
            Log.v("burning", "height==" + height);

        }
    }

延時一段時間,等待控件測量、布局完成後再獲取

//延時一段時間,等待控件測量、布局完成後再獲取
@Override
    protected void onStart() {
        super.onStart();
        tv.post(new Runnable() {
            @Override
            public void run() {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
            }
        });
    }

監聽onlayout方法執行完成之後,就可以獲取控件大小了

    @Override
        protected void onStart() {
            super.onStart();
            ViewTreeObserver observer = tv.getViewTreeObserver();
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    int width = tv.getMeasuredWidth();
                    int height = tv.getMeasuredHeight();
                    tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            });
        }

draw方法 大概有幾個步驟?
主要分為6個步驟:

繪制該View的背景
如果要視圖顯示漸變框,這裡會做一些准備工作 調用onDraw()方法繪制視圖本身 ,每個View都需要override該方法,ViewGroup不需要實現該方法,因為ViewGroup沒有內容,但是ViewGroup需要通知View 調用onDraw函數,也就是下面的dispatchDraw(); 繪制子視圖的內容,dispatchDraw()函數。在View中這是個空函數,具體的視圖不需要實現該方法,ViewGroup類已經為我們重寫了dispatchDraw()的功能實現,該方法內部會遍歷每個子視圖,調用drawChild()去重新回調每個子視圖的draw()方法。 如果需要, 繪制當前視圖在滑動時的邊框漸變效果 繪制滾動條

View的刷新機制?
當子View需要刷新時會調用子View的invalidate()來重新繪制。View的刷新機制,是通過父View負責刷新、布局顯示子View;而當子View需要刷新時,則是通知父View來完成,我們可通過下圖更容易理解之間的關系。
http://blog.csdn.net/vfush

事件分發中的 onTouch 和 onTouchEvent 有什麼區別,又該如何使用?
這兩個方法都是在 View 的 dispatchTouchEvent 中調用的,onTouch 優先於 onTouchEvent 執行。如果在 onTouch 方法中通過返回 true 將事件消費掉,onTouchEvent 將不會再執行。
onTouch 執行需要滿足兩個條件:

mOnTouchListener 的值不能為空 當前點擊的控件必須是 enable 的。因此如果你有一個控件是非 enable 的,那麼給它注冊 onTouch 事件將永遠得不到 執行。對於這一類控件,如果我們想要監聽它的 touch 事件,就必須通過在該控件中重寫 onTouchEvent 方法來實現,相關代碼塊如下:
 if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

onTouch 和onClick有什麼區別,?
onTouch事件要先於onClick事件執行,onTouch在事件分發方法dispatchTouchEvent中調用,而onClick在事件處理方法onTouchEvent中被調用,onTouchEvent要後於dispatchTouchEvent方法的調用。

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