Android View總結

jopen 8年前發布 | 13K 次閱讀 Android Android開發 移動開發

關于Android View控件

Android中控件大致被分為兩類ViewGroup,View。ViewGroup作為容器管理View。Android視圖,是類似于Dom樹的架構。父視圖負責測量定位繪制等操作。我們經常在用的findViewById方法代價昂貴的原因,就是因為他負責至上而下遍歷整棵控件樹,來尋找View實例,在重復操作中盡量少用。現在在用的很多控件都是直接或者間接繼承自View的,如下圖。

Android View總結

Android UI界面架構

每個Activity包含一個PhoneWindow對象,PhoneWindow設置DecorView為應用窗口的根視圖。在里面就是熟悉的TitleView和ContentView,沒錯,平時使用的setContentView()就是設置的ContentView。

Android View總結

Android是如何繪制View的?

當一個Activity啟動時,會被要求繪制出它的布局。Android框架會處理這個請求,當然前提是Activity提供了合理的布局。繪制從根視圖開始,從上至下遍歷整棵視圖樹,每一個ViewGroup負責讓自己的子View被繪制,每一個View負責繪制自己,通過draw()方法.繪制過程分三步走。

  • Measure
  • Layout
  • Draw
  • </ul>

    整個繪制流程是在ViewRoot中的performTraversals()方法展開的。部分源代碼如下。

    private void performTraversals() {
        ......
        //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
        //lp.width和lp.height在創建ViewGroup實例時等于MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }

    在繪制之前當然要知道view的尺寸和繪制。所以先進行measu和layout(測量和定位),如下圖。

    Android View總結

    Measure過程

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //....

    //回調onMeasure()方法    
    onMeasure(widthMeasureSpec, heightMeasureSpec);  
    
    //more  
    

    }</pre>

    計算view的實際大小,獲得高寬存入mMeasuredHeight和mMeasureWidth,measure(int, int)傳入的兩個參數。MeasureSpec是一個32位int值,高2位為測量的模式,低30位為測量的大小。測量的模式可以分為以下三種。

    • EXACTLY

      精確值模式,當layout_width或layout_height指定為具體數值,或者為match_parent時,系統使用EXACTLY。

      </div> </li>

    • AT_MOST

      最大值模式,指定為wrap_content時,控件的尺寸不能超過父控件允許的最大尺寸。

      </div> </li>

    • UNSPECIFIED不指定測量模式,View想多大就多大,一般不太使用。

      </li> </ul>

      根據上面的源碼可知,measure方法不可被重寫,自定義時需要重寫的是onMeasure方法

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

      查看源碼可知最終的高寬是調用setMeasuredDimension()設定的,如果不重寫,默認是直接調用getDefaultSize獲取尺寸的。

      使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之后被調用才能返回有效值。

      Layout過程

      Layout方法就是用來確定view布局的位置,就好像你知道了一件東西的大小以后,總要知道位置才能畫上去。

      mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

      layout獲取四個參數,左,上,右,下坐標,相對于父視圖而言。這里可以看到,使用了剛剛測量的寬和高。

      public void layout(int l, int t, int r, int b) {
          int oldL = mLeft;
          int oldT = mTop;
          int oldB = mBottom;
          int oldR = mRight;
          boolean changed = setFrame(l, t, r, b);
          if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
              .....
              onLayout(changed, l, t, r, b);
              .....
      }

      通過setFrame設置坐標。如果坐標改變過了,則重新進行定位。如果是View對象,那么onLayout是個空方法。因為定位是由ViewGroup確定的。

      當layout結束以后getWidth()與getHeight()才會返回正確的值。

      這里出現一個問題,getWidth/Height()andgetMeasuredWidth/Height()有什么區別?

      • getWidth():View在設定好佈局後整個View的寬度。
      • getMeasuredWidth():對View上的內容進行測量後得到的View內容佔據的寬度
      • </ul>

        Android View總結

        Draw過程

        public void draw(Canvas canvas) {
                ......
                /*

             * 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
            ......
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
        
            // skip step 2 & 5 if possible (common case)
            ......
        
            // Step 2, save the canvas' layers
            ......
                if (drawTop) {
                    canvas.saveLayer(left, top, right, top + length, null, flags);
                }
            ......
        
            // 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
            ......
            if (drawTop) {
                matrix.setScale(1, fadeHeight * topFadeStrength);
                matrix.postTranslate(left, top);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(left, top, right, top + length, p);
            }
            ......
        
            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);
            ......
        }</pre><br />
        

        重點是第三步調用onDraw方法。其它幾步都是繪制一些邊邊角角的東西比如背景、scrollBar之類的。其中dispatchDraw,是用來遞歸調用子View,如果沒有則不需要。

        onDraw方法是需要自己實現的,因為每個控件繪制的內容不同。主要用canvas對象進行繪制,這里就不說了。

        參考資料

        1. Android視圖繪制流程完全解析,帶你一步步深入了解View(二)
        2. Android應用層View繪制流程與源碼分析
        3. How Android Draws Views
        4. 《Android群英傳》
        5. What is the difference between getWidth/Height() and getMeasuredWidth/Height() in Android SDK?
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!