【轉載】自定義View,有這一篇就夠了

爲了掃除學習中的忙點,儘量多的覆蓋Android知識的邊邊角角,決定對自定義View作一個稍微全面一點的使用方法總結,在內容上面並無什麼獨特的地方,其餘大神們博客上面基本上都有講這方面的內容,若是你對自定義View很熟了,那麼就不用往下看啦~。若是對自定義View不是很熟,或者說不少內容忘記了想複習一下,更或者說是歷來沒用過,歡迎和我一塊兒重溫這方面的知識,或許個人博文更復合你的胃口呢(__) 嘻嘻......java


1.自定義View

首先咱們要明白,爲何要自定義View?主要是Android系統內置的View沒法實現咱們的需求,咱們須要針對咱們的業務需求定製咱們想要的View。自定義View咱們大部分時候只須要重寫兩個函數:onMeasure()和onDraw()。onMeasure負責對當前View的尺寸進行測量,onDraw負責把當前這個View繪製出來。固然了,你還得寫至少2個構造函數:android

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

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

1.1.onMeasure

咱們自定義的View,首先得要測量寬高尺寸。爲何要測量寬高尺寸?我在剛學自定義View的時候很是沒法理解!由於我當時以爲,我在xml文件中已經指定好了寬高尺寸了,我自定義View中有必要再次獲取寬高並設置寬高嗎? 既然我自定義的View是繼承自View類,google團隊直接在View類中直接把xml設置的寬高獲取,而且設置進去不就行了嗎?那google爲啥還讓咱們作這樣的"重複工做"呢?客官別急,立刻給您上茶~web


在學習Android的時候,咱們就知道,在xml佈局文件中,咱們的layout_width和layout_height參數能夠不寫具體的尺寸,而是wrap_content或者是match_parent。其意思咱們都知道,就是將尺寸設置爲"包住內容"和"填充父佈局給咱們的全部空間"。這兩個設置並無指定真正的大小,可視咱們繪製到屏幕上的View必須是要有具體點寬高的,正是由於這個緣由,咱們必須本身去處理和設置尺寸。固然了,View類給了默認的處理,可是若是View類的默認處理不知足咱們的要求,咱們就得重寫onMeasure函數拉~。這裏舉個例子,好比咱們但願咱們的View是個正方形,若是在xml中指定寬高爲wrap_content,若是使用View類提供的measure處理方式,顯然沒法知足咱們的需求~。canvas


先看看onMeasure函數原型:ide

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

參數中的widthMeasureSpec和heightMeasureSpec是個什麼鬼?看起來很像width和height,沒錯,這兩個參數就是包含寬和高的信息。什麼?包含?難道還要其餘信息?是的!它還包含測量模式,也就是說,一個int整數,裏面放了測量模式和尺寸大小。那麼一個數怎麼放兩個信息呢?咱們知道,咱們在設置寬高時有3個選擇:wrap_content、match_parent以及指定固定尺寸,而測量模式也有3種:UNSPECIFIED、EXACTLY、AT_MOST,固然,他們並非一一對應關係哈,這三種模式後面我會詳細介紹,但測量模式並不是就是這3種狀況,而若是使用二進制,咱們只須要使用2個bit就能夠作到,由於2個bit取值範圍是[0,3]裏面能夠存放4個數足夠咱們用了。那麼Google是怎麼把一個int同時放測量模式和尺寸信息呢?咱們知道int型數據佔用32個bit,而Google實現的是,將int數據的前2個bit用於區分不一樣的佈局模式,後面30個bit存放的是尺寸的數據。函數


那咱們怎麼從int數據中提取測量模式和尺寸呢?放心,不用你每次都要寫一次移位<<和取且& 操做,Android內置類MeasureSpec幫咱們寫好拉~,咱們只需按照下面方法就能夠拿到拉:佈局

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

愛思考的你確定會問,既然咱們能經過widthMeasureSpec拿到寬度尺寸大小,那咱們還要測量模式幹嗎?測量模式會不會是多餘的?請注意:這裏的尺寸大小並非最終咱們的View的尺寸大小,而是父View提供的參考大小。咱們看看測量模式,測量模式是幹啥用個的呢?學習

測量模式 表示意思
UNSPECIFIED 父容器沒有對當前View有任何限制,當前View能夠任意取尺寸
EXACTLY 當前的尺寸就是當前View應該去的尺寸
AT_MOST 當前尺寸是當前View能取的最大尺寸

而上面的測量模式跟咱們的佈局時的wrap_content、match_parent以及寫成固定的尺寸有什麼對應的關係呢?測試

match_parent——>EXACTLY。怎麼理解呢?match_parent就是要利用父View給咱們提供的全部剩餘空間,而父View剩餘空間是肯定的,也就是這個測量模式的整數裏面存放的尺寸。
warp_content——>AT_MOST。怎麼理解呢?就是咱們想要將大小設置爲包裹咱們View內容,那麼尺寸大小就是父View給咱們做爲參考的尺寸,只要不超過這個尺寸就能夠拉,具體尺寸就根據咱們的需求去設定。
固定尺寸(如100dp)——>EXACTLY。用戶本身指定了尺寸大小,咱們就不用再去幹涉了,固然是以指定的大小爲主拉。google


1.2.動手重寫onMeasure函數

上面講了太多理論,咱們實際操做一下吧,感覺一下onMeasure的使用,假設咱們要實現這樣一個效果:將當前的View以正方形的形式顯示,即要寬高相等,而且默認的寬高值爲100像素。就能夠這樣編寫:

private int getMySize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//若是沒有指定大小,就設置爲默認大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//若是測量模式是最大取值爲size
                //咱們將大小取最大值,你也能夠取其餘值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//若是是固定的大小,那就不要去改變它
                mySize = size;
                break;
            }
        }
        return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(100, widthMeasureSpec);
        int height = getMySize(100, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }

        setMeasuredDimension(width, height);
}

咱們設置一下佈局

<com.hc.studyview.MyView android:layout_width="match_parent" android:layout_height="100dp" android:background="#ff0000" />

看看使用了咱們自定義onMeasure函數後的效果:

 

enter description here

自定義View.png

 


若是咱們不重寫onMeasure,效果則是以下:

 

enter description here

不重寫onMeasure.png

 


1.3.重寫onDraw

上面咱們學會了自定義尺寸大小,那麼尺寸咱們會設定了,接下來就是把咱們想要的效果畫出來吧~繪製咱們想要的效果很簡單,直接在畫板Canvas對象上繪製就好啦,過於簡單,咱們以一個簡單的例子去學習:假設咱們須要實現的是,咱們的View顯示一個圓形,咱們在上面已經實現了寬高尺寸相等的基礎上,繼續往下作:

@Override
    protected void onDraw(Canvas canvas) {
        //調用父View的onDraw函數,由於View這個類幫咱們實現了一些
        // 基本的而繪製功能,好比繪製背景顏色、背景圖片等
        super.onDraw(canvas);
        int r = getMeasuredWidth() / 2;//也能夠是getMeasuredHeight()/2,本例中咱們已經將寬高設置相等了
        //圓心的橫座標爲當前的View的左邊起始位置+半徑
        int centerX = getLeft() + r;
        //圓心的縱座標爲當前的View的頂部起始位置+半徑
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        //開始繪製
        canvas.drawCircle(centerX, centerY, r, paint);
    }

效果如圖:

 

enter description here

顯示效果.png

 


1.4.自定義佈局屬性

若是有些屬性咱們但願由用戶指定,只有當用戶不指定的時候才用咱們硬編碼的值,好比上面的默認尺寸,咱們想要由用戶本身在佈局文件裏面指定該怎麼作呢?那固然是經過咱們自定義屬性,讓用戶本身定義屬性啦~


首先咱們須要在res/values/stayles.xml文件(若是沒有請本身新建)裏面聲明一個咱們自定義的屬性:

<resources>

    <!--name爲聲明的"屬性集合"名,能夠隨便取,可是最好是設置爲跟咱們的View同樣的名稱-->
    <declare-styleable name="MyView">
        <!--聲明咱們的屬性,名稱爲default_size,取值類型爲尺寸類型(dp,px等)-->
        <attr name="default_size" format="dimension" />
    </declare-styleable>
</resources>

接下來就是在佈局文件用上咱們的自定義屬性啦~

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:hc="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">

    <com.hc.studyview.MyView android:layout_width="match_parent" android:layout_height="100dp" hc:default_size="100dp" />

</LinearLayout>

注意:須要在根標籤(LinearLayout)裏面設定命名空間,命名空間名稱能夠隨便取,好比hc,命名空間口面取的值是固定的:"htt://schemas.android.com/apk/res-auto"


最後就是在咱們的自定義的View裏面把咱們自定義的屬性的值取出來,在構造函數中,還記得AttributeSet屬性嗎?就是靠它幫咱們把佈局裏面的屬性取出來:

private int defalutSize;
  public MyView(Context context, AttributeSet attrs) {
      super(context, attrs);
      //第二個參數就是咱們在styles.xml文件中的<declare-styleable>標籤
        //即屬性集合的標籤,在R文件中名稱爲R.styleable+name
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);

        //第一個參數爲屬性集合裏面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
        //第二個參數爲,若是沒有設置這個屬性,則設置的默認的值
        defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);

        //最後記得將TypedArray對象回收
        a.recycle();
   }

最後把MyView的完整代碼附上:

package com.hc.studyview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/** * Package com.hc.studyview * Created by HuaChao on 2016/6/3. */
public class MyView extends View {

    private int defalutSize;

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

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //第二個參數就是咱們在styles.xml文件中的<declare-styleable>標籤
        //即屬性集合的標籤,在R文件中名稱爲R.styleable+name
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        //第一個參數爲屬性集合裏面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
        //第二個參數爲,若是沒有設置這個屬性,則設置的默認的值
        defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
        //最後記得將TypedArray對象回收
        a.recycle();
    }


    private int getMySize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//若是沒有指定大小,就設置爲默認大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//若是測量模式是最大取值爲size
                //咱們將大小取最大值,你也能夠取其餘值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//若是是固定的大小,那就不要去改變它
                mySize = size;
                break;
            }
        }
        return mySize;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(defalutSize, widthMeasureSpec);
        int height = getMySize(defalutSize, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //調用父View的onDraw函數,由於View這個類幫咱們實現了一些
        // 基本的而繪製功能,好比繪製背景顏色、背景圖片等
        super.onDraw(canvas);
        int r = getMeasuredWidth() / 2;//也能夠是getMeasuredHeight()/2,本例中咱們已經將寬高設置相等了
        //圓心的橫座標爲當前的View的左邊起始位置+半徑
        int centerX = getLeft() + r;
        //圓心的縱座標爲當前的View的頂部起始位置+半徑
        int centerY = getTop() + r;

        Paint paint = new Paint();
        paint.setColor(Color.GREEN);
        //開始繪製
        canvas.drawCircle(centerX, centerY, r, paint);
    }
}

2.自定義ViewGroup

自定義View的過程很簡單,就那幾步,可自定義ViewGroup可就沒那麼簡單啦~,由於它不只要管好本身的,還要兼顧它的子View。咱們都知道ViewGroup是個View容器,它裝納child View而且負責把childView放入指定的位置。咱們假設一下,若是是讓你負責設計ViewGroup,你會怎麼去設計呢?


1.首先,咱們得知道各個子View的大小吧,只有先知道子View的大小,咱們才知道當前的ViewGroup該設置爲多大去容納它們。
2.根據子View的大小,以及咱們的ViewGroup要實現的功能,決定出ViewGroup的大小。
3.ViewGroup和子View的大小算出來了以後,接下來就是去擺放了吧,具體怎麼去拜訪呢?這得根據你定製的需求去擺放了,好比,你想讓子View按照垂直順序一個挨着一個放,或則會按照前後順序一個疊一個去放,這是你本身決定的。
4.已經知道怎麼去拜訪還不行啊,決定了怎麼拜訪就是至關於把已有的空間"分割"成大大小小的空間,每一個控件對於一個子View,接下來就是把子View對號入座了,把它們放進它們該放的地方去。


如今就完成了ViewGroup的設計了,咱們來個具體的案例,將子View按從上到下垂直順序一個挨着一個擺放,即模仿實現LinearLayout的垂直佈局。


首先重寫onMeasure,實現測量子View大小以及設定ViewGroup的大小:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //將全部的子View進行測量,這會觸發每一個子View的onMeasure函數
        //注意要與measureChild區分,measureChild是對單個view進行測量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

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

        int childCount = getChildCount();

        if (childCount == 0) {//若是沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            //若是寬高都是包裹內容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //咱們將高度設置爲全部子View的高度相加,寬度設爲子View中最大的寬度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);

            } else if (heightMode == MeasureSpec.AT_MOST) {//若是隻有高度是包裹內容
                //寬度設置爲ViewGroup本身的測量寬度,高度設置爲全部子View的高度總和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//若是隻有寬度是包裹內容
                //寬度設置爲子View中寬度最大的值,高度設置爲ViewGroup本身的測量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);

            }
        }
    }
    /*** * 獲取子View中寬度最大的值 */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();

        }

        return maxWidth;
    }

    /*** * 將全部子View的高度相加 **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();

        }

        return height;
    }

代碼中的註釋我已經寫得很詳細,再也不對每一行代碼進行講解。上面的onMeasure將子View測量好了,以及把本身的尺寸也設置好了,接下來咱們去擺放子View吧~

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當前的高度位置
        int curHeight = t;
        //將子View逐個擺放
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //擺放子View,參數分別是子View矩形區域的左、上、右、下邊
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }

咱們測試一下,將咱們自定義的ViewGroup裏面放3個Button,將這3個Button的寬度設置不同,把咱們的ViewGroup的寬高都設置爲包裹內容wrap_content,爲了看的效果明顯,咱們給ViewGroup加個背景:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
    <com.hc.studyview.MyViewGroup android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ff9900">
        <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="btn" />
        <Button android:layout_width="200dp" android:layout_height="wrap_content" android:text="btn" />
        <Button android:layout_width="50dp" android:layout_height="wrap_content" android:text="btn" />
    </com.hc.studyview.MyViewGroup>
</LinearLayout>

看看最後的效果吧~

 

enter description here

自定義ViewGroup.png

 


是否是很激動咱們本身也能夠實現LinearLayout的效果啦~~
最後附上MyViewGroup的完整源碼:

package com.hc.studyview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/** * Package com.hc.studyview * Created by HuaChao on 2016/6/3. */
public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

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

    /*** * 獲取子View中寬度最大的值 */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();
        }
        return maxWidth;
    }

    /*** * 將全部子View的高度相加 **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();

        }
        return height;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //將全部的子View進行測量,這會觸發每一個子View的onMeasure函數
        //注意要與measureChild區分,measureChild是對單個view進行測量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

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

        int childCount = getChildCount();

        if (childCount == 0) {//若是沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            //若是寬高都是包裹內容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //咱們將高度設置爲全部子View的高度相加,寬度設爲子View中最大的寬度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);
            } else if (heightMode == MeasureSpec.AT_MOST) {//若是隻有高度是包裹內容
                //寬度設置爲ViewGroup本身的測量寬度,高度設置爲全部子View的高度總和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//若是隻有寬度是包裹內容
                //寬度設置爲子View中寬度最大的值,高度設置爲ViewGroup本身的測量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當前的高度位置
        int curHeight = t;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }
}

好啦~自定義View的學習到此結束,是否是發現自定義View如此簡單呢?

做者:huachao1001
連接:http://www.jianshu.com/p/c84693096e41

相關文章
相關標籤/搜索