1. 程式人生 > >自定義View Layout過程

自定義View Layout過程

前言


  • 自定義ViewAndroid開發者必須瞭解的基礎
  • 網上有大量關於自定義View原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等
  • 今天,我將全面總結自定義View原理中的Layout過程,我能保證這是市面上的最全面、最清晰、最易懂的

目錄

示意圖

1. 作用

計算檢視(View)的位置

即計算View的四個頂點位置:LeftTopRightBottom

2. 知識儲備

3. layout過程詳解

類似measure過程,layout過程根據View的型別分為2種情況:

示意圖

接下來,我將詳細分析這2種情況下的layout

過程

3.1 單一View的layout過程

  • 應用場景
    在無現成的控制元件View滿足需求、需自己實現時,則使用自定義單一View

    1. 如:製作一個支援載入網路圖片的ImageView控制元件
    2. 注:自定義View在多數情況下都有替代方案:圖片 / 組合動畫,但二者可能會導致記憶體耗費過大,從而引起記憶體溢位等問題。
  • 具體使用
    繼承自ViewSurfaceView 或 其他View;不包含子View

  • 具體流程

示意圖

下面我將一個個方法進行詳細分析

  • 原始碼分析
    layout過程的入口 = layout(),具體如下:
/**
  * 原始碼分析:layout()
  * 作用:確定View本身的位置,即設定View本身的四個頂點位置
  */
public void layout(int l, int t, int r, int b) { // 當前檢視的四個頂點 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 1. 確定View的位置:setFrame() / setOpticalFrame() // 即初始化四個頂點的值、判斷當前View大小和位置是否發生了變化 & 返回 // ->>分析1、分析2 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // 2. 若檢視的大小 & 位置發生變化
// 會重新確定該View所有的子View在父容器的位置:onLayout() if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); // 對於單一View的laytou過程:由於單一View是沒有子View的,故onLayout()是一個空實現->>分析3 // 對於ViewGroup的laytou過程:由於確定位置與具體佈局有關,所以onLayout()在ViewGroup為1個抽象方法,需重寫實現(後面會詳細說) ... } /** * 分析1:setFrame() * 作用:根據傳入的4個位置值,設定View本身的四個頂點位置 * 即:最終確定View本身的位置 */ protected boolean setFrame(int left, int top, int right, int bottom) { ... // 通過以下賦值語句記錄下了檢視的位置資訊,即確定View的四個頂點 // 從而確定了檢視的位置 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); } /** * 分析2:setOpticalFrame() * 作用:根據傳入的4個位置值,設定View本身的四個頂點位置 * 即:最終確定View本身的位置 */ private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); // 內部實際上是呼叫setFrame() return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } // 回到呼叫原處 /** * 分析3:onLayout() * 注:對於單一View的laytou過程 * a. 由於單一View是沒有子View的,故onLayout()是一個空實現 * b. 由於在layout()中已經對自身View進行了位置計算,所以單一View的layout過程在layout()後就已完成了 */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 引數說明 // changed 當前View的大小和位置改變了 // left 左部位置 // top 頂部位置 // right 右部位置 // bottom 底部位置 }

至此,單一Viewlayout過程已分析完畢。

  • 總結
    單一Viewlayout過程解析如下:
    示意圖

3.2 ViewGroup的layout過程

  • 應用場景
    利用現有的元件根據特定的佈局方式來組成新的元件

  • 具體使用
    繼承自ViewGroup 或 各種Layout;含有子 View

    如:底部導航條中的條目,一般都是上圖示(ImageView)、下文字(TextView),那麼這兩個就可以用自定義ViewGroup組合成為一個Veiw,提供兩個屬性分別用來設定文字和圖片,使用起來會更加方便。
    Paste_Image.png

  • 原理(步驟)

    1. 計算自身ViewGroup的位置:layout()
    2. 遍歷子View & 確定自身子View在ViewGroup的位置(呼叫子Viewlayout()):onLayout()

    a. 步驟2 類似於 單一Viewlayout過程
    b. 自上而下、一層層地傳遞下去,直到完成整個View樹的layout()過程

示意圖

  • 流程

示意圖

此處需注意:ViewGroupView 同樣擁有layout()onLayout(),但二者不同的:

  • 一開始計算ViewGroup位置時,呼叫的是ViewGrouplayout()onLayout()
  • 當開始遍歷子View & 計運算元View位置時,呼叫的是子Viewlayout()onLayout()

    類似於單一Viewlayout過程

  • 下面我將一個個方法進行詳細分析:layout過程入口為layout()

/**
  * 原始碼分析:layout()
  * 作用:確定View本身的位置,即設定View本身的四個頂點位置
  * 注:與單一View的layout()原始碼一致
  */ 
  public void layout(int l, int t, int r, int b) {  

    // 當前檢視的四個頂點
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  

    // 1. 確定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四個頂點的值、判斷當前View大小和位置是否發生了變化 & 返回 
    // ->>分析1、分析2
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若檢視的大小 & 位置發生變化
    // 會重新確定該View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

        onLayout(changed, l, t, r, b);  
        // 對於單一View的laytou過程:由於單一View是沒有子View的,故onLayout()是一個空實現(上面已分析完畢)
        // 對於ViewGroup的laytou過程:由於確定位置與具體佈局有關,所以onLayout()在ViewGroup為1個抽象方法,需重寫實現 ->>分析3
  ...

}  

/**
  * 分析1:setFrame()
  * 作用:確定View本身的位置,即設定View本身的四個頂點位置
  */ 
  protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
    // 通過以下賦值語句記錄下了檢視的位置資訊,即確定View的四個頂點
    // 從而確定了檢視的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

    }

/**
  * 分析2:setOpticalFrame()
  * 作用:確定View本身的位置,即設定View本身的四個頂點位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 內部實際上是呼叫setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    // 回到呼叫原處

/**
  * 分析3:onLayout()
  * 作用:計算該ViewGroup包含所有的子View在父容器的位置()
  * 注: 
  *      a. 定義為抽象方法,需重寫,因:子View的確定位置與具體佈局有關,所以onLayout()在ViewGroup沒有實現
  *      b. 在自定義ViewGroup時必須複寫onLayout()!!!!!
  *      c. 複寫原理:遍歷子View 、計算當前子View的四個位置值 & 確定自身子View的位置(呼叫子View layout())
  */ 
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

     // 引數說明
     // changed 當前View的大小和位置改變了 
     // left 左部位置
     // top 頂部位置
     // right 右部位置
     // bottom 底部位置

     // 1. 遍歷子View:迴圈所有子View
          for (int i=0; i<getChildCount(); i++) {
              View child = getChildAt(i);   

              // 2. 計算當前子View的四個位置值
                // 2.1 位置的計算邏輯
                ...// 需自己實現,也是自定義View的關鍵

                // 2.2 對計算後的位置值進行賦值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom

              // 3. 根據上述4個位置的計算值,設定子View的4個頂點:呼叫子view的layout() & 傳遞計算過的引數
              // 即確定了子View在父容器的位置
              child.layout(mLeft, mTop, mRight, mBottom);
              // 該過程類似於單一View的layout過程中的layout()和onLayout(),此處不作過多描述
          }
      }
  }

總結

對於ViewGroup的layout過程,如下:
示意圖

此處需注意:ViewGroupView 同樣擁有layout()onLayout(),但二者不同的:


  • 一開始計算ViewGroup位置時,呼叫的是ViewGrouplayout()onLayout()
  • 當開始遍歷子View & 計運算元View位置時,呼叫的是子Viewlayout()onLayout()

類似於單一Viewlayout過程
至此,ViewGrouplayout過程已講解完畢。

4. 例項講解

  • 為了更好理解ViewGrouplayout過程(特別是複寫onLayout()
  • 下面,我將用2個例項來加深對ViewGroup layout過程的理解
    1. 系統提供的ViewGroup的子類:LinearLayout
    2. 自定義View(繼承了ViewGroup類)

4.1 例項解析1(LinearLayout)

4.1.1 原理

  1. 計算出LinearLayout本身在父佈局的位置
  2. 計算出LinearLayout中所有子View在容器中的位置

4.1.2 具體流程

示意圖

4.1.2 原始碼分析

  • 在上述流程中,對於LinearLayout的layout()的實現與上面所說是一樣的,此處不作過多闡述
  • 故直接進入LinearLayout複寫的onLayout()分析
/**
  * 原始碼分析:LinearLayout複寫的onLayout()
  * 注:複寫的邏輯 和 LinearLayout measure過程的 onMeasure()類似
  */ 
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

      // 根據自身方向屬性,而選擇不同的處理方式
      if (mOrientation == VERTICAL) {
          layoutVertical(l, t, r, b);
      } else {
          layoutHorizontal(l, t, r, b);
      }
  }
      // 由於垂直 / 水平方向類似,所以此處僅分析垂直方向(Vertical)的處理過程 ->>分析1

/**
  * 分析1:layoutVertical(l, t, r, b)
  */
    void layoutVertical(int left, int top, int right, int bottom) {

        // 子View的數量
        final int count = getVirtualChildCount();

        // 1. 遍歷子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {

                // 2. 計運算元View的測量寬 / 高值
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                // 3. 確定自身子View的位置
                // 即:遞迴呼叫子View的setChildFrame(),實際上是呼叫了子View的layout() ->>分析2
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);

                // childTop逐漸增大,即後面的子元素會被放置在靠下的位置
                // 這符合垂直方向的LinearLayout的特性
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

/**
  * 分析2:setChildFrame()
  */
    private void setChildFrame( View child, int left, int top, int width, int height){

        // setChildFrame()僅僅只是呼叫了子View的layout()而已
        child.layout(left, top, left ++ width, top + height);

        }
    // 在子View的layout()又通過呼叫setFrame()確定View的四個頂點
    // 即確定了子View的位置
    // 如此不斷迴圈確定所有子View的位置,最終確定ViewGroup的位置

4.2 例項解析2:自定義View

  • 上面講的例子是系統提供的、已經封裝好的ViewGroup子類:LinearLayout
  • 但是,一般來說我們使用的都是自定義View;
  • 接下來,我用一個簡單的例子講下自定義Viewlayout()過程

4.2.1 例項檢視說明

例項檢視 = 1個ViewGroup(灰色檢視),包含1個黃色的子View,如下圖:
示意圖

4.2.2 原理

  1. 計算出ViewGroup在父佈局的位置
  2. 計算出ViewGroup中子View在容器中的位置

原理流程

4.2.3 具體計算邏輯

  • 具體計算邏輯是指計運算元View的位置,即計算四頂點位置 = 計算Left、Top、Right和Bottom;
  • 主要是寫在複寫的onLayout()
  • 計算公式如下:

示意圖

r = Left + width + Left;// 因左右間距一樣
b = Top + height + Top;// 因上下間距一樣

Left = (r - width) / 2Top = (b - height) / 2Right = width + Left;
Bottom = height + Top;

4.2.3 程式碼分析

因為其餘方法同上,這裡不作過多描述,所以這裡只分析複寫的onLayout()

/**
  * 原始碼分析:LinearLayout複寫的onLayout()
  * 注:複寫的邏輯 和 LinearLayout measure過程的 onMeasure()類似
  */ 
  @Override  
protected void onLayout(boolean changed, int l, int t, int r, int b) {  

     // 引數說明
     // changed 當前View的大小和位置改變了 
     // left 左部位置
     // top 頂部位置
     // right 右部位置
     // bottom 底部位置

        // 1. 遍歷子View:迴圈所有子View
        // 注:本例中其實只有一個
        for (int i=0; i<getChildCount(); i++) {
            View child = getChildAt(i);

            // 取出當前子View寬 / 高
            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();

            // 2. 計算當前子View的四個位置值
                // 2.1 位置的計算邏輯
                int mLeft = (r - width) / 2;
                int mTop = (b - height) / 2;
                int mRight =  mLeft + width;
                int mBottom =  mLeft + width;

            // 3. 根據上述4個位置的計算值,設定子View的4個頂點
            // 即確定了子View在父容器的位置
            child.layout(mLeft, mTop, mRight,mBottom);
        }
    }
}

佈局檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<scut.carson_ho.layout_demo.Demo_ViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#eee998"
    tools:context="scut.carson_ho.layout_demo.MainActivity">

    <Button
        android:text="ChildView"
        android:layout_width="200dip"
        android:layout_height="200dip"
        android:background="#333444"
        android:id="@+id/ChildView" />
</scut.carson_ho.layout_demo.Demo_ViewGroup >

  • 效果圖

示意圖

好了,你是不是發現,粘了我的程式碼但是畫不出來?!(如下圖)

實際示意圖

因為我還沒說draw流程啊哈哈哈!

draw流程:將View最終繪製出來

layout()過程講到這裡講完了,接下來我將繼續將自定義View的最後一個流程draw流程,有興趣就繼續關注我啦啦!!

5. 細節問題:getWidth() ( getHeight())與 getMeasuredWidth() (getMeasuredHeight())獲取的寬 (高)有什麼區別?

答:

首先明確定義:

  • getWidth() / getHeight():獲得View最終的寬 / 高
  • getMeasuredWidth() / getMeasuredHeight():獲得 View測量的寬 / 高

先看下各自的原始碼:

// 獲得View測量的寬 / 高
  public final int getMeasuredWidth() {  
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // measure過程中返回的mMeasuredWidth
  }  

  public final int getMeasuredHeight() {  
      return mMeasuredHeight & MEASURED_SIZE_MASK;  
      // measure過程中返回的mMeasuredHeight
  }  


// 獲得View最終的寬 / 高
  public final int getWidth() {  
      return mRight - mLeft;  
      // View最終的寬 = 子View的右邊界 - 子view的左邊界。
  }  

  public final int getHeight() {  
      return mBottom - mTop;  
     // View最終的高 = 子View的下邊界 - 子view的上邊界。
  }  

二者的區別:

示意圖

上面標紅:一般情況下,二者獲取的寬 / 高是相等的。那麼,“非一般”情況是什麼?

答:人為設定:通過重寫Viewlayout()強行設定


@Override
public void layout( int l , int t, int r , int b){

   // 改變傳入的頂點位置引數
   super.layout(l,t,r+100,b+100);

   // 如此一來,在任何情況下,getWidth() / getHeight()獲得的寬/高 總比 getMeasuredWidth() / getMeasuredHeight()獲取的寬/高大100px
   // 即:View的最終寬/高 總比 測量寬/高 大100px

}

雖然這樣的人為設定無實際意義,但證明了View的最終寬 / 高 與 測量寬 / 高是可以不一樣

特別注意

網上流傳這麼一個原因描述:

  • 實際上在當螢幕可包裹內容時,他們的值是相等的;
  • 只有當view超出屏幕後,才能看出他們的區別:getMeasuredWidth()是實際View的大小,與螢幕無關,而getHeight的大小此時則是螢幕的大小。當超出屏幕後getMeasuredWidth()等於getWidth()加上螢幕之外沒有顯示的大小

這個結論是錯的!詳細請點選文章

結論

在非人為設定的情況下,View的最終寬/高(getWidth() / getHeight()
View的測量寬/高 (getMeasuredWidth() / getMeasuredHeight())永遠是相等

6. 總結

  • 本文主要講解了自定義View中的Layout過程,總結如下:

示意圖

示意圖

請幫頂 / 評論點贊!因為你們的贊同/鼓勵是我寫作的最大動力!

相關推薦

定義View Layout過程

前言 自定義View是Android開發者必須瞭解的基礎 網上有大量關於自定義View原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 今天,我將全面總結自定義View原理中的Layout過程,我能保證這是市面上的最全面、

定義view佈局過程詳解

佈局過程,就是程式在執行時利用佈局檔案的程式碼來計算出實際尺寸的過程。 佈局分為兩個階段:測量階段和佈局階段。 測量階段:從上到下遞迴地呼叫每個 View 或者 ViewGroup 的 measure() 方法,測量他們的尺寸並計算它們的位置;  佈局階段:從上到下遞迴地呼叫每個 View 或

Android定義View-Layout原理篇

Android自定義View通常需要經過measure、layout和draw過程,如果你沒有了解過measure過程,可以先看看這篇文章。 一、Layout的作用:計算檢視的位置,即Left、Top、Right、Bottom四點的位置 二、layout過程:跟measu

定義View Draw過程- 最易懂的定義View原理系列(4)

前言 自定義View是Android開發者必須瞭解的基礎 網上有大量關於自定義View原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 今天,我將全面總結自定義View原理中的Draw過程,我能保證這是市面上的最全面、最清

定義View原理篇(2)- layout過程

1. 簡介 View的繪製過程分為三部分:measure、layout、draw。 measure用來測量View的寬和高。 layout用來計算View的位置。 draw用來繪製View。 經過measure之後就進入了layout過

定義View來畫大轉盤進行抽獎的過程

package com.example.app_bingtu_work; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import andro

定義ViewView的Measure測量過程解析

    相信絕大多數Android開發者都有自定義View來滿足各種各樣需求的經歷,也知道一個View的繪製展示要經過measure、layout、draw三大流程,三者中measure的過程相比是稍微複雜一點點的。這篇文章作為一個Android基礎的分享,分享

定義View:測量measure,佈局layout,繪製draw

1. 什麼是View 在Android的官方文件中是這樣描述的:表示了使用者介面的基本構建模組。一個View佔用了螢幕上的一個矩形區域並且負責介面繪製和事件處理。 手機螢幕上所有看得見摸得著的都是View。這一點對所有圖形系統來說都一樣,例如ios的UIVi

Android中View的繪製過程 onMeasure方法簡述 附有定義View例子

/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use thi

Android-定義View前傳-View的三大流程-Layout

defs llc 由於 就是 ready views spec protect wro Android-自定義View前傳-View的三大流程-Layout 參考 《Android開發藝術探索》 https://github.com/hongyangAndroid/Flo

定義view系列】View的measure過程

  View的測量過程是三大流程中最複雜的。 在現實生活中,如果我們要去畫一個圖形,就必須知道他的大小和位置。測量(測量view的寬和高),知道view的大小。 一.LayoutParams   LayoutParams繼承於Android.View.

Android 定義View

wid declare created odi lex getwidth 實現 tdi led   最近在看鴻洋大神的博客,在看到自定義部分View部分時,想到之前案子中經常會要用到"圖片 + 文字"這類控件,如下圖所示: 之前的做法是在布局文件中,將一個Imag

2.Border Layout 定義一個Layout來完成布局。

log 自定義 min int size ger 官方文檔 implement for 目標:          1.每一個被添加到布局裏的控件都是QLayoutItem,我們根據方位添加。 2.定義一個結構體 ItemWrapper。裏面包含QLayoutItem

定義VIew方法

bili change 鍵盤 boolean eve 失去 nat finish bool onFinishInflate() 回調方法,當應用從XML加載該組件並用它構建界面之後調用的方法 onMeasure() 檢測View組件及其子組件的大小 onLayout() 當

定義View總結

class net lin 定義 img view mage .net 技術分享 寫的很好,代你分析原碼,關於 View Measure 測量機制,讓我一次把話說完 自定義View總結

Android零基礎入門第24節:定義View簡單使用

子類 protect jin 討論 我們 @+ amp 進階 運行程序 當我們開發中遇到Android原生的組件無法滿足需求時,這時候就應該自定義View來滿足這些特殊的組件需求。 一、概述 很多初入Android開發的程序員,對於Android自定義View可能比較

簡單定義VIEW報錯問題

nfc 定義 http dnf androi dem and .com android aNDROIDNFC%E8%AF%BB%E5%8D%A1%E5%99%A8%E7%9A%84DEMO http://music.baidu.com/songlist/495819911

Android定義view詳解

this boolean mar 處理 都是 並且 jdk text 命名 從繼承開始 懂點面向對象語言知識的都知道:封裝,繼承和多態,這是面向對象的三個基本特征,所以在自定義View的時候,最簡單的方法就是繼承現有的View 通過上面這段代碼,我定義了一個Ske

Android -- 定義view實現keep歡迎頁倒計時效果

super onfinish -m use new getc awt ttr alt 1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,所以打算自己來寫寫,然後就有了這篇文章。 2,還是老規矩,先看一下我們今天實現的效果   相較於我們常見的倒計時

Android定義View效果目錄

class 重寫 自定義 textview 居中 url 冒泡 and 雷達圖 1、絢麗的loading動效的實現 2、Android自定義View:進度條+冒泡文本 3、Android雷達圖(蜘蛛網圖) 4、Android文本閃爍 5、Android繪制圓形進度條 6、重