一篇文章搞懂Android 自定義viewgroup的難點

本文的目的

目的在於教會你們到底如何自定義viewgroup,自定義佈局和自定義測量到底如何寫。不少網上隨便搜搜的概念和流程圖 這裏再也不過多描述了,建議你們看本文以前,先看看基本的自定義viewgroup流程,心中有個大概便可。本文注重於實踐android

viewgroup 的測量佈局流程基本梳理

稍微回顧下,基本的viewgroup繪製和佈局流程中的重點:算法

1.view 在onMeasure()方法中進行自我測量和保存,也就是說對於view(不是viewgroup噢)來講必定在onMeasure方法中 計算出本身的尺寸而且保存下來canvas

2.viewgroup實際上最終也是循環從上大小來調用子view的measure方法,注意子view的measure其實最終調用的是子view的onMeasure 方法。因此咱們理解這個過程爲: viewgroup循環遍歷調用全部子view的onmeasure方法,利用onmeasure方法計算出來的大小,來肯定這些子view最終能夠佔用的大小和所處的佈局的位置。bash

3.measure方法是一個final方法,能夠理解爲作測量工做準備工做的,既然是final方法因此咱們沒法重寫它,不須要過多 關注他,由於measure最終要調用onmeasure ,這個onmeasure咱們是能夠重寫的。要關注這個。layout和onlayout是同樣的 關係。app

4.父view調用子view的layout方法的時候會把以前measure階段肯定的位置和大小都傳遞給子view。ide

5.對於自定義view/viewgroup來講 咱們幾乎只須要關注下面三種需求:佈局

  • 對於已有的android自帶的view,咱們只須要重寫他的onMeasure方法便可。修改一下這個尺寸便可完成需求。
  • 對於android系統沒有的,屬於咱們自定義的view,比上面那個要複雜一點,要徹底重寫onMeasure方法。
  • 第三種最複雜,須要重寫onmeasure和onlayout2個方法,來完成一個複雜viewgroup的測量和佈局。
  1. onMeasure方法的特殊說明:

  1. 如何理解父view對子view的限制?字體

    onMeasure的兩個參數既然是父view對子view的限制,那麼這個限制的值究竟是哪來的呢?ui

    實際上,父view對子view的限制絕大多數就來自於咱們開發者所設置的layout開頭的這些屬性spa

    比方說咱們給一個imageview設置了他的layout_width和layout_height 這2個屬性,那這2個屬性其實就是咱們開發者 所指望的寬高屬性,可是要注意了, 設置的這2個屬性是給父view看的,實際上對於絕大多數的layout開頭的屬性這些屬性都是設置給父view看的

    爲何要給父view看?由於父view要知道這些屬性之後才知道要對子view的測量加以什麼限制?

    究竟是不限制(UNSPECIFIED)?仍是限制個最大值(AT_MOST),讓子view不超過這個值?仍是直接限制死,我讓你是多少就得是多少(EXACTLY)。

自定義一個BannerImageView 修改onMeasure方法

所謂bannerImageview,就是不少電商其實都會放廣告圖,這個廣告圖的寬高比都是可變的,咱們在平常開發過程當中 也會常常接觸到這種需求:imageview的寬高比 在高保真中都標註出來,可是考慮到不少手機的屏幕寬度或者高度都不肯定 因此咱們一般都要手動來計算出這個imageview高度或者寬度,而後動態改變width或者height的值。這種方法可用可是很麻煩 這裏給出一個自定義的imageview,經過設置一個ratio的屬性便可動態的設置iv的高度。非常方便

看下效果

最後看下代碼,重要的部分都寫在註釋裏了,再也不過多講了。

public class BannerImageView extends ImageView {

    //寬高比
    float ratio;

    public BannerImageView(Context context) {
        super(context);
    }

    public BannerImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
        ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
        typedArray.recycle();
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //人家本身的測量仍是要本身走一遍的,由於這個方法內部會調用setMeasuredDimension方法來保存測量結果了
        //只有保存了之後 咱們才能取得這個測量結果 不然你下面是取不到的
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //取測量結果
        int mWidth = getMeasuredWidth();

        int mHeight = (int) (mWidth * ratio);

        //保存了之後,父view就能夠拿到這個測量的寬高了。不保存是拿不到的噢。
        setMeasuredDimension(mWidth, mHeight);
    }
}
複製代碼

自定義view,徹底本身寫onMeasure方法

首先明確一個結論:

對於徹底自定義的view,徹底本身寫的onMeasure方法來講,你保存的寬高必需要符合父view的限制,不然會發生bug, 保存父view對子view的限制的方法也很簡單直接調用resolveSize方法便可。

因此對於徹底自定義的view onMeasure方法也不難寫了,

  1. 先算本身想要的寬高,好比你畫了個圓,那麼寬高就確定是半徑的兩倍大小, 要是圓下面還有字, 那麼高度確定除了半徑的兩倍還要有字體的大小。對吧。很簡單。這個純看你自定義view是啥樣的

  2. 算完本身想要的寬高之後 直接拿resolveSize 方法處理一下 便可。

  3. 最後setMeasuredDimension 保存。

範例:

public class LoadingView extends View {

    //圓形的半徑
    int radius;

    //圓形外部矩形rect的起點
    int left = 10, top = 30;


    Paint mPaint = new Paint();

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
        radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
    }

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

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


        int width = left + radius * 2;
        int height = top + radius * 2;

        //必定要用resolveSize方法來格式化一下你的view寬高噢,不然遇到某些layout的時候必定會出現奇怪的bug的。
        //由於不用這個 你就徹底沒有父view的感覺了 最後強調一遍
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF oval = new RectF(left, top,
                left + radius * 2, top + radius * 2);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(oval, mPaint);
        //先畫圓弧
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        canvas.drawArc(oval, -90, 360, false, mPaint);
    }
}

複製代碼

佈局文件:

<LinearLayout
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#000000"
        android:orientation="horizontal">

        <com.example.a16040657.customviewtest.LoadingView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/dly"
            app:radius="200"></com.example.a16040657.customviewtest.LoadingView>

        <com.example.a16040657.customviewtest.LoadingView
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/dly"
            app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
    </LinearLayout>

複製代碼

最後效果:

自定義一個viewgroup

這個其實也就是稍微複雜了一點,可是仍是有跡可循的,只是稍微須要一點額外的耐心。

自定義一個viewgroup 須要注意的點以下:

  1. 必定是先重寫onMeasure肯定子view的寬高和本身的寬高之後 才能夠繼續寫onlayout 對這些子view進行佈局噢~~
  2. viewgroup 的onMeasure其實就是遍歷本身的view 對本身的每個子view進行measure,絕大多數時候對子view的 measure均可以直接用 measureChild()這個方法來替代,簡化咱們的寫法,若是你的viewgroup很複雜的話 沒法就是本身寫一遍measureChild 而不是調用measureChild 罷了。
  3. 計算出viewgroup本身的尺寸而且保存,保存的方法仍是哪一個setMeasuredDimension 不要忘記了
  4. 逼不得已要重寫measureChild方法的時候,其實也不難無非就是對父view的測量和子view的測量 作一個取捨關係而已, 你看懂了基礎的measureChild方法,之後就確定會寫本身的複雜的measureChild方法了。

下面是一個極簡的例子,一個很簡單的flowlayout的實現,沒有對margin paddding作處理,也假設了每個tag的高度 是固定的,能夠說是極爲簡單了,可是麻雀雖小 五臟俱全,足夠大家好好理解自定義viewgroup的關鍵點了。

/**
 * 寫一個簡單的flowlayout 從左到右的簡單layout,若是寬度不夠放 就直接另起一行layout
 * 這個相似的開源控件有不少,有不少寫的出色的,我這裏只僅僅實現一個初級的flowlayout
 * 也是最簡單的,目的是爲了理解自定義viewgroup的關鍵核心點。
 * <p>
 * 比方說這裏並無對padding或者margin作特殊處理,大家本身寫viewgroup的時候 記得把這些屬性的處理都加上
 * 不然一旦有人用了這些屬性 發現沒有生效就比較難看了。。。。。。
 */
public class SimpleFlowLayout extends ViewGroup {
    public SimpleFlowLayout(Context context) {
        super(context);
    }

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

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

    /**
     * layout的算法 其實就是 不夠放剩下一行 那另外放一行 這個過程必定要本身寫一遍才能體會,
     * 我的有我的的寫法,說不定你的寫法比開源的項目還要好
     * 其實也沒什麼誇張的,沒法就是前面onMeasure結束之後 你能夠拿到全部子view和本身的 測量寬高 而後就算唄
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childTop = 0;
        int childLeft = 0;
        int childRight = 0;
        int childBottom = 0;

        //已使用 width
        int usedWidth = 0;


        //customlayout 本身可以使用的寬度
        int layoutWidth = getMeasuredWidth();
        Log.v("wuyue", "layoutWidth==" + layoutWidth);
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //取得這個子view要求的寬度和高度
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //若是寬度不夠了 就另外啓動一行
            if (layoutWidth - usedWidth < childWidth) {
                childLeft = 0;
                usedWidth = 0;
                childTop += childHeight;
                childRight = childWidth;
                childBottom = childTop + childHeight;
                childView.layout(0, childTop, childRight, childBottom);
                usedWidth = usedWidth + childWidth;
                childLeft = childWidth;
                continue;
            }
            childRight = childLeft + childWidth;
            childBottom = childTop + childHeight;
            childView.layout(childLeft, childTop, childRight, childBottom);
            childLeft = childLeft + childWidth;
            usedWidth = usedWidth + childWidth;

        }
    }

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

        //先取出SimpleFlowLayout的父view 對SimpleFlowLayout 的測量限制 這一步很重要噢。
        //你只有知道本身的寬高 才能限制你子view的寬高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);


        int usedWidth = 0;      //已使用的寬度
        int remaining = 0;      //剩餘可用寬度
        int totalHeight = 0;    //總高度
        int lineHeight = 0;     //當前行高

        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            LayoutParams lp = childView.getLayoutParams();

            //先測量子view
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            //而後計算一下寬度裏面 還有多少是可用的 也就是剩餘可用寬度
            remaining = widthSize - usedWidth;

            //若是一行不夠放了,也就是說這個子view測量的寬度 大於 這一行 剩下的寬度的時候 咱們就要另外啓一行了
            if (childView.getMeasuredWidth() > remaining) {
                //另外啓動一行的時候,使用過的寬度 固然要設置爲0
                usedWidth = 0;
                //另外啓動一行了 咱們的總高度也要加一下,否則高度就不對了
                totalHeight = totalHeight + lineHeight;
            }

            //已使用 width 進行 累加
            usedWidth = usedWidth + childView.getMeasuredWidth();
            //當前 view 的高度
            lineHeight = childView.getMeasuredHeight();
        }

        //若是SimpleFlowLayout 的高度 爲wrap cotent的時候 才用咱們疊加的高度,不然,咱們固然用父view對若是SimpleFlowLayout 限制的高度
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = totalHeight;
        }
        setMeasuredDimension(widthSize, heightSize);
    }
}

複製代碼

最後看下效果

相關文章
相關標籤/搜索