Android 精通自定義視圖(3)

項(xiàng)目Demo:https://github.com/liaozhoubei/CustomViewDemo

自定義的開(kāi)關(guān)視圖

前面我們學(xué)習(xí)了幾個(gè)自定義視圖,但是我們發(fā)現(xiàn)了一個(gè)特點(diǎn),那就是那些自定義視圖都是通過(guò)現(xiàn)有的組件的組合做出來(lái)的視圖,雖然也屬于自定義視圖的一種,但也可以說(shuō)是偽自定義視圖。那么怎樣樣才能夠真正定義出自己的視圖,下面我們通過(guò)學(xué)習(xí)直接繼承View類,來(lái)獲取一個(gè)開(kāi)光按鍵,效果如下圖:

toggleview.gif

這次的目標(biāo)是定義一個(gè)開(kāi)關(guān),它的功能要像系統(tǒng)組件一樣,可以在xml中設(shè)定它的屬性,可以在代碼中調(diào)用它的方法。下面我們就來(lái)分析這個(gè)自定義開(kāi)關(guān)的代碼,研究它的構(gòu)成吧!代碼如下:

public class ToggleView extends View {
    private Bitmap mSlideButtonBitmap;
    private Bitmap mSwitchBackgroundBitmap; // 背景圖片
    private Paint mPaint;
    private boolean mSwitchState = false; // 開(kāi)關(guān)狀態(tài), 默認(rèn)false
    private float mCurrentX; // 滑動(dòng)的位置
    private boolean isTouchMode = false;
    private OnSwitchStateUpdateListener onSwitchStateUpdateListener;

    /**
     * 用于代碼創(chuàng)建控件
     * @param context
     */
    public ToggleView(Context context) {
        super(context);
        init();
    }
    /**
     * 用于在xml里使用, 可指定自定義屬性
     * @param context
     * @param attrs
     */
    public ToggleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();

        // 第一種在獲取配置的自定義屬性,官方推薦
        TypedArray attrsArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ToggleView, 0,0);
        // R.styleable.ToggleView是在attrs.xml中給定屬性的名字,后兩個(gè)為默認(rèn)值,0代表不尋找默認(rèn)值
        // 獲取在XML中設(shè)置的布爾值
        mSwitchState = attrsArray.getBoolean(R.styleable.ToggleView_switch_state, false);
        // 獲取從xml中得到的圖片資源ID
        int switch_background = attrsArray.getResourceId(R.styleable.ToggleView_switch_background, -1);
        int slide_button = attrsArray.getResourceId(R.styleable.ToggleView_slide_button, -1);        

        setSwitchBackgroundResource(switch_background);
        setSlideButtonResource(slide_button);
        setSwitchState(mSwitchState);
    }

    /**
     * 用于在xml里使用, 可指定自定義屬性, 如果指定了樣式, 則走此構(gòu)造函數(shù)
     * @param context
     * @param attrs
     * @param defStyle
     */
    public ToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mSwitchBackgroundBitmap.getWidth(), mSwitchBackgroundBitmap.getHeight());
    }
    // Canvas 畫布, 畫板. 在上邊繪制的內(nèi)容都會(huì)顯示到界面上.
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 繪制背景
        canvas.drawBitmap(mSwitchBackgroundBitmap, 0, 0, mPaint );
        // 2. 繪制滑塊
        if (isTouchMode) {
    // 根據(jù)當(dāng)前用戶觸摸到的位置畫滑塊

            // 讓滑塊向左移動(dòng)自身一半大小的位置
            float newPositon = mCurrentX - mSlideButtonBitmap.getWidth() / 2.0f;
            float maxWidth = mSwitchBackgroundBitmap.getWidth() - mSlideButtonBitmap.getWidth();
            // 限定滑塊范圍
            if (newPositon < 0) {
                newPositon = 0; // 左邊范圍
            }
            if (newPositon > maxWidth) {
                newPositon = maxWidth; // 右邊范圍
            }

            canvas.drawBitmap(mSlideButtonBitmap, newPositon, 0, mPaint);
        } else {
            // 根據(jù)開(kāi)關(guān)狀態(tài)boolean, 直接設(shè)置圖片位置
            if (mSwitchState){ // 開(kāi)
                float maxWidth = mSwitchBackgroundBitmap.getWidth() - mSlideButtonBitmap.getWidth();
                canvas.drawBitmap(mSlideButtonBitmap, maxWidth, 0, mPaint);
            } else { // 關(guān)
                canvas.drawBitmap(mSlideButtonBitmap, 0, 0, mPaint);
            }
        }

    }

    // 重寫觸摸事件, 響應(yīng)用戶的觸摸.
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isTouchMode = true;
            mCurrentX = event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            mCurrentX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            isTouchMode =false;
            mCurrentX = event.getX();

            float center = mSwitchBackgroundBitmap.getWidth() / 2;
            // 根據(jù)當(dāng)前按下的位置, 和控件中心的位置進(jìn)行比較.
            boolean state = mCurrentX > center;

            // 如果開(kāi)關(guān)狀態(tài)變化了, 通知界面. 里邊開(kāi)關(guān)狀態(tài)更新了.
            if (state != mSwitchState && onSwitchStateUpdateListener != null){
                // 把最新的boolean, 狀態(tài)傳出去了
                onSwitchStateUpdateListener.onStateUpdate(state);
            }

            mSwitchState = state;
            break;

        default:
            break;
        }
        // 重繪界面
        invalidate(); // 會(huì)引發(fā)onDraw()被調(diào)用, 里邊的變量會(huì)重新生效.界面會(huì)更新
        return true; // 消費(fèi)了用戶的觸摸事件, 才可以收到其他的事件.
    }

    /**
     * 設(shè)置背景圖
     * @param switchBackground
     */
    public void setSwitchBackgroundResource(int switchBackground) {
        mSwitchBackgroundBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
    }
    /**
     * 設(shè)置滑塊圖片資源
     * @param slideButton
     */
    public void setSlideButtonResource(int slideButton) {
        mSlideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
    }
    public void setSwitchState(boolean mSwitchState) {
        this.mSwitchState = mSwitchState;

    }
    /**
     * 設(shè)置開(kāi)關(guān)狀態(tài)
     * @param b
     */
    public interface OnSwitchStateUpdateListener{
        // 狀態(tài)回調(diào), 把當(dāng)前狀態(tài)傳出去
        void onStateUpdate(boolean state);
    }

    public void setOnSwitchStateUpdateListener(OnSwitchStateUpdateListener onSwitchStateUpdateListener){
        this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
    }

}

這么一長(zhǎng)串的代碼看上去有點(diǎn)觸目驚心的感覺(jué),但是不要怕,我們只要一個(gè)方法一個(gè)方法的分析很快就能分析完的。

首先我們分析它的三個(gè)構(gòu)造方法:

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

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

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

這三個(gè)構(gòu)造方法是繼承自view類的,而且是必需要重寫的構(gòu)造方法。第一個(gè)只有Context參數(shù)的方法是用于代碼創(chuàng)建控件的方法,用的可多了,直接new TextView(getContext())這種形式都是用它構(gòu)造出來(lái)的。而擁有兩個(gè)參數(shù)的構(gòu)造方法則是用來(lái)獲取在xml中設(shè)定好的自定義屬性值的,如在ToggleView這個(gè)項(xiàng)目中用這些方法獲取值:

TypedArray attrsArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ToggleView, 0,0);             
    mSwitchState = attrsArray.getBoolean(R.styleable.ToggleView_switch_state, false);        
    int switch_background = attrsArray.getResourceId(R.styleable.ToggleView_switch_background, -1);
    int slide_button = attrsArray.getResourceId(R.styleable.ToggleView_slide_button, -1);

setSwitchBackgroundResource(switch_background);
    setSlideButtonResource(slide_button);
    setSwitchState(mSwitchState);

TypedArray是用于管理屬性類型的數(shù)組,我們將獲得的所有屬性都暫時(shí)存儲(chǔ)在里面,然后在里面獲取。以attrsArray.getBoolean()為例,需要傳入getBoolean()中的參數(shù),第一個(gè)是屬性名稱路徑,第二個(gè)則是默認(rèn)值,其他獲取屬性值的方法類似。當(dāng)獲取到屬性的值之后,再通過(guò)設(shè)置控件的值,我們就能夠在項(xiàng)目中直接使用xml設(shè)定屬性值了。

但是在使用之前我們需要在res-values中創(chuàng)建attrs.xml文件,這種文件用于設(shè)定自定義屬性的,在這個(gè)文件中我們添加了一下代碼:

<resources>
    <declare-styleable name="ToggleView">
        <attr name="switch_background" format="reference" />
        <attr name="slide_button" format="reference" />
        <attr name="switch_state" format="boolean" />
    </declare-styleable>
</resources>

開(kāi)頭的declare-styleable name="ToggleView"代表我們自定義屬性文件名為ToggleView,然后里面的attr則是自定義的屬性名和屬性類型,format則代表這個(gè)屬性的類型。

雖然上面我們已經(jīng)能夠通過(guò)xml獲取屬性值,也能設(shè)置控件的值,但是如果在xml布局文件中使用也是要講究技巧的。

使用自定義控件

使用自定義控件我們需要使用控件的全路徑,也就是 包名.類名,如下:

 <com.example.customviewdemo.Toggleview.ToggleView
android:id="@+id/toggleView"
android:layout_centerInParent="true"
toggleview:switch_background="@drawable/switch_background"
toggleview:slide_button="@drawable/slide_button"
toggleview:switch_state="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />

在這里我們觀察到有個(gè)toggleview:switch_background,這是使用我們自定義屬性的方法,toggleview表示命名空間(可自由更改,但是在使用時(shí)要相同),但是我們現(xiàn)在沒(méi)有命名空間,因此可以在根布局中定義一個(gè)命名空間,命名規(guī)則為: xmlns:[空間名]="http://schemas.android.com/apk/res/[包名]",ToggleView的命名空間如下:

項(xiàng)目Demo:https://github.com/liaozhoubei/CustomViewDemo
xmlns:toggleview="http://schemas.android.com/apk/res/com.example.customviewdemo"

設(shè)定好命名空間之后就能夠像普通的控件屬性一樣使用了。繼續(xù)分析toggleview:switch_background,其中的switch_background表示之前在attrs中定義的屬性名,在這里我們已經(jīng)可以直接設(shè)置背景圖片了。

回到構(gòu)造方法中,分析擁有三個(gè)參數(shù)的構(gòu)造方法,這個(gè)方法是當(dāng)控件有設(shè)置style樣式的時(shí)候使用的,我們并沒(méi)有設(shè)置樣式,所以不需理會(huì)。

分析完構(gòu)造方法之后,我們創(chuàng)建了三個(gè)方法,它們是通過(guò)代碼設(shè)置屬性,如下:

public void setSwitchBackgroundResource(int switchBackground) {
  mSwitchBackgroundBitmap = BitmapFactory.decodeResource(getResources(), switchBackground);
}

public void setSlideButtonResource(int slideButton) {
  mSlideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
}
public void setSwitchState(boolean mSwitchState) {
  this.mSwitchState = mSwitchState;  
}

代碼很簡(jiǎn)單,也就不多做解釋。

然后,我們要思考到這個(gè)控件已經(jīng)被創(chuàng)建出來(lái)呢,也設(shè)置了圖片資源,那么接下來(lái)應(yīng)該怎么辦呢?很明顯,控件既然已經(jīng)被構(gòu)造出來(lái),那么就應(yīng)該在屏幕中顯示出來(lái),所以我們重寫了onDraw()方法,但是有個(gè)問(wèn)題,那就是我們還不知道控件的大小。想要畫出一個(gè)東西,卻不知道控件的大小怎么可以,所以我們重寫了onMeasure()方法。

使用onMeasure()方法的時(shí)候,我們要注意一點(diǎn),那就是一個(gè)控件展示在屏幕中,是在Activity活動(dòng)setContentView()設(shè)置好布局之后,在Activity的生命中期走到onResume()的時(shí)候才會(huì)顯示控件,在這之前是沒(méi)有控件的,也就意味著沒(méi)有控件的大小。

這時(shí)我們無(wú)法直接使用getWidth()/getHeight()方法來(lái)獲取控件的高度和寬度。沒(méi)關(guān)系,View類中有setMeasuredDimension()方法能夠測(cè)量到給定圖片的寬高,如下:

setMeasuredDimension(mSwitchBackgroundBitmap.getWidth(), mSwitchBackgroundBitmap.getHeight());

這個(gè)方法是將獲得的圖片資源原始的寬高得到。而使用普通的getWidth()/getHeight()則是當(dāng)控件在屏幕中出現(xiàn)之后才能獲取寬高,否則為0!

在獲取圖片寬高之后,我們就能夠正常的使用getWidth()/getHeight()方法了。在onDraw()方法中使用canvas.drawBitmap()便可將圖片在屏幕中繪制出來(lái)了。

其實(shí)走到這一步我們基本上已經(jīng)完成了自定義視圖的所有步驟了。

在onDraw()方法里面還有很多代碼,里面的意思是限定開(kāi)關(guān)按鍵的圖片的位置,讓其位置限定在某個(gè)范圍之內(nèi),保證其不會(huì)發(fā)生跑出開(kāi)關(guān)位置的bug。

onTouchEvent()觸摸事件也是同樣的邏輯,在用手指滑動(dòng)的時(shí)候保證其能夠左右滑動(dòng),并且停留在開(kāi)或者關(guān)的位置,觸摸事件最后調(diào)用了

invalidate();

這個(gè)方法表示重繪視圖,每次開(kāi)關(guān)被移動(dòng)之后都要重新繪制一遍,讓開(kāi)關(guān)動(dòng)起來(lái)。

最后我們還是用接口的方式將當(dāng)前開(kāi)關(guān)狀態(tài)傳出去,代碼如下:

public interface OnSwitchStateUpdateListener{
// 狀態(tài)回調(diào), 把當(dāng)前狀態(tài)傳出去
void onStateUpdate(boolean state);
}

public void setOnSwitchStateUpdateListener(OnSwitchStateUpdateListener onSwitchStateUpdateListener){
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}

當(dāng)這個(gè)方法被重寫,在onTouchEvent()觸摸事件中的MotionEvent.ACTION_UP手指抬起時(shí)就會(huì)調(diào)用接口中的方法,代碼如下:

if (state != mSwitchState && onSwitchStateUpdateListener != null){
  // 把最新的boolean, 狀態(tài)傳出去了
  onSwitchStateUpdateListener.onStateUpdate(state);
}

這里簡(jiǎn)單的解析了一下自定義開(kāi)關(guān)的實(shí)現(xiàn)原理,里面的代碼還是需要大家多多研究才能夠吃透弄懂

擴(kuò)展閱讀:

Android 精通自定義視圖(1) http://www.itdecent.cn/p/c2195269ce44

Android 精通自定義視圖(2) http://www.itdecent.cn/p/092e126b623f

Android 精通自定義視圖(4) http://www.itdecent.cn/p/850e387fc9d8

Android 精通自定義視圖(5) http://www.itdecent.cn/p/93feac19c396

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容