自定義控件

以下內(nèi)容整理自互聯(lián)網(wǎng),僅用于個(gè)人學(xué)習(xí)


如何自定義控件

  1. 自定義屬性的聲明和獲取
  • 分析需要的自定義屬性
  • 在res/values/attrs.xml定義聲明
  • 在layout文件中進(jìn)行使用
  • 在View的構(gòu)造方法中進(jìn)行獲取
  1. 測量onMeasure
  2. 布局onLayout(ViewGroup)
  3. 繪制onDraw
  4. onTouchEvent
  5. onInterceptTouchEvent(ViewGroup)
  6. 狀態(tài)的恢復(fù)與保存

自定義View大部分時(shí)候只需重寫兩個(gè)函數(shù):onMeasure()、onDraw()。

1. 自定義view

onMeasure負(fù)責(zé)對當(dāng)前View的尺寸進(jìn)行測量,onDraw負(fù)責(zé)把當(dāng)前這個(gè)View繪制出來。最后,至少寫2個(gè)構(gòu)造函數(shù)。

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

onMeasure()

我們自定義的View,首先得要測量寬高尺寸。

onMeasure函數(shù)原型:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  

參數(shù)中的widthMeasureSpec和heightMeasureSpec包含寬和高的信息,這些信息包括測量模式和尺寸大小。

一個(gè)int整數(shù)如何存放兩種信息?
首先要了解測量模式,測量模式有三種:UNSPECIFIED,EXACTLY,AT_MOST。二進(jìn)制只需要 2bit 就能表示,而int型整數(shù)有 32bit ,Google的做法是,將int數(shù)據(jù)的前面2個(gè)bit用于區(qū)分不同的布局模式,后面30個(gè)bit存放的是尺寸的數(shù)據(jù)。

Android通過內(nèi)置類MeasureSpec可以獲取int中的測量模式和尺寸大小。

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

測量模式是用來做什么的?

測量模式 表示意思
UNSPECIFIED 父容器沒有對當(dāng)前View有任何限制,當(dāng)前View可以任意取尺寸
EXACTLY 當(dāng)前的尺寸就是當(dāng)前View應(yīng)該取的尺寸
AT_MOST 當(dāng)前尺寸是當(dāng)前View能取的最大尺寸

測量尺寸與wrap_content、match_parent的對應(yīng)關(guān)系

  • warp_content 對應(yīng) AT_MOST:warp_content意味著將大小設(shè)置為足夠包裹內(nèi)部view內(nèi)容即可,就是我們想要將大小設(shè)置為包裹內(nèi)容,那么尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個(gè)尺寸就可以,具體尺寸就根據(jù)我們的需求去設(shè)定。
  • match_parent 對應(yīng) EXACTLY:match_parent就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,也就是這個(gè)測量模式的整數(shù)里面存放的尺寸。
  • 固定尺寸 對應(yīng) EXACTLY:用戶自己指定了尺寸大小,我們就不用再去干涉了,當(dāng)然是以指定的大小為主。

重寫onMeasure

自定義一個(gè)默認(rèn)寬高為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: {//如果沒有指定大小,就設(shè)置為默認(rèn)大小 
                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); 
}

接著就可以在xml中使用并設(shè)置布局

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

如果使用自定義的view,則會(huì)在左上角顯示一個(gè)正方形,如果不使用自定義view,則會(huì)在最上方顯示一個(gè)高為100dp,長度充滿父容器的長方形。

重寫onDraw

學(xué)會(huì)了設(shè)置尺寸,接下來就是把效果畫出來。在上面的onMeasure基礎(chǔ)上,重寫onDraw,實(shí)現(xiàn)一個(gè)顯示圓形的例子。

@Override 
    protected void onDraw(Canvas canvas) { 
        //調(diào)用父View的onDraw函數(shù),因?yàn)閂iew這個(gè)類幫我們實(shí)現(xiàn)了一些 
        // 基本的而繪制功能,比如繪制背景顏色、背景圖片等 
        super.onDraw(canvas); 
        int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我們已經(jīng)將寬高設(shè)置相等了 
        //圓心的橫坐標(biāo)為當(dāng)前的View的左邊起始位置+半徑 
        int centerX = getLeft() + r; 
        //圓心的縱坐標(biāo)為當(dāng)前的View的頂部起始位置+半徑 
        int centerY = getTop() + r; 
 
        Paint paint = new Paint(); 
        paint.setColor(Color.GREEN); 
        //開始繪制 
        canvas.drawCircle(centerX, centerY, r, paint); 
   }

顯示效果為在左上角顯示一個(gè)圓形。

自定義布局屬性

如果有些屬性我們希望由用戶指定,只有當(dāng)用戶不指定的時(shí)候才用我們硬編碼的值,比如上面的默認(rèn)尺寸。我們可以通過自定義自己的屬性,讓用戶使用我們定義的屬性。

首先我們需要在res/values/attrs.xml文件(如果沒有請自己新建)里面聲明一個(gè)我們自定義的屬性:

<resources> 
 
    <!--name為聲明的"屬性集合"名,可以隨便取,但是最好是設(shè)置為跟我們的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:ljr="http://schemas.android.com/apk/res-auto" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
 
    <com.ljr.example.MyView 
        android:layout_width="match_parent" 
        android:layout_height="100dp" 
        ljr:default_size="100dp" /> 
 
</LinearLayout>

我們需要在根標(biāo)簽(LinearLayout)里面設(shè)定命名空間,命名空間名稱可以隨便取,比如ljr,命名空間后面取得值是固定的:

"http://schemas.android.com/apk/res-auto"

最后就是在我們的自定義的View里面把我們自定義的屬性的值取出來,在構(gòu)造函數(shù)中,有個(gè)AttributeSet屬性,就是靠它幫我們把布局里面的屬性取出來:

private int defalutSize; 
//構(gòu)造函數(shù)
public MyView(Context context, AttributeSet attrs) { 
      super(context, attrs); 
      //第二個(gè)參數(shù)就是我們在attrs.xml文件中的<declare-styleable>標(biāo)簽 
        //即屬性集合的標(biāo)簽,在R文件中名稱為R.styleable+name 
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView); 
 
        //第一個(gè)參數(shù)為屬性集合里面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱 
        //第二個(gè)參數(shù)為,如果沒有設(shè)置這個(gè)屬性,則設(shè)置的默認(rèn)的值 
        defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100); 
 
        //最后記得將TypedArray對象回收 
        a.recycle(); 
 }

2. 自定義ViewGroup

  1. 先獲得子view的大小,這樣我們才能知道需要多大的ViewGroup去容納它們。
  1. 根據(jù)子view的大小以及需要實(shí)現(xiàn)的功能,決定ViewGroup的大小。
  2. 決定大小之后,就該將子view擺放在ViewGroup中。

接下來實(shí)現(xiàn)將子View按從上到下垂直順序一個(gè)挨著一個(gè)擺放的例子。

@Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
        //將所有的子View進(jìn)行測量,這會(huì)觸發(fā)每個(gè)子View的onMeasure函數(shù) 
        //注意要與measureChild區(qū)分,measureChild是對單個(gè)view進(jìn)行測量 
        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,當(dāng)前ViewGroup沒有存在的意義,不用占用空間 
            setMeasuredDimension(0, 0); 
        } else { 
            //如果寬高都是包裹內(nèi)容 
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { 
                //我們將高度設(shè)置為所有子View的高度相加,寬度設(shè)為子View中最大的寬度 
                int height = getTotleHeight(); 
                int width = getMaxChildWidth(); 
                setMeasuredDimension(width, height); 
 
            } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹內(nèi)容 
                //寬度設(shè)置為ViewGroup自己的測量寬度,高度設(shè)置為所有子View的高度總和 
                setMeasuredDimension(widthSize, getTotleHeight()); 
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內(nèi)容 
                //寬度設(shè)置為子View中寬度最大的值,高度設(shè)置為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測量好了,以及把自己的尺寸也設(shè)置好了,接下來擺放子View。

@Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
        int count = getChildCount(); 
        //記錄當(dāng)前的高度位置 
        int curHeight = t; 
        //將子View逐個(gè)擺放 
        for (int i = 0; i < count; i++) { 
            View child = getChildAt(i); 
            int height = child.getMeasuredHeight(); 
            int width = child.getMeasuredWidth(); 
            //擺放子View,參數(shù)分別是子View矩形區(qū)域的左、上、右、下邊 
            child.layout(l, curHeight, l + width, curHeight + height); 
            curHeight += height; 
        } 
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容