自制控件1 開關按鈕

本文出自 “阿敏其人” 簡書博客,轉載或引用請注明出處。

自定義控件——初識自定義控件里面,我們已經大致介紹三種自定義控件,分別是

  • 自制控件
  • 組合控件
  • 拓展控件

并且,我們已經對自制控件就繼承自View和繼承自ViewGroup進行了分析和最簡單deme展示。

熟能生巧,接下的幾篇文章,我們依然來進行自制控件。
在本篇里面,我們來進行自制簡單的開關按鈕。

有圖有真相,先看一下最終的效果圖。

效果.gif

馬上開工。

一、思路整理

從上面的圖片中,我們看出,這個自定義控件涉及到3張圖片,1張是黑色背景,1張是白色背景,另外一張就是一個圓形球的圖片。

思路:弄一個類繼承自View,比如叫做DiyToggleView,利用onDraw()方法里面,控制著三張圖片在對應的時刻顯示對應的圖片。

我們觸摸球狀圖片的時候,這張圖片會動起來,所以需要用到onTouchEvent,然后在這里面進行MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP的判斷

在DiyToggleView里面,弄一些方法和回調接口給控件的使用者使用。

大概思路已經整理好了,現在開工。

二、準備三張圖片,然后在在繼承自View的自定義控件里面做出一個最簡單的開關的樣子

.
.

1、準備3張圖片

球.png
關閉背景.png
打開背景.png

2、畫上簡單的開關樣子

這個開關我們繼承自View,然后復寫三個構造方法。

其實說到底,我們這個開關的自定義流程就是弄3個Bitmap,然后對點擊和滑動進行監聽,根據滑動的位置控制Bitmap是否顯示。

既然思路擺在這里,那么我們就先在onDraw畫上一個開關的樣子。

DiyToggleView

public class DiyToggleView extends View {
    private Bitmap toggleBall;
    private Bitmap toggleOnBg;
    private Bitmap toggleOffBg;
    public DiyToggleView(Context context) {
        super(context);
    }
    public DiyToggleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    /**
     * 設置開關的滑動的球
     * @param resId
     */
    public void setToggleBallBitmap(int resId) {
        toggleBall = BitmapFactory.decodeResource(getResources(), resId);
    }
    /**
     * 設置開關的打開狀態的背景
     * @param resId
     */
    public void setToggleOnBgBitmap(int resId){
        toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
    }
    /**
     * 設置開關關閉狀態的背景
     * @param resId
     */
    public void setToggleOffBgBitmap(int resId){
        toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(toggleOnBg != null){
            canvas.drawBitmap(toggleOnBg, 0, 0, null);
        }
        if(toggleBall != null){
            canvas.drawBitmap(toggleBall,0,0,null);
        }
        
    }
    // 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
    // 在onMeasure里面精確控制大小用到的是  setMeasuredDimension 這個方法
    // 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = toggleOnBg.getWidth();
        int measureHeight = toggleOnBg.getHeight();
        setMeasuredDimension(measureWidth,measureHeight);  // 控制View的大小的關鍵方法
    }
    
}

.
.
MainActivity

public class MainActivity extends Activity {
    private DiyToggleView mDtv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDtv = (DiyToggleView) findViewById(R.id.mDtv);
        mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
        mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
        mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
    }
}

.
.
activity_main

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.amqr.diytoggle.MainActivity">
    <com.amqr.diytoggle.DiyToggleView
        android:id="@+id/mDtv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="#00ff00"
        />
</RelativeLayout>

畫好了
(綠色背景是故意設置上去的,便于標示)

模樣初長成.png

三、利用 onTouchEvent和onDraw讓開關球可以被滑動

代碼都不用改動,只需要改動 DiyToggleView

public class DiyToggleView extends View {
    private Bitmap toggleBall;
    private Bitmap toggleOnBg;
    private Bitmap toggleOffBg;
    private int ballCurrentX;  // 當前開關球所在的X
    public DiyToggleView(Context context) {
        super(context);
    }
    public DiyToggleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    /**
     * 設置開關的滑動的球
     * @param resId
     */
    public void setToggleBallBitmap(int resId) {
        toggleBall = BitmapFactory.decodeResource(getResources(), resId);
    }
    /**
     * 設置開關的打開狀態的背景
     * @param resId
     */
    public void setToggleOnBgBitmap(int resId){
        toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
    }
    /**
     * 設置開關關閉狀態的背景
     * @param resId
     */
    public void setToggleOffBgBitmap(int resId){
        toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(toggleOnBg != null){
            canvas.drawBitmap(toggleOnBg, 0, 0, null);
        }
        if(toggleBall != null){
            canvas.drawBitmap(toggleBall,ballCurrentX,0,null);
        }
    }
    // 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
    // 在onMeasure里面精確控制大小用到的是  setMeasuredDimension 這個方法
    // 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = toggleOnBg.getWidth();
        int measureHeight = toggleOnBg.getHeight();
        setMeasuredDimension(measureWidth,measureHeight);  // 控制View的大小的關鍵方法
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        // 觸摸事件的down,move,up,按下,移動,松開
        // getX()是觸摸的點與控件自身的距離
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                ballCurrentX = (int)(event.getX()+0.5f); // getX()代表當前,返回值是float,加上0.5是為了確保四舍五進進1
                break;
            case MotionEvent.ACTION_MOVE:
                ballCurrentX = (int)(event.getX()+0.5f);
                break;
            case MotionEvent.ACTION_UP:
                ballCurrentX = (int)(event.getX()+0.5f);
                break;
        }
        invalidate(); // 重新繪制,可以理解為調用onDraw
        return true; // 返回 true,代表當前View消費當前touch
    }
}

簡單的滑動最好了,但是現在滑到中途釋放的時候沒有進行位置歸位判斷,而且還有越界問題。

四、釋放的位置歸和越界問題的解決

其實也簡單,就是添加兩個boolean值,判斷是否打開和是否觸摸

private boolean isOpen = true;
private boolean isOpen;  // 作用是區分開 觸摸事件的 up

然后onDraw和onTouchEvent相互結合,isOpen和isOpen的值,做出相應的位置處理

最后我們寫了一個回調,讓調用者可以獲知當前的開關狀態和控制開關的狀態。

.
.
DiyToggleView


public class DiyToggleView extends View {
    private Bitmap toggleBall;
    private Bitmap toggleOnBg;
    private Bitmap toggleOffBg;
    private int ballCurrentX;  // 當前開關球所在的X
    private boolean isOpen = true;
    private boolean isTouch;  // 作用是區分開 觸摸事件的 up
    private ToggleStaleListener toggleStaleListener;
    /**
     * 對開關狀態的回調操作
     * @param toggleStaleListener
     */
    public void setToggleState(ToggleStaleListener toggleStaleListener){
        this.toggleStaleListener = toggleStaleListener;
    }
    public void setToggleState(boolean state){
        isOpen = state;
        invalidate();
    }
    public boolean getToggleState(){
        return isOpen;
    }
    public DiyToggleView(Context context) {
        super(context);
    }
    public DiyToggleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    /**
     * 設置開關的滑動的球
     *
     * @param resId
     */
    public void setToggleBallBitmap(int resId) {
        toggleBall = BitmapFactory.decodeResource(getResources(), resId);
    }
    /**
     * 設置開關的打開狀態的背景
     *
     * @param resId
     */
    public void setToggleOnBgBitmap(int resId) {
        toggleOnBg = BitmapFactory.decodeResource(getResources(), resId);
    }
    /**
     * 設置開關關閉狀態的背景
     *
     * @param resId
     */
    public void setToggleOffBgBitmap(int resId) {
        toggleOffBg = BitmapFactory.decodeResource(getResources(), resId);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (toggleOnBg == null ||toggleOffBg==null|| toggleBall == null) {
            return;
        }
        int ballWidth = toggleBall.getWidth();
        int ballToRightX = toggleOnBg.getWidth() - ballWidth;
        // 當處于觸摸事件的down和move狀態
        if(isTouch){
            if(isOpen){
                canvas.drawBitmap(toggleOnBg, 0, 0, null);
                // 開關球 不觸及邊界的自由活動范圍
                if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
                    canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
                }
                // 當開關球與View的距離大于右側的球和邊框的距離,就停在右側
                if (ballCurrentX>ballToRightX){
                    canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
                }
                // 當開關球與View的距離小于左側球與邊距的邊框的距離,就停在左側
                if (ballCurrentX < ballWidth) {
                    canvas.drawBitmap(toggleBall, 0, 0, null);
                    isOpen = true;
                }
            }else{
                canvas.drawBitmap(toggleOffBg, 0, 0, null);
                if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
                    canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
                }
                if (ballCurrentX > ballToRightX) {
                    canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
                    isOpen = false;
                }
                if(ballCurrentX < ballWidth){
                    canvas.drawBitmap(toggleBall, 0, 0, null);
                }
            }
        // 當觸摸時間的up狀態被觸發
        }else{
            if(isOpen){
                canvas.drawBitmap(toggleOnBg, 0, 0, null);
                canvas.drawBitmap(toggleBall, 0, 0, null);
            }else{
                canvas.drawBitmap(toggleOffBg, 0, 0, null);
                canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
            }
        }
    }
    // 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
    // 在onMeasure里面精確控制大小用到的是  setMeasuredDimension 這個方法
    // 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = toggleOnBg.getWidth();
        int measureHeight = toggleOnBg.getHeight();
        setMeasuredDimension(measureWidth, measureHeight);  // 控制View的大小的關鍵方法
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        // 觸摸事件的down,move,up,按下,移動,松開
        // getX()是觸摸的點與控件自身的距離
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                isTouch = true;
                ballCurrentX = (int)(event.getX()+0.5f); // getX()代表當前,返回值是float,加上0.5是為了確保四舍五進進1
                break;
            case MotionEvent.ACTION_MOVE:
                isTouch = true;
                ballCurrentX = (int) (event.getX() + 0.5f);
                break;
            case MotionEvent.ACTION_UP:
                isTouch = false;
                ballCurrentX = (int) (event.getX() + 0.5f);
                if(ballCurrentX<toggleBall.getWidth()){
                    isOpen = true;
                    if(toggleStaleListener != null){
                        // 回調真正被執行
                        toggleStaleListener.toggleState(this,isOpen);
                    }
                }
                if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
                    isOpen = false;
                    if(toggleStaleListener != null){
                        toggleStaleListener.toggleState(this,isOpen);
                    }
                }
                break;
        }
        invalidate(); // 重新繪制,可以理解為調用onDraw
        return true; // 返回 true,代表當前View消費當前touch
    }
    // 回調接口
    public interface ToggleStaleListener{
        void toggleState(DiyToggleView view,boolean state);
    }
}

上面的代碼其實主要看onDraw和onTouchEvent就好。
上面的onTouchEvent需要注意的是記得在后面加上invalidate();每次invalidate()被執行就會去重新調用一下onDraw

1、利用getX獲得球當前相對于控件的距離

不管是越界處理還是手指釋放時的位置歸正,都需要對球的位置進行判斷,那么怎么獲取球當前的位置呢?利用getX

這里我們有必要先來了解什么View的
getX()、getY()
getRawX()、getRawY()
這么幾個方法。

這幾個方法都是關于距離和點的方法。

先看圖

getX和getRawX的區別.png

上結論:
getX()是觸摸的點與控件自身的距離
getRawX()是觸摸的點與屏幕的距離

(getX之后得到的是float類型的值,而不是int,所以我們加上0.5f保證帶小數的float轉換成int類型的時候都能夠進1,方便計算。)

我們利用getWidth可以得到背景的寬度。
利用getX可以得到當前我們的球當前按下的點距離背景的距離。

這兩者一結合,再加判斷,就可解決越界和位置歸正的問題

越界問題的解決

左側不越界

// 當開關球與View的距離小于左側球與邊距的邊框的距離,就停在左側
if (ballCurrentX < ballWidth) {
    canvas.drawBitmap(toggleBall, 0, 0, null);
    isOpen = true;
}

右側不越界

// 當開關球與View的距離大于右側的球和邊框的距離,就停在右側
if (ballCurrentX>ballToRightX){
    canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
}

位置歸正問題的解決

        // 當觸摸時間的up狀態被觸發
        }else{
            if(isOpen){
                canvas.drawBitmap(toggleOnBg, 0, 0, null);
                canvas.drawBitmap(toggleBall, 0, 0, null);
            }else{
                canvas.drawBitmap(toggleOffBg, 0, 0, null);
                canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
            }
        }

設置回調接口,給使用者自主設置開關的狀態的權力

    // 回調接口
    public interface ToggleStaleListener{
        void toggleState(DiyToggleView view,boolean state);
    }

回調執行的地方

case MotionEvent.ACTION_UP:
    isTouch = false;
    ballCurrentX = (int) (event.getX() + 0.5f);
    if(ballCurrentX<toggleBall.getWidth()){
        isOpen = true;
        if(toggleStaleListener != null){
            // 回調真正被執行
            toggleStaleListener.toggleState(this,isOpen);
        }
    }
    if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
        isOpen = false;
        if(toggleStaleListener != null){
            toggleStaleListener.toggleState(this,isOpen);
        }
    }
    break;

關于回調可以參考這里 說說安卓回調——CallBack
.
.
MainActivity 使用這個回調

public class MainActivity extends Activity {
    private DiyToggleView mDtv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDtv = (DiyToggleView) findViewById(R.id.mDtv);
        mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
        mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
        mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
        mDtv.setToggleState(false);
        mDtv.setToggleState(new DiyToggleView.ToggleStaleListener() {
            @Override
            public void toggleState(DiyToggleView view, boolean state) {
                Toast.makeText(MainActivity.this,"當前的開關狀態是:"+mDtv.getToggleState(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

至此完成。
另外一篇博文自制控件2 —— 自制控件 仿qq側滑菜單一文中,將繼續自制控件,歡迎點擊查閱。

效果.gif

下載鏈接

本篇完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容