Android自定義控件:滑動開關(guān)機

最近重構(gòu)代碼,發(fā)現(xiàn)了之前偷懶遺留的一個問題。有一個控制設(shè)備開關(guān)機的控件,由于之前趕項目交期,匆匆忙忙直接在Activity中重寫onTouch事件,效果雖然也實現(xiàn)了,但是肯定不是很好的,今天重新將這個小玩意重新封裝成一個自定義控件,話不多說,先看看實現(xiàn)的效果。


這里寫圖片描述

其實看樣子都知道,是一個蠻簡單的自定義控件,至于為什么要寫這篇博客呢,因為也有段時間沒有搞自定義控件了,一時手癢,哈哈 = =,溫故而知新,總結(jié)一下總歸是有好處的。

既然是自定義控件,那么自然少不了自定義屬性,先貼上attrs.xml代碼。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SlideSwitchView">
        <attr name="slide_button" format="reference"/>
        <attr name="android:text"/>
        <attr name="android:textSize"/>
        <attr name="android:textColor"/>
    </declare-styleable>
</resources>

這里定義了四個屬性,分別是文本內(nèi)容,文本字號,文本顏色以及滑動button的背景圖片,如果有拓展需求的話,也可以很簡單的拓展。

然后再看布局代碼, 簡單粗暴,就只有一個控件。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:swipe="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.zyw.horrarndoo.swipebutton.MainActivity">

    <com.zyw.horrarndoo.swipebutton.SlideSwitchView
        android:layout_centerInParent="true"
        android:id="@+id/slide_switch_view"
        swipe:slide_button="@mipmap/slide"
        android:text = "Slide to power on"
        android:textSize = "22sp"
        android:textColor = "@android:color/holo_green_dark"
        android:layout_width="wrap_content"
        android:layout_height="60dp"/>

</RelativeLayout>

下面開始寫我們的自定義控件,寫代碼之前,不妨先擼一擼思路。

  1. 我們這個自定義控件跟現(xiàn)有的控件實際上是沒有什么聯(lián)系的,所以我們這里自定義控件繼承自View;
  2. 由于button是要隨手勢拖動的,所以我們肯定是要重寫onTouchEvent方法的,在onTouchEvent中動態(tài)的更新button的X坐標(biāo)值;
  3. 既然界面會涉及到重繪,所以我們肯定需要重寫onDraw方法,為了實現(xiàn)button拖動,這里我們通過drawBitmap的方法動態(tài)繪制button;
  4. 由于我們這個控件會放到各個界面中去用,我們肯定就需要重寫onMeasure方法,動態(tài)的確定button的寬高以及text的長度等;
  5. 外界需要知道控件滑動的結(jié)果,我們這里定義一個接口將狀態(tài)回調(diào)出去;

思路擼完,上代碼。

/**
 * 自定義滑動開關(guān)
 * <p>
 * Created by Horrarndoo on 2017/6/1.
 */
public class SlideSwitchView extends View {
    private Bitmap slideButtonBitmap; // 滑塊圖片
    private Paint mPaint; // 畫筆
    private float currentX; //當(dāng)前滑動的x坐標(biāo)
    private int mBaseLineY; //  text基準(zhǔn)線
    private String mTextContent; //text內(nèi)容

    public SlideSwitchView(Context context) {
        this(context, null);
    }

    public SlideSwitchView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideSwitchView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initPaint();
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideSwitchView);
        setSlideButtonResource(ta.getResourceId(R.styleable.SlideSwitchView_slide_button, -1));
        setText(ta.getString(R.styleable.SlideSwitchView_android_text));
        setTextSize(ta.getDimension(R.styleable.SlideSwitchView_android_textSize,30));
        setTextColor(ta.getColor(R.styleable.SlideSwitchView_android_textColor, Color.BLACK));
        ta.recycle();
    }

    /**
     * 初始化畫筆
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.LEFT);
        mPaint.setAntiAlias(true);
    }

    /**
     * 初始化text居中基準(zhǔn)線
     */
    private void initTextBaseLine() {
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float top = fontMetrics.top;//為基線到字體上邊框的距離,即上圖中的top
        float bottom = fontMetrics.bottom;//為基線到字體下邊框的距離,即上圖中的bottom
        mBaseLineY = (int) (getMeasuredHeight() / 2 - top / 2 - bottom / 2);//基線中間點的y軸計算公式
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST) {
            int newWidth = (int) (slideButtonBitmap.getWidth() * 2 + getTextWidth());
            if (width >= newWidth)
                width = newWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            if (height < slideButtonBitmap.getHeight()) {
                // 獲得圖片的寬高
                int widthSlide = slideButtonBitmap.getWidth();
                int heightSlide = slideButtonBitmap.getHeight();
                float scaleHeight = height * 1.0f / slideButtonBitmap.getHeight();
                Matrix matrix = new Matrix();
                matrix.postScale(scaleHeight, scaleHeight);
                slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
                        heightSlide, matrix, true);
                invalidate();
            }
        }

        if (slideButtonBitmap.getWidth() > (width - getTextWidth()) / 2) {
            // 獲得圖片的寬高
            int widthSlide = slideButtonBitmap.getWidth();
            int heightSlide = slideButtonBitmap.getHeight();
            float scaleWidth = (width - getTextWidth()) / 2 / slideButtonBitmap.getWidth();
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleWidth);
            slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
                    heightSlide, matrix, true);
            invalidate();
        }

        setMeasuredDimension(width, slideButtonBitmap.getHeight());
        initTextBaseLine();
    }

    // Canvas 畫布, 畫板. 在上邊繪制的內(nèi)容都會顯示到界面上.
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 繪制text
        canvas.drawText(mTextContent, slideButtonBitmap.getWidth(), mBaseLineY, mPaint);

        // 2. 繪制滑塊
        if (isTouchMode) {
            // 根據(jù)當(dāng)前用戶觸摸到的位置畫滑塊
            // 讓滑塊向左移動自身一半大小的位置
            float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;

            int maxLeft = getMeasuredWidth() - slideButtonBitmap.getWidth();

            // 限定滑塊范圍
            if (newLeft < 0) {
                newLeft = 0; // 左邊范圍
            } else if (newLeft > maxLeft) {
                newLeft = maxLeft; // 右邊范圍
            }

            canvas.drawBitmap(slideButtonBitmap, newLeft, 0, mPaint);
        } else {
            //還原button位置
            canvas.drawBitmap(slideButtonBitmap, 0, 0, mPaint);
        }

    }

    boolean isTouchMode = false;
    private OnSwitchStateUpdateListener onSwitchStateUpdateListener;

    // 重寫觸摸事件, 響應(yīng)用戶的觸摸.
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isTouchMode = true;
                currentX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                currentX = event.getX();
                break;
            case MotionEvent.ACTION_UP:
                isTouchMode = false;
                currentX = event.getX();

                float center = getMeasuredWidth() / 2.0f;

                // 根據(jù)當(dāng)前按下的位置, 和控件中心的位置進行比較.
                boolean isStateChanged = currentX > center;

                // 如果開關(guān)狀態(tài)變化了, 通知界面
                if (isStateChanged && onSwitchStateUpdateListener != null) {
                    onSwitchStateUpdateListener.onStateUpdate();
                }
                break;

            default:
                break;
        }

        // 重繪界面
        invalidate(); // 會引發(fā)onDraw()被調(diào)用, 里邊的變量會重新生效.界面會更新

        return true; // 消費了用戶的觸摸事件, 才可以收到其他的事件.
    }

    /**
     * 設(shè)置滑塊圖片資源
     *
     * @param slideButton 滑塊圖片資源
     */
    public void setSlideButtonResource(int slideButton) {
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
    }

    /**
     * 設(shè)置text字號大小
     *
     * @param textSize text字號大小
     */
    public void setTextSize(float textSize) {
        mPaint.setTextSize(textSize);
        mPaint.setStrokeWidth(textSize / 15.f);
    }

    /**
     * 設(shè)置text內(nèi)容
     *
     * @param text text內(nèi)容
     */
    public void setText(String text) {
        mTextContent = text;
    }

    /**
     * 設(shè)置text顏色
     *
     * @param color text顏色資源
     */
    public void setTextColor(int color) {
        mPaint.setColor(color);
    }

    /**
     * 獲取text文字寬度
     *
     * @return text文字寬度
     */
    private float getTextWidth() {
        return mPaint.measureText(mTextContent);
    }

    /**
     * 獲取text文字高度
     *
     * @return text文字高度
     */
    private float getTextHeight() {
        return mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top;
    }

    public interface OnSwitchStateUpdateListener {
        // 狀態(tài)回調(diào)
        void onStateUpdate();
    }

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

這里我們主要看看onTouchEvent方法和onMeasure方法。
在onTouchEvent中,我們在ACTION_MOVE中不斷的刷新button的X坐標(biāo),然后刷新button的位置,達到拖動button滑動的效果 ,最后在ACTION_UP中判斷滑動位置是否超過控件一半位置來確定是否傳遞滑動狀態(tài)變化給回調(diào)。

在onMeasure中,我們計算了幾種情況。

  1. 首先要保證寬度為wrap_content的時候,控件要有滑動的空間,所以在wrap_content的時候,設(shè)置控件寬度值為button.width*2+text.width,保證控件最低的寬度值。
  2. 控件高度確定的情況下,button的寬高也根據(jù)控件寬高做相應(yīng)比例的縮放,避免圖片超出控件范圍。
  3. 最后要計算圖片寬度,保證view.width = button.width*2+text.width。

最后一步,在Activity中調(diào)用。

public class MainActivity extends AppCompatActivity {
    private SlideSwitchView mSlideSwitchView;
    private boolean mIsPowerOn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSlideSwitchView = (SlideSwitchView) findViewById(R.id.slide_switch_view);
        mSlideSwitchView.setOnSwitchStateUpdateListener(new SlideSwitchView.OnSwitchStateUpdateListener() {
            @Override
            public void onStateUpdate() {
                mIsPowerOn = !mIsPowerOn;
                String content = mIsPowerOn ? "Slide to power off" : "Slide to power on";
                mSlideSwitchView.setText(content);
            }
        });
    }
}

附上完整demo地址:https://github.com/Horrarndoo/SwipeButton

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

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

  • 6、View的繪制 (1)當(dāng)測量好一個View之后,我們就可以簡單的重寫 onDraw()方法,并在 Canvas...
    b5e7a6386c84閱讀 1,970評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 星星不常在,人亦許久未謀面,黑夜之中陪伴我的只有星星和記憶中的你。 艷陽下,人行道旁,一邊與朋友笑談其他,一邊在心...
    饅頭面具閱讀 273評論 0 0
  • 前情提要:戀你十年,未曾改變(四十八) 我把冰箱里剩下的食材全都翻了出來,做了幾個坤喜歡吃的小菜。解下圍裙的那一...
    lemoney閱讀 649評論 3 4
  • 有人說你生不逢時, 一開花就遭遇嚴(yán)寒天氣; 有人說你孤芳自賞, 獨自開放,與百花脫離; 有人說你沒有朋友, 蜂兒蝶...
    竹林灑陽光閱讀 261評論 2 2

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