Android控件架構與自定義控件詳解(一)——Android控件架構與View的繪製原理

Android控件架構

在Android中,控件大致被分爲兩類,即ViewGroup控件與View控件。ViewGroup控件作爲父控件可以包含多個View控件,並管理其包含的View控件。通過ViewGroup,整個界面上的控件形成了一個樹形結構,即控件樹,上層控件負責下層子控件的測量與繪製,並傳遞交互事件。在每棵控件樹的頂部,都擁有一個ViewParent對象,這就是整棵樹的控制核心,所有的交互管理事件都由它來統一調度和分配,從而可以對整個視圖進行整體控制。如下圖展示了一個View視圖樹:

這裏寫圖片描述

Activity界面的架構圖如下所示:

這裏寫圖片描述

每個Activity都包含一個Window對象,在Android中Window對象通常由PhoneWindow來實現。PhoneWindow將一個DecorView設置爲整個應用窗口的根View。DecorView作爲窗口界面的頂層視圖,封裝了一些窗口操作的通用方法。可以說,DecorView將要顯示的具體內容呈現在了PhoneWindow上,這裏面的所有View的監聽事件,都通過WindowManagerService來進行接收,並通過Activity對象來回調相應的監聽。在顯示上,它將屏幕分爲兩部分,一個是TitleView,另一個是ContentView。ContentView是一個ID爲content的FrameLayout,activity_main.xml就是設置在這樣一個FrameLayout裏。

如果用戶通過設置requestWindowFeature(Window.FEATURE_NO_TITLE)來設置全屏顯示,那麼DecorView中將只有ContentView了,這就解釋了爲什麼調用requestWindowFeature()方法一定要在調用setContentView()方法之前才能生效的原因。

在代碼中,當程序在onCreate()方法中調用setContentView()方法後,ActivityManagerService會回調onResume()方法,此時系統纔會把整個DecorView添加到PhoneWindow中,並讓其顯示出來,從而完成界面的繪製。

View的測量

View的測量過程是在View的onMeasure()方法中進行的。Android系統給我們提供了一個設計短小精悍卻功能強大的類——MeasureSpec類,通過它來幫助我們測量View。MeasureSpec是一個32位的int值,其中高2位爲測量的模式,低30位爲測量的大小,在計算中使用位運算的原因是爲了提高並優化效率。

測量的模式可以爲以下三種:

  1. EXACTLY
    即精確值模式,當我們將控件的layout_width屬性或layout_height屬性指定爲具體數值時,或者指定爲match_parent屬性時,系統使用的是EXACTLY模式。
  2. AT_MOST
    即最大值模式,當控件的layout_width屬性或layout_height屬性指定爲wrap_content時,控件大小一般隨着控件的子控件或內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大尺寸即可。
  3. UNSPECIFIED
    這個屬性它不指定其大小測量的模式,View想多大就多大,通常情況下在繪製自定義View時才使用。

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

package com.example.huangfei.myapplication;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by huangfeihong on 2016/5/9.
 * View的測量與繪製
 */
public class ExampleView extends View {
    public ExampleView(Context context) {
        super(context);
    }

    public ExampleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ExampleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 在super.onMeasure(widthMeasureSpec, heightMeasureSpec);方法中,系統最終會調用setMeasuredDimension
     * (int measuredWidth, int measuredHeight)方法將測量後的寬高值設置進去,從而完成測量工作。所以在重寫
     * onMeasure()後,最終就是把測量後的寬高值作爲參數設置給setMeasuredDimension()方法。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.GRAY);
        int width = getWidth();
        int height = getHeight();
        Log.d("xys", "width : " + width + " height : " + height);

    }

    /**
     * 測量控件的高度
     *
     * @param heightMeasureSpec
     * @return
     */
    private int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * 測量控件的寬度
     *
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        int result = 0;
        //獲取View的測量模式
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        //獲取View的大小
        int specSize = MeasureSpec.getSize(widthMeasureSpec);

        //通過判斷測量的模式,給出不同的測量值
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            /**
             * 需要一個默認值,當控件的寬高屬性指定爲wrap_content時,如果不重寫onMeasure()方法,那麼系統
             * 就不知道該使用默認多大的尺寸。因此,他就會默認填充整個佈局,所以重寫onMeasure()方法的目的,
             * 就是爲了能夠給View一個wrap_content屬性下的默認大小。
             */
            result = 200;
            if(specMode == MeasureSpec.AT_MOST){
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
}

View的繪製

View的繪製過程是在View的onDraw(Canvas canvas)方法中進行的。要想在Android的界面中繪製相應的圖像,就必須在Canvas上進行繪製。Canvas就像一個畫板,使用Paint就可以在上面作畫了。

那如何在代碼中創建一個Canvas對象呢?

Canvas canvas = new Canvas(bitmap);

當創建一個Canvas對象時,爲什麼要傳進去一個bitmap對象呢?如果不傳入一個bitmap對象,IDE編譯雖然不會報錯,但是一般我們不會這樣做。這是因爲傳進去的bitmap與通過這個bitmap創建的Canvas畫布是緊緊聯繫在一起了,這個過程我們稱之爲裝載畫布。這個bitmap用來存儲所有繪製在Canvas上的像素信息。所以當你通過這種方式創建了Canva對象後,後面調用所有的Canvas.drawXXX方法都發生在這個bitmap上。

如果在View類的onDraw(Canvas canvas)方法中,通過下面這段代碼,我們可以瞭解到canvas與bitmap直接的關係。首先在onDraw方法中繪製兩個bitmap,代碼如下所示:

canvas.drawBitmap(bitmap1, 0, 0 null);
canvas.drawBitmap(bitmap2, 0, 0 null);

而對於bitmap2,我們將它裝載到另一個Canvas對象中,代碼如下所示:

Canvas bitmapCanvas = new Canvas(bitmap2);

在其他地方使用Canvas對象的繪圖方法在裝載bitmap2的Canvas對象上進行繪圖,代碼如下所示:

bitmapCanvas.drawXXX

通過bitmapCanvas將繪製效果作用在了bitmap2上,再刷新View的時候,就會發現通過onDraw()方法畫出來的bitmap2已經發生了改變,這就是因爲bitmap2承載了在bitmapCanvas上所進行的繪圖操作。雖然我們也使用了Canvas的繪製API,但其實並沒有將圖像直接繪製在onDraw()方法指定的那塊畫布上,而是通過改變bitmap,然後讓View重繪,從而顯示改變之後的bitmap。

ViewGroup的測量與繪製

ViewGroup會管理其子View,其中一個就是負責子View的顯示大小。當ViewGroup的大小爲wrap_content時,ViewGroup就需要對子View進行遍歷,以便獲得所有子View的大小,從而來決定自己的大小。而在其他模式下則會通過具體的指定值來設置自身的大小。

ViewGroup在測量時通過遍歷所有子View,從而調用子View的measure()方法來獲得每一個子View的測量結果。

當子View測量完畢後,就需要將子View放到合適的位置,這個過程就是View的Layout過程。ViewGroup在執行Layout過程時,同樣是使用遍歷來調用子View的layout()方法,並指定其具體顯示的位置,從而決定其佈局位置。

在自定義ViewGroup時,通常會去重寫onLayout()方法來控制子View顯示位置的邏輯。同樣,如果需要支持wrap_content屬性,那麼它必須重寫onMeasure()方法,這點與View是相同的。

ViewGroup通常情況下不需要繪製,因爲它本身就沒有需要繪製的東西,如果不是指定了ViewGroup的背景顏色,那麼ViewGroup的onDraw()方法都不會被調用。但是,ViewGroup會使用dispatchDraw()方法來繪製其子View,其過程同樣是通過遍歷所有子View,並調用子View的繪製方法來完成繪製工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章