【Android】自定義控件之View原理與使用

MelMora 8年前發布 | 30K 次閱讀 安卓開發 Android開發 移動開發

一、簡介

不論在學習Android還是在做Android開發,我們都離不開View,所以學好View對一個Android開發人員來說尤為重要。Android中的每個控件都會在界面上得到一塊矩形的區域,而在Android中,控件大致被分為 兩類 ,即 ViewGroup 控件和 View 控件。ViewGroup控件作為父控件可以包含多個View控件,并管理其包含的View控件。下面分條來對View做一個簡單的介紹。

二、View的原理介紹

  1. View表示的的屏幕上的某一塊矩形的區域,而且所有的View都是矩形的;
  2. 如同簡介中介紹,View是不能添加子View的,而ViewGroup是可以添加子View的。ViewGroup之所以能夠添加子View,是因為它實現了兩個接口: ViewParentViewManager
  3. Activity之所以能加載并且控制View,是因為它包含了一個Window,所有的圖形化界面都是由View顯示的而Service之所以稱之為沒有界面的activity是因為它不包含有Window,不能夠加載View;
  4. 一個View有且只能有一個父View;
  5. 在Android中Window對象通常由PhoneWindow來實現的,PhoneWindow將一個DecorView設置為整個應用窗口的根View,即 DecorView為整個Window界面的最頂層View 。也可以說DecorView將要顯示的具體內容呈現在了PhoneWindow上;
  6. DecorView是FrameLayout的子類,它繼承了FrameLayout,即頂層的FrameLayout的實現類是Decorview,它是在phoneWindow里面創建的;
  7. 頂層的FrameLayout的父view是Handler,Handler的作用除了線程之間的通訊以外,還可以跟WindowManagerService進行通訊;
  8. windowManagerService是后臺的一個服務,它控制并且管理者屏幕;
  9. 一個應用可以有很多個window,其由windowManager來管理,而windowManager又由windowManagerService來管理;
  10. 如果想要顯示一個view那么他所要經歷三個方法: 1.測量measure , 2.布局layout , 3.繪制draw

三、View的測量/布局/繪制過程

顯示一個View主要進過以下三個步驟:

  • 1、Measure測量一個View的大小
  • 2、Layout擺放一個View的位置
  • 3、Draw畫出View的顯示內容
    其中measure和layout方法都是final的,無法重寫,雖然draw不是final的,但是也不建議重寫該方法。
    這三個方法都已經寫好了View的邏輯,如果我們想實現自身的邏輯,而又不破壞View的工作流程,可以重寫onMeasure、onLayout、onDraw方法。下面來一一介紹這三個方法。

Paste_Image.png

3.1 View的測量

Android系統在繪制View之前,必須對View進行測量,即告訴系統該畫一個多大的View,這個過程在onMeasure()方法中進行。測量過程如下圖所示:

Paste_Image.png

3.1.1 MeasureSpec類

Android系統給我們提供了一個設計小而強的工具類——— MeasureSpec類

1、MeasureSpe描述了父View對子View大小的期望。里面包含了測量模式和大小。

2、MeasureSpe類把測量模式和大小組合到一個32位的int型的數值中,其中高2位表示模式,低30位表示大小而在計算中使用位運算的原因是為了提高并優化效率。

3、我們可以通過以下方式從MeasureSpec中提取模式和大小,該方法內部是采用位移計算。

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

也可以通過MeasureSpec的靜態方法把大小和模式合成,該方法內部只是簡單的相加。

MeasureSpec.makeMeasureSpec(specSize,specMode);

3.1.2 測量模式

在對View進行測量時,Android提供了三種測量模式:

  • 1. EXACTLY

    精確值模式 ,當控件的layout_width屬性或layout_height屬性指定為具體數值時,例如android:layout_width="100dp",或者指定為match_parent屬性時,系統使用的是EXACTLY 模式。

  • 2. AT_MOST

    最大值模式 ,當控件的layout_width屬性或layout_height屬性指定為warp_content時,控件大小一般隨著控件的子控件或者內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大尺寸即可。

  • 3.UNSPECIFIED

    這個屬性很奇怪,因為它不指定其大小測量的模式,View想多大就多大,通常情況下在繪制自定義View時才會使用。

View默認的onMeasure()方法只支持EXACTLY模式,所以如果在自定義控件的時候不重寫onMeasure()方法的話,就只能使用EXACTLY模式,且控件只可以響應你指定的具體寬高值或者是match_parent屬性。如果要讓自定義的View支持wrap_content屬性,那么就必須重寫onMeasure()方法來指定wrap_content時的大小。

而通過上面介紹的MeasureSpec這個類,我們就可以獲取View的測量模式和View想要繪制的大小。

3.1.3 MeasureSpec判定規則

在自定義View的時候要通過判斷測量的模式,給出不同的測量值,下面的一張圖表羅列了 MeasureSpec判定規則。

Paste_Image.png

3.1.4 實例演示

step1 :自定義一個類繼承FrameLayout重寫構造方法:

public class CustomView extends FrameLayout {
    //構造方法省略...
}

step2 :重寫onMeasure()方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

通過查看super.onMeasure()方法,發現系統最終會調用setMeasureDimension(int measuredWidth, int measuredHeight)方法將測量后的寬高設置進去,從而完成測量工作。所以接下來要做的就是將最終測量后的寬高值作為參數設置給setMeasureDimension()方法,即重寫的onMeasure()方法代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasureDimension(
          measureWidth(widthMeasureSpec), 
          measureHeigth(heightMeasureSpec));
}

因為在上面我們調用了自定義的measureWidth()方法和measureHeight()方法對寬高進行了重新定義,接下來我們就來自定義測量值。

step3 :自定義測量值:

這里以measureWidth()方法為例,來進行自定義測量值操作。

首先,從MeasureSpec對象中獲取到測量模式和測量大小值:

int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);

其次,通過判斷測量模式,給出不同的測量值:

①當specMode為EXACTLY時,直接使用指定的specSize即可;

②當specMode為其他兩種模式時,需要給它一個默認的大小。

注: 如果指定的是wrap_content屬性,即AT_MOST模式,則需要取出我們指定的大小與specSize中 最小 的一個來作為最后的測量值。參考代碼如下:

private int measureWidth(int measureSpec) {
int width = 0; /**

 * 1、從MeasureSpec對象中提出出具體的測量模式和大小
 */
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
/**
 * 2、通過判斷測量模式,給出不同的測量值
 */
if (specMode == MeasureSpec.EXACTLY) {   // match_parent , accurate
    width = specSize;
} else {
    width = 200;    //給一個默認的大小
    if (specMode == MeasureSpec.AT_MOST) {  // wrap_content 
       width = Math.min(width,specSize); //注意取兩者之間小的值 
   }
}
return width;

}</code></pre>

對于measureHeight()方法基本上與上面的measureWidth()方法一致,此處省略。

通過以上三個步驟即可搞定View的測量,接下來簡單介紹一下布局。

3.1.5 拓展

如果想在activity中的onCreat()方法中獲取控件測量以后的寬跟高,那么可以用下面的方法:

final TextView tv = (TextView) findViewById(R.id.tv);
tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        int measuredWidth = tv.getMeasuredWidth();
        int measuredHeight = tv.getMeasuredHeight();
    }
});

3.2 View的布局

首先我們來看一下layout布局流程圖:

Paste_Image.png

接下來一一介紹上面流程圖中的三個參數:

  • layout

    ①Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。

    ②子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,特別是RelativeLayout,影響布局的因素非常多。

    ③layout方法雖然可以被復寫,但是不建議去復寫,我們可以直接調用layout方法去確定自身的位置, 而且可以去復寫onLayout方法去確定子view的位置

  • setFrame

    ①setFrame方法是一個隱藏方法,所以作為應用層程序員來說,無法重寫該方法。該方法體內部通過比對本次的l、t、r、b四個值與上次是否相同來判斷自身的位置和大小是否發生了改變。

    ②如果發生了改變,將會調用invalidate請求重繪。

    ③記錄本次的l、t、r、b,用于下次比對。

    ④如果大小發生了變化,onSizeChanged方法,該方法在大多數View中都是空實現,我們可以重寫該方法用于監聽View大小發生變化的事件,在可以滾動的視圖中重載了該方法,用于重新根據大小計算出需要滾動的值,以便顯示之前顯示的區域。

  • onLayout

   ①onLayout是ViewGroup用來決定子View擺放位置的,各種布局的差異都在該方法中得到了體現。

   ② onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因為父View位置和大小不變,并不能代表子View的位置和大小沒有發生改變。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // super.onLayout(changed, left, top, right, bottom);
     //重寫~ ~ ~(略)
}

3.3 View的繪制

draw方法繪制要遵循一定的順序:

1.畫背景

2,5.畫邊緣

3.畫自身: ondraw方法

4.畫子View: dispatchDraw方法

6.畫滾動條

首先我們來看一下draw繪制流程:

Paste_Image.png

以下是對上面三個方法的說明:

  • draw

    draw是由ViewRoot的performTraversals方法發起,它將調用DecorView的draw方法,并把成員變量canvas傳給給draw方法。而在后面draw遍歷中,傳遞的都是同一個canvas。所以android的繪制是同一個window中的所有View都繪制在同一個畫布上。等繪制完成,將會通知WMS把canvas上的內容繪制到屏幕上。自定義View時一般不重寫該方法。

  • onDraw

    ①View用來繪制自身的實現方法,如果我們想要自定義View,通常需要重載該方法。

    ②TextView中在該方法中繪制文字、光標和CompoundDrawable

    ③ImageView中相對簡單,只是繪制了圖片

    Canvas canvas = new Canvas(bitmap);

    之所以要傳入一個bitmap,是因為傳進來的bitmap與通過這個bitmap創建的Canvas畫布是緊緊聯系在一起的,這個過程稱之為裝載畫布。

    canvas.drawBitmap(bitmap, 0, 0, null);
    Canvas mCanvas = new Canvas(bitmap);

    通過mCanvas將繪制效果作用在了bitmap上,再通過invalidate()刷新的時候,我們就會發現通過onDraw()方法畫出來的bitmap已經發生了改變。

  • dispatchDraw

    先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁后的區域繪制。

    遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。

    根據滾動值和子View在父View中的坐標進行畫布原點坐標的移動,根據子在父View中的坐標計算出子View的視圖大小,然后對畫布進行剪裁。

    dispatchDraw的邏輯其實比較復雜,但是幸運的是對子View流程都采用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪制事件的派遣分發。

    重寫時,千萬千萬不要注釋了super.方法

好了,關于View的原理與使用就介紹到這里,關于View的更深層的理解有機會再進行探討。

來自:http://www.jianshu.com/p/a3014f8442b0

 

 本文由用戶 MelMora 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!