Android最易懂的自定義View講解

前言: 最近開發(fā)的時候, 頻繁的需要使用到自定義控件控乾。自定義控件是成為高級工程師必不可少的條件之一恕刘,所以今天決定認(rèn)真總結(jié)一下。其實自定義控件也沒有想象中的那么復(fù)雜,無非只要掌握其中的幾個關(guān)鍵方法就能滿足絕大部分需求刹衫。但是若要真的要深入進(jìn)去,都能寫一本書了搞挣,這里就不做那么深入了带迟。能滿足日常的需求即可, 想深入了解的可自行查閱其他資料進(jìn)行學(xué)習(xí)。

在學(xué)習(xí)本篇自定義View之前,讀者有必要先學(xué)習(xí)一下View的繪制流程,這樣才能更好的理解文字的內(nèi)容囱桨。必知必會 | 面試官裝逼失敗之View的繪制流程

首先我們要明白仓犬,為什么要自定義View?主要是Android系統(tǒng)內(nèi)置的View無法實現(xiàn)我們的需求舍肠,我們需要針對我們的業(yè)務(wù)需求定制我們想要的View搀继。簡單來說自定義控件無非就兩種窘面,自定義View和自定義ViewGroup:

  • 自定義View
    可以理解為自定義View的父類,是一個單獨(dú)的控件叽躯,里面無法存放子View财边。例如TextView,ImageView等都是繼承View的点骑,View里面最關(guān)鍵的方法是onMeasureonDraw酣难。

  • 自定義ViewGroup
    ViewGroup是View的子類,相當(dāng)于一個容器黑滴,里面可以放子View憨募。例如LinearLayout,RelativeLayout等都是繼承ViewGroup的跷跪。ViewGroup里面最關(guān)鍵的方法是onMeasure和onDraw和onLayout馋嗜。其中onLayout是ViewGroup中特有的方法,用來實現(xiàn)子View的擺放吵瞻。

1. 自定義View

自定義View的話我們大部分時候只需重寫兩個函數(shù):onMeasure()onDraw()甘磨。onMeasure負(fù)責(zé)對當(dāng)前View的尺寸進(jìn)行測量橡羞,onDraw負(fù)責(zé)把當(dāng)前這個View繪制出來。當(dāng)然了济舆,你還得寫至少寫2個構(gòu)造函數(shù):

    // 一個參數(shù)的構(gòu)造方法,在代碼中創(chuàng)建該控件時卿泽,調(diào)用該構(gòu)造方法
    public MyView(Context context) {
        super(context);
    }
  
    // 在xml 中引用該控件時,調(diào)用該方法滋觉。attrs是定義在xml布局中的屬性集合
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs); 
    }

1.1 重寫onMeasure

我們自定義View签夭,首先得要測量寬高尺寸。為什么要測量寬高尺寸椎侠?有的人要問了,我不是在xml文件中已經(jīng)指定好了寬高尺寸了嗎, 我自定義的View有必要再一次獲取寬高去設(shè)置寬高嗎第租?既然我自定義的View是繼承自View類,google團(tuán)隊直接在View類中直接把xml設(shè)置的寬高獲取我纪,并且設(shè)置進(jìn)去不就好了嗎慎宾?為什么要讓我們自己來做,真可恨!別著急,既然google讓我們做這樣的“重復(fù)工作”,自然有他的道理。

在學(xué)習(xí)Android的時候浅悉,我們就知道趟据,在xml布局文件中,我們的layout_widthlayout_height參數(shù)可以不用寫具體的尺寸术健,而是wrap_content或者是match_parent汹碱。其意思我們都知道,就是將尺寸設(shè)置為“包住內(nèi)容”和“填充父布局給我們的所有空間”荞估。這兩個設(shè)置并沒有指定真正的大小咳促,可是我們繪制到屏幕上的View必須是要有具體的寬高的稚新,這回知道了吧?并不是所有情況下我們都會給某個View特定的尺寸的等缀。正是因為這個原因枷莉,我們必須自己去處理和設(shè)置尺寸。當(dāng)然了尺迂,View類給了默認(rèn)的處理笤妙,但是如果View類的默認(rèn)處理不滿足我們的要求,我們就得重寫onMeasure函數(shù)啦噪裕。這里舉個例子蹲盘,比如我們希望我們的View是個正方形,如果在xml中指定寬高為wrap_content膳音,如果使用View類提供的measure處理方式召衔,顯然無法滿足我們的需求。

關(guān)于onMeasure函數(shù)的源碼解析,我已經(jīng)在上一篇文章中做了詳細(xì)的解釋了,不了解的請移步必知必會 | 面試官裝逼失敗之View的繪制流程祭陷。了解了onMeaSure方法的實現(xiàn)原理,在自定義View時我們需要對其進(jìn)行重寫苍凛。

講了太多理論,我們來實際操作一下吧兵志,感受一下onMeasure的使用醇蝴,現(xiàn)在假設(shè)我們要實現(xiàn)這樣一個效果:將當(dāng)前的View以正方形的形式顯示,即要寬高相等想罕,并且默認(rèn)的寬高值為100像素悠栓。代碼如下:

// defaultSize 默認(rèn)尺寸,這里為100像素
// measureSpec 測量規(guī)格
private int getSize(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 = getSize(100, widthMeasureSpec);
        int height = getSize(100, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }
      
        // 設(shè)置測量之后的參數(shù)
        setMeasuredDimension(width, height);
}

布局中使用它:

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

使用了我們自己定義的onMeasure函數(shù)后的效果:

正方形顯示View

而如果我們不重寫onMeasure惭适,效果則是如下:

未重寫onMeasure的效果

顯然重寫之后按照了我們意愿去顯示的,實現(xiàn)了我們的需求。

1.2 重寫onDraw

上面我們學(xué)會了自定義尺寸大小楼镐,尺寸我們會設(shè)定了癞志,接下來就是把我們想要的效果畫出來吧~繪制我們想要的效果很簡單,直接在畫板Canvas對象上繪制就好啦鸠蚪,邏輯過于簡單今阳,我們以一個簡單的例子去學(xué)習(xí):假設(shè)我們需要實現(xiàn)的是,我們的View顯示一個圓形茅信,我們在上面已經(jīng)實現(xiàn)了寬高尺寸相等的基礎(chǔ)上盾舌,繼續(xù)往下做:

@Override
    protected void onDraw(Canvas canvas) {
        //調(diào)用父View的onDraw函數(shù),因為View這個類幫我們實現(xiàn)了一些
        // 基本的而繪制功能蘸鲸,比如繪制背景顏色妖谴、背景圖片等
        super.onDraw(canvas);
       //也可以是getMeasuredHeight()/2。
       //本例中我們已經(jīng)將寬高設(shè)置相等了。
        int r = getMeasuredWidth() / 2;
        //圓心的橫坐標(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);
    }

效果圖如下:

圓形顯示

1.3 自定義屬性

有時候有些屬性我們希望由用戶指定膝舅,只有當(dāng)用戶不指定的時候才用我們硬編碼的值嗡载,比如上面的默認(rèn)尺寸,我們想要由用戶自己在布局文件里面指定該怎么做呢仍稀?那當(dāng)然是通我們自定屬性洼滚,讓用戶用我們定義的屬性啦~

  • 首先我們需要在res/values/attrs.xml文件(如果沒有請自己新建)里面聲明一個我們自定義的屬性:
<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:jieyao="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

</LinearLayout>

注意:需要在根標(biāo)簽(LinearLayout)里面設(shè)定命名空間,命名空間名稱可以隨便取享幽,比如 jieyao铲掐,命名空間后面取的值是固定的:"http://schemas.android.com/apk/res-auto"

  • 最后就是在我們的自定義的View的代碼里面把我們自定義的屬性的值取出來,在構(gòu)造函數(shù)中值桩,還記得有個AttributeSet屬性嗎摆霉?就是靠它幫我們把布局里面的屬性取出來:
  private int defalutSize;

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

最后咳秉,把MyView的完整代碼附上:

package com.jieyao.test;

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;

public class MyView extends View {

    private int defalutSize;

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

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

    private int getSize(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 = getSize(defalutSize, widthMeasureSpec);
        int height = getSize(defalutSize, heightMeasureSpec);

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

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //調(diào)用父View的onDraw函數(shù),因為View這個類幫我們實現(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);
    }
}

2. 自定義ViewGroup

自定義View的過程很簡單,就那幾步碍彭,可自定義ViewGroup可就沒那么簡單啦~晤硕,因為它不僅要管好自己的,還要兼顧它的子View庇忌。我們都知道ViewGroup是個View容器舞箍,它裝納child View并且負(fù)責(zé)把child View放入指定的位置。我們結(jié)合一個具體案例來一步步實現(xiàn)自定義ViewGroup的過程:將子View按從上到下以垂直順序一個挨著一個擺放皆疹,即模仿實現(xiàn)LinearLayout的垂直布局疏橄。

2.1 重寫onMeasure

重寫onMeasure,實現(xiàn)測量子View大小以及設(shè)定ViewGroup的大小,代碼如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //將所有的子View進(jìn)行測量,這會觸發(fā)每個子View的onMeasure函數(shù)
        //注意要與measureChild區(qū)分捎迫,measureChild是對單個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();// 子View個數(shù)

        if (childCount == 0) {//如果沒有子View,當(dāng)前ViewGroup沒有存在的意義晃酒,不用占用空間
            setMeasuredDimension(0, 0);
        } else { // 有子View,對MeasureSpec為AT_MOST時進(jìn)行特殊處理
            //如果寬高都是包裹內(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;
    }

代碼中的注釋我已經(jīng)寫得很詳細(xì),不再對每一行代碼進(jìn)行講解,相信很容易理解吧彰导。

2.2 重寫onLayout

上面的onMeasure將子View測量好了蛔翅,以及把自己的尺寸也設(shè)置好了,接下來我們?nèi)[放子View吧~只需要重寫onLayout方法即可,代碼如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當(dāng)前的高度位置
        int curHeight = t;
        //將子View逐個擺放
        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;
        }
    }

自定義ViewGroup已經(jīng)完成, 我們來測試一下效果盖腿,將我們自定義的ViewGroup里面放3個Button ,將這3個Button的寬度設(shè)置不一樣,把我們的ViewGroup的寬高都設(shè)置為包裹內(nèi)容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.jieyao.test.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>

看看最后的效果吧~是不是很激動我們自己也可以實現(xiàn)LinearLayout的效果啦

自定義ViewGroup

最后附上MyViewGroup的完整源碼:

package com.jieyao.test;

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

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進(jìn)行測量,這會觸發(fā)每個子View的onMeasure函數(shù)
        //注意要與measureChild區(qū)分膏燃,measureChild是對單個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();//子View個數(shù)

        if (childCount == 0) {//如果沒有子View,當(dāng)前ViewGroup沒有存在的意義茂卦,不用占用空間
            setMeasuredDimension(0, 0);
        } else { // 有子View,對MeasureSpec為AT_MOST時進(jìn)行特殊處理
            //如果寬高都是包裹內(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);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當(dāng)前的高度位置
        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;
        }
    }   
}

3. 實戰(zhàn)項目

本人雖然是一個Android開發(fā)者,卻對蘋果手機(jī)有獨(dú)特的愛好。經(jīng)常使用蘋果手機(jī)的朋友可能知道, 蘋果的設(shè)置界面有很多滑動的開關(guān)按鈕, 可以左滑右滑實現(xiàn)某個功能的開啟和關(guān)閉, 看上去也是很酷炫有沒有~今天, 就來實現(xiàn)一下這個功能伶贰。

首先,來看一下我實現(xiàn)的滑動開關(guān)效果圖:

滑動開關(guān)效果圖

這個滑動開關(guān)是一個純粹的自定義控件蛛砰,上面的按鈕會隨著我們的左右滑動而滑動,并且在狀態(tài)改變時通知用戶黍衙,這也是應(yīng)用中設(shè)置某些狀態(tài)信息時最常見的控件泥畅。

在實際開發(fā)中,完整的實現(xiàn)一個自定義控件,并讓該控件具備某個功能,一般來說要有以下幾個步驟:

  1. 創(chuàng)建一個view繼承自View或者ViewGroup
  2. 定義自定義view的屬性
  3. 在代碼中獲取屬性,并給自定義屬性相應(yīng)的設(shè)置事件
  4. 根據(jù)實際重寫自定義view的onMeasure,onLayout,onDraw方法
  5. 與用戶進(jìn)行交互的邏輯實現(xiàn)
  6. 自定義view的代碼優(yōu)化
  • 1.創(chuàng)建view
public class ToggleButton extends View { // 滑動開關(guān)類
}
    1. 自定義view屬性
  <?xml version="1.0" encoding="utf-8"?>
  <resources>
     <declare-styleable name="ToggleButton">
          <!-- 滑動開關(guān)背景圖片屬性-->
          <attr name="SwitchBtnBackgroud" format="reference" />
           <!-- 滑動塊背景圖片屬性-->
          <attr name="SlidBtnBackgroud" format="reference" />
          <!-- 滑動開關(guān)的狀態(tài)-->
         <attr name="CurrentState" format="boolean" />
     </declare-styleable>
  </resources>
    1. 在代碼中獲取屬性并給自定義屬性相應(yīng)的設(shè)置事件
        private Bitmap switchBitmap;//滑動開關(guān)的背景圖片
        private Bitmap slidBitmap;//滑動塊的背景圖片
        private boolean currentState;// 滑動開關(guān)的狀態(tài)

        //在xml 中引用該控件時,調(diào)用該方法
        public ToggleButton(Context context, AttributeSet attrs) {
                super(context, attrs);
                String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
                currentState = attrs.getAttributeBooleanValue(namespace, "CurrentState",
                int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace, "SwitchBtnBackgroud", -1);
                int slidBtnBackgroudId =attrs.getAttributeResourceValue(namespace, "SlidBtnBackgroud", -1);
                setSwitchBtnBackgroudResource(switchBtnBackgroudId);
                setSlidBtnBackgroudResource(slidBtnBackgroudId);
        }
  
        //在代碼中創(chuàng)建該控件時琅翻,調(diào)用該構(gòu)造方法
        public ToggleButton(Context context) {
                super(context);
        }

        // 為了可以高度自定義和增強(qiáng)可擴(kuò)展性,我們給滑動按鈕背景和滑動塊背景都提供了設(shè)置方法
        //設(shè)置滑動開關(guān)的背景圖片
        public void setSwitchBtnBackgroudResource(int switchBackground) {
                switchBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
        }

        // 設(shè)置滑動塊的背景圖片
       public void setSlidBtnBackgroudResource(int slideButtonBackground) {
                slidBitmap = BitmapFactory.decodeResource(getResources(), slideButtonBackground);
       }

      //設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
      public void setCurrentState(boolean b) {
                currentState = b;
      }
    1. 重寫onMeasure方法和onDraw方法
       // 1位仁、測量滑動開關(guān)的寬高
       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // TODO Auto-generated method stub
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
       }
  
       // 2、繪制方椎,畫出我們的滑動開關(guān)
       //canvas:畫布聂抢,將圖形繪制在canvas,才能顯示到屏幕上
       @Override
       protected void onDraw(Canvas canvas) {
           //繪制滑動開關(guān)的背景圖片
           canvas.drawBitmap(switchBitmap, 0, 0, null);
           //繪制滑動塊的背景圖片,要根據(jù)手勢實時繪制
           if(isTouching){//手指觸摸的時候辩尊,根據(jù)currentx 的值來繪制滑動塊
               //根據(jù)手指的X 值涛浙,來繪制滑動塊圖片
               int left = currentX - slidBitmap.getWidth()/2;
               if(left < 0){//設(shè)置左邊界
                      left = 0;//左邊零點(diǎn)
               }else if(left > (switchBitmap.getWidth() - slidBitmap.getWidth())){//設(shè)置右邊界
                      left = switchBitmap.getWidth() - slidBitmap.getWidth();//中心點(diǎn)
               }
               canvas.drawBitmap(slidBitmap, left, 0, null);//根據(jù)左邊界位置繪制滑動塊背景
           }else{ // 手指已經(jīng)離開控件的時候,根據(jù)狀態(tài)來繪制滑動塊
               // 根據(jù)狀態(tài)值,來繪制滑動塊
               if(currentState){ //當(dāng)前為true轿亮,開關(guān)打開疮薇,滑動塊顯示在最右邊
                      canvas.drawBitmap(slidBitmap,switchBitmap.getWidth() - slidBitmap.getWidth(),0, null);
               }else{//當(dāng)前為false,開關(guān)關(guān)閉我注,滑動塊顯示在最左邊
                       canvas.drawBitmap(slidBitmap, 0, 0, null);
               }
           }
       }

5.與用戶進(jìn)行交互的邏輯實現(xiàn)

    // 當(dāng)控件被觸摸后按咒,會調(diào)用該方法(通過改動isTouching 和currentState的值動態(tài)繪制滑動塊)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:// 手指按下
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:// 手指滑動
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_UP:// 手指抬起
            isTouching = false;
            currentX = (int) event.getX();
            int center = switchBitmap.getWidth() / 2;
            // 當(dāng)滑動塊中心點(diǎn)大于滑動開關(guān)背景圖片的中心線時,顯示到右邊但骨,狀態(tài)改為true
            boolean state = currentState;
            // 獲取滑動塊的狀態(tài)
            currentState = currentX > center;
            // 設(shè)置滑動塊的狀態(tài)
            // state != currentState說明開關(guān)狀態(tài)發(fā)生了改變
            if (mToggleBtnStateChangeListener != null && state != currentState) {
                 mToggleBtnStateChangeListener.onToggleBtnStateChange(currentState);
            }
            break;
        default:
            break;
        }
        // 強(qiáng)制讓控件重新繪制励七,
        invalidate(); //此方法可以強(qiáng)制重新調(diào)用onDraw方法
        // 自己處理觸摸事件
        return true;
    }

    // 給滑動塊設(shè)置狀態(tài)改變監(jiān)聽(方便在activity代碼中做相應(yīng)邏輯處理)
    // 參數(shù)為ToggleBtnStateChangeListener 接口,傳入之后會回調(diào)onToggleBtnStateChange方法。
    // 根據(jù)回調(diào)方法中的currentState做對應(yīng)邏輯判斷和邏輯處理
    public void setToggleBtnStateChangeListener(
            ToggleBtnStateChangeListener listener) {
        this.mToggleBtnStateChangeListener = listener;
    }

    // 滑動開關(guān)狀態(tài)改變的回調(diào)接口
    public interface ToggleBtnStateChangeListener {
        void onToggleBtnStateChange(boolean currentState);
    }
  1. 自定義view的代碼優(yōu)化:

在上面的步驟結(jié)束之后奔缠,其實一個完善的自定義控件已經(jīng)出來了掠抬。接下來你要做的只是確保自定義控件運(yùn)行得流暢,官方的說法是:為了避免你的控件看得來遲緩校哎,確保動畫始終保持每秒60幀.

下面是官網(wǎng)給出的優(yōu)化建議:

1两波、避免不必要的代碼
2、在onDraw()方法中不應(yīng)該有會導(dǎo)致垃圾回收的代碼闷哆。
3腰奋、盡可能少讓onDraw()方法調(diào)用,大多數(shù)onDraw()方法調(diào)用都是手動調(diào)用了invalidate()的結(jié)果抱怔,所以如果不是必須劣坊,不要調(diào)用invalidate()方法。

下面貼出自定義滑動開關(guān)的完整源碼:

/**
 * 自定義滑動開關(guān)
 */
public class ToggleButton extends View {

    private Bitmap switchBitmap;// 滑動開關(guān)的背景圖片

    private Bitmap slidBitmap;// 滑動塊的背景圖片

    private boolean currentState; // 當(dāng)前滑動開關(guān)的狀態(tài)

    private int currentX;// 手指觸摸點(diǎn)的X值

    private boolean isTouching = false; // 是否觸摸到屏幕

    private ToggleBtnStateChangeListener mToggleBtnStateChangeListener;// 狀態(tài)改變監(jiān)聽器

    // 在xml中引用該控件時屈留,調(diào)用該方法
    public ToggleButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 聲明的命名空間
        String namespace = "http://schemas.android.com/apk/res/com.itheima.togglebuttondemo";
        // 獲取布局中滑動開關(guān)狀態(tài)的屬性
        currentState = attrs.getAttributeBooleanValue(namespace,
                "CurrentState", false);
        // 獲取布局中滑動開關(guān)背景的屬性
        int switchBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
                "SwitchBtnBackgroud", -1);
        // 獲取布局中滑動開關(guān)滑動塊的背景的屬性
        int slidBtnBackgroudId = attrs.getAttributeResourceValue(namespace,
                "SlidBtnBackgroud", -1);
        // 根據(jù)布局中的屬性設(shè)置滑動開關(guān)背景
        setSwitchBtnBackgroudResource(switchBtnBackgroudId);
        // 根據(jù)布局中的屬性設(shè)置滑動開關(guān)滑動塊的背景
        setSlidBtnBackgroudResource(slidBtnBackgroudId);
    }

    // 在代碼中創(chuàng)建該控件時局冰,調(diào)用該構(gòu)造方法
    public ToggleButton(Context context) {
        super(context);
    }

    // 設(shè)置滑動開關(guān)的背景圖片
    public void setSwitchBtnBackgroudResource(int switchBackground) {
        switchBitmap = BitmapFactory.decodeResource(getResources(),
                switchBackground);
    }

    // 設(shè)置滑動塊的背景圖片
    public void setSlidBtnBackgroudResource(int slideButtonBackground) {
        slidBitmap = BitmapFactory.decodeResource(getResources(),
                slideButtonBackground);
    }

    // 設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
    public void setCurrentState(boolean b) {
        currentState = b;
    }

    // 1、測量滑動開關(guān)的寬高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(switchBitmap.getWidth(), switchBitmap.getHeight());
    }

    // 2灌危、繪制锐想,畫出我們的滑動開關(guān)
    // canvas:畫布,將圖形繪制在canvas乍狐,才能顯示到屏幕上
    @Override
    protected void onDraw(Canvas canvas) {
        // 繪制滑動開關(guān)的背景圖片
        canvas.drawBitmap(switchBitmap, 0, 0, null);
        // 繪制滑動塊的背景圖片
        if (isTouching) {// 手指觸摸的時候,根據(jù)currentX的值來繪制滑動塊
            // 根據(jù)手指的X值固逗,來繪制滑動塊圖片
            int left = currentX - slidBitmap.getWidth() / 2;
            if (left < 0) { // 設(shè)置左邊界
                left = 0;
            } else if (left > (switchBitmap.getWidth() - slidBitmap.getWidth())) {// 設(shè)置右邊界
                left = switchBitmap.getWidth() - slidBitmap.getWidth();
            }
            canvas.drawBitmap(slidBitmap, left, 0, null);
        } else {// 手指離開控件的時候浅蚪,根據(jù)狀態(tài)來繪制滑動塊
                // 根據(jù)狀態(tài)值,來繪制滑動塊
            if (currentState) {// 當(dāng)前為true烫罩,開關(guān)打開惜傲,滑動塊顯示在最右邊
                canvas.drawBitmap(slidBitmap, switchBitmap.getWidth()
                        - slidBitmap.getWidth(), 0, null);
            } else {// 當(dāng)前為false,開關(guān)關(guān)閉贝攒,滑動塊顯示在最左邊
                canvas.drawBitmap(slidBitmap, 0, 0, null);
            }
        }
    }

    // 當(dāng)控件被觸摸后盗誊,會調(diào)用該方法
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:// 手指按下
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:// 手指滑動
            isTouching = true;
            currentX = (int) event.getX();
            break;
        case MotionEvent.ACTION_UP:// 手指抬起
            isTouching = false;
            currentX = (int) event.getX();
            int center = switchBitmap.getWidth() / 2;
            // 當(dāng)滑動塊中心點(diǎn)大于滑動開關(guān)背景圖片的中心線時,顯示到右邊,當(dāng)前狀態(tài)為true
            boolean state = currentState;
            // 獲取滑動塊的狀態(tài)
            currentState = currentX > center;
            // 設(shè)置滑動塊的狀態(tài)
            if (mToggleBtnStateChangeListener != null && state != currentState) {
                mToggleBtnStateChangeListener
                        .onToggleBtnStateChange(currentState);
            }
            break;
        default:
            break;
        }
        // 強(qiáng)制讓控件重新繪制,重新調(diào)用onDraw方法
        invalidate();
        // 自己處理觸摸事件
        return true;
    }

    // 給滑動塊設(shè)置狀態(tài)改變監(jiān)聽
    public void setToggleBtnStateChangeListener(
            ToggleBtnStateChangeListener listener) {
        this.mToggleBtnStateChangeListener = listener;
    }

    // 滑動開關(guān)狀態(tài)改變的回調(diào)接口
    public interface ToggleBtnStateChangeListener {
        void onToggleBtnStateChange(boolean currentState);
    }
}

大功告成O(∩_∩)O哈哈~ 下面就是使用啦~

xml布局文件如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:jieyao="http://schemas.android.com/apk/res/com.jieyao.togglebuttondemo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jieyao.togglebuttondemo.view.ToggleButton 
        android:id="@+id/togglebutton"
        android:layout_width="wrap_content"
        android:layout_centerInParent="true"
        jieyao:SwitchBtnBackgroud="@drawable/switch_background"
        jieyao:SlidBtnBackgroud="@drawable/slide_button_background"
        jieyao:CurrentState="false"
        android:layout_height="wrap_content"/>

</RelativeLayout>

activity中使用~

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 初始化滑動開關(guān)
        ToggleButton togglebutton = (ToggleButton) findViewById(R.id.togglebutton);
        // 設(shè)置滑動開關(guān)的背景圖片
        togglebutton.setSwitchBtnBackgroudResource(R.drawable.switch_background);
        // 設(shè)置滑動塊的背景圖片
        togglebutton.setSlidBtnBackgroudResource(R.drawable.slide_button_background);
        // 設(shè)置滑動開關(guān)的默認(rèn)狀態(tài)
        togglebutton.setCurrentState(true);
        // 設(shè)置滑動開關(guān)狀態(tài)監(jiān)聽
        togglebutton.setToggleBtnStateChangeListener(new ToggleBtnStateChangeListener() {

                    @Override
                    public void onToggleBtnStateChange(boolean currentState) {
                        //下面就是根據(jù)currentState狀態(tài)做相應(yīng)的邏輯咯,根據(jù)需求來做
                        if (currentState) {
                            Toast.makeText(getApplicationContext(), "開關(guān)打開",Toast.LENGTH_SHORT).show();
                        } else {
                            Toast.makeText(getApplicationContext(), "開關(guān)關(guān)閉",Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

效果圖如下:

滑動開關(guān)效果

以上就是自定義View的全過程啦~ 希望能對你們有幫助~! 本人技術(shù)有限,如有錯誤,還請指出,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市云石,隨后出現(xiàn)的幾起案子姑宽,更是在濱河造成了極大的恐慌,老刑警劉巖滋戳,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡陕壹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門树埠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糠馆,“玉大人,你說我怎么就攤上這事怎憋∮致担” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵盛霎,是天一觀的道長赠橙。 經(jīng)常有香客問我,道長愤炸,這世上最難降的妖魔是什么期揪? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮规个,結(jié)果婚禮上凤薛,老公的妹妹穿的比我還像新娘。我一直安慰自己诞仓,他們只是感情好缤苫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著墅拭,像睡著了一般活玲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谍婉,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天舒憾,我揣著相機(jī)與錄音,去河邊找鬼穗熬。 笑死镀迂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唤蔗。 我是一名探鬼主播探遵,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窟赏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了箱季?” 一聲冷哼從身側(cè)響起涯穷,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎规哪,沒想到半個月后求豫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诉稍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年蝠嘉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杯巨。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蚤告,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出服爷,到底是詐尸還是另有隱情杜恰,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布仍源,位于F島的核電站心褐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏笼踩。R本人自食惡果不足惜逗爹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嚎于。 院中可真熱鬧掘而,春花似錦、人聲如沸于购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肋僧。三九已至斑胜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嫌吠,已是汗流浹背伪窖。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留居兆,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓竹伸,卻偏偏與公主長得像泥栖,于是被迫代替她去往敵國和親簇宽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

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