關於自定義View的理解

概述

關於自定義View,看到一篇文章,思路清晰,幫到了我,想說記錄下來,總結下

        Android中View框架的工作機制中,主要有三個過程:

                1、View樹的測量(measure)Android View框架的measure機制

                2、View樹的佈局(layout) Android View框架的layout機制

                3、View樹的繪製(draw)Android View框架的draw機制

        View框架的工作流程爲:測量每個View大小(measure)-->把每個View放置到相應的位置(layout)-->繪製每個View(draw)。

 

 開發人員在繪製UI的時候,基本都是通過XML佈局文件的方式來配置UI,而每個View必須要設置的兩個羣屬性就是layout_width和layout_height,這兩個屬性代表着當前View的尺寸。

 所以這兩個屬性的值是必須要指定的,這兩個屬性的取值只能爲三種類型:

                 1、固定的大小,比如100dp。

                 2、剛好包裹其中的內容,wrap_content。

                 3、想要和父佈局一樣大,match_parent / fill_parent。

        由於Android希望提供一個更優雅的GUI框架,所以提供了自適應的尺寸,也就是 wrap_content 和 match_parent 。

        試想一下,那如果這些屬性只允許設置固定的大小,那麼每個View的尺寸在繪製的時候就已經確定了,所以可能都不需要measure過程。但是由於需要滿足自適應尺寸的機制,所以需要一個measure過程。

measure過程都幹了點什麼事?

        由於上面提到的自適應尺寸的機制,所以在用自適應尺寸來定義View大小的時候,View的真實尺寸還不能確定。但是View尺寸最終需要映射到屏幕上的像素大小,所以measure過程就是幹這件事,把各種尺寸值,經過計算,得到具體的像素值。measure過程會遍歷整棵View樹,然後依次測量每個View真實的尺寸。具體是每個ViewGroup會向它內部的每個子View發送measure命令,然後由具體子View的onMeasure()來測量自己的尺寸。最後測量的結果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的數據單位是像素。

對於自適應的尺寸機制,如何合理的測量一顆View樹?

        系統在遍歷完佈局文件後,針對佈局文件,在內存中生成對應的View樹結構,這個時候,整棵View樹種的所有View對象,都還沒有具體的尺寸,因爲measure過程最終是要確定每個View打的準確尺寸,也就是準確的像素值。但是剛開始的時候,View中layout_width和layout_height兩個屬性的值,都只是自適應的尺寸,也就是match_parent和wrap_content,這兩個值在系統中爲負數,所以系統不會把它們當成具體的尺寸值。所以當一個View需要把它內部的match_parent或者wrap_content轉換成具體的像素值的時候,他需要知道兩個信息。

        1、針對於match_parent,父佈局當前具體像素值是多少,因爲match_parent就是子View想要和父佈局一樣大。

        2、針對wrap_content,子View需要根據當前自己內部的content,算出一個合理的能包裹所有內容的最小值。但是如果這個最小值比當前父佈局還大,那不行,父佈局會告訴你,我只有這麼大,你也不應該超過這個尺寸。

 

       也就是說,在measure過程中,ViewGroup會根據自己當前的狀況,結合子View的尺寸數據,進行一個綜合評定,然後把相關信息告訴子View,然後子View在onMeasure自己的時候,一邊需要考慮到自己的content大小,一邊還要考慮的父佈局的限制信息,然後綜合評定,測量出一個最優的結果。

那麼ViewGroup是如何向子View傳遞限制信息的?

        談到傳遞限制信息,那就是MeasureSpec類了,該類貫穿於整個measure過程,用來傳遞父佈局對子View尺寸測量的約束信息。簡單來說,該類就保存兩類數據。

                1、子View當前所在父佈局的具體尺寸。

                2、父佈局對子View的限制類型。

        那麼限制類型又分爲三種類型:

                1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以給你多大,你放心大膽的measure吧,不用管其他的。也不用管我傳遞給你的尺寸值。(其實Android高版本中推薦,只要是這個模式,尺寸設置爲0)

                2、EXACTLY,精確的。意思就是,根據我當前的狀況,結合你指定的尺寸參數來考慮,你就應該是這個尺寸,具體大小在MeasureSpec的尺寸屬性中,自己去查看吧,你也不要管你的content有多大了,就用這個尺寸吧。

                3、AT_MOST,最多的。意思就是,根據我當前的情況,結合你指定的尺寸參數來考慮,在不超過我給你限定的尺寸的前提下,你測量一個恰好能包裹你內容的尺寸就可以了。

當自定義View的時候,也需要處理measure過程,主要有兩種情況。

        1、繼承自View的子類。

                需要覆寫onMeasure來正確測量自己。最後都需要調用setMeasuredDimension來保存測量結果

                一般來說,自定義View的measure過程僞代碼爲:

                

        2、繼承自ViewGroup的子類。

                不但需要覆寫onMeasure來正確測量自己,可能還要覆寫一系列measureChild方法,來正確的測量子view,比如ScrollView。或者乾脆放棄父類實現的measureChild規則,自己重新實現一套測量子view的規則,比如RelativeLayout。最後都需要調用setMeasuredDimension來保存測量結果。

                一般來說,自定義ViewGroup的measure過程的僞代碼爲:

                

layout過程都幹了點什麼事?

        由於View是以樹結構進行存儲,所以典型的數據操作就是遞歸操作,所以,View框架中,採用了內部自治的layout過程。

        每個葉子節點根據父節點傳遞過來的位置信息,設置自己的位置數據,每個非葉子節點,除了負責根據父節點傳遞過來的位置信息,設置自己的位置數據外(如果有父節點的話),還需要根據自己內部的layout規則(比如垂直排布等),計算出每一個子節點的位置信息,然後向子節點傳遞layout過程。

        對於ViewGroup,除了根據自己的parent傳遞的位置信息,來設置自己的位置之外,還需要根據自己的layout規則,爲每一個子View計算出準確的位置(相對於子View的父佈局的位置)。

        對於View,根據自己的parent傳遞的位置信息,來設置自己的位置。

View對象的位置信息,在內部是以4個成員變量的保存的,分別是mLeft、mRight、mTop、mBottom。他們的含義如圖所示。

View:

1、layout

/** 

分配一個位置信息到一個View上面,每個parent會調用children的layout方法來設置children的位置。最好不要覆寫該方法,有children的viewGroup,應該覆寫onLayout方法

*/

public void layout(int l, int t, int r, int b) ;

 

ViewGroup:

 

ViewGroup中,只需要覆寫onLayout方法,來計算出每一個子View的位置,並且把layout流程傳遞給子View。

源代碼:

ViewGroup沒有實現,具體可以參考LinearLayout和RelativeLayout的onLayout方法。雖然各個具體實現都很複雜,但是基本流程是一樣的,可以參考下面的僞代碼

結論

        一般來說,自定義View,如果該View不包含子View,類似於TextView這種的,是不需要覆寫onLayout方法的。而含有子View的,比如LinearLayout這種,就需要根據自己的佈局規則,來計算每一個子View的位置。

draw過程都幹了點什麼事?

        View框架中,draw過程主要是繪製View的外觀。ViewGroup除了負責繪製自己之外,還需要負責繪製所有的子View。而不含子View的View對象,就負責繪製自己就可以了。

        draw過程的主要流程如下:

        1、繪製 backgroud(drawBackground)     
        2、如果需要的話,保存canvas的layer,來準備fading(不是必要的步驟)
        3、繪製view的content(onDraw方法)
        4、繪製children(dispatchDraw方法)
        5、如果需要的話,繪製fading edges,然後還原layer(不是必要的步驟)
        6、繪製裝飾器、比如scrollBar(onDrawForeground)

View:

1、draw

/** 

繪製一個View以及他的子View。最好不要覆寫該方法,應該覆寫onDraw方法來繪製自己。

*/

public void draw(Canvas canvas);

ViewGroup:

1、dispatchDraw

/** 繪製子View,View類是空實現,ViewGroup類中有實現 */

protected void dispatchDraw(Canvas canvas);

 

代碼示例:

複製代碼

public class VerticalOffsetLayout extends ViewGroup {

    private static final int OFFSET = 100;
    private Paint mPaint;

    public VerticalOffsetLayout(Context context) {
        super(context);
        init(context, null, 0);
    }

    public VerticalOffsetLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

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

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        mPaint = new Paint(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setAlpha(125);
    }

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            ViewGroup.LayoutParams lp = child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
            child.measure(childWidthSpec, childHeightSpec);
        }

        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    int widthAddOffset = i * OFFSET + child.getMeasuredWidth();
                    width = Math.max(width, widthAddOffset);
                }
                break;
            default:
                break;

        }

        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    height = height + child.getMeasuredHeight();
                }
                break;
            default:
                break;

        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;
        int right = 0;
        int top = 0;
        int bottom = 0;

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            left = i * OFFSET;
            right = left + child.getMeasuredWidth();
            bottom = top + child.getMeasuredHeight();

            child.layout(left, top, right, bottom);

            top += child.getMeasuredHeight();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int x = getWidth()/2;
        int y = getHeight()/2;
        canvas.drawCircle(x, y, Math.min(x, y), mPaint);
    }
}

複製代碼

 

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