Android自定義滑動確認控件SlideView

項目GitHub

 https://github.com/Gnepux/SlideViewjava

前言

目前App上有不少對於按鈕誤操做的控制。好比點擊按鈕後彈出確認框,可是這樣的模式略顯死板。爲了給App賦予更多的生命力,能夠借鑑網站登陸滑動確認的方式。這種方式目前更多地用於web登陸。如下是某網站登陸時使用的滑動驗證,用來取代以往的驗證碼模式。android

咱們的App能夠借鑑上述的模式自定義一個滑動確認的控件,能夠用於控制誤操做點擊的場景。固然其餘的應用場景能夠等待你們細細挖掘。git

設計思路

咱們就暫且命名這個自定義的滑動控件叫SlideViewgithub

先來總結一下SlideView的主要功能:「按住一個進度條裏的按鈕往右滑,若是滑到通常鬆開按鈕自動回到原位,若是滑到底則給出完成提示」。web

貌似就是這麼一句話的事。固然還有更多屬性設置,好比背景的文字、顏色、滑動按鈕和進度條的比例等等。canvas

經過前面一句話的介紹,是否是讓咱們想起了Andoird的SeekBar控件?確實重寫SeekBar控件的確能夠實現咱們想要實現的功能,但可定製稍微差了些,因此決定重頭開始構建SlideView。app

方案

既然要重構構建SlideView,那咱們就要實現一個自定義的ViewGroup。添加背景圖和提示文字。以後再將能夠拖動的按鈕加入到這個ViewGroup中。那個所謂「能夠拖動的按鈕」咱們就叫它SlideIcon,這是一個自定義View。也能夠添加背景圖和提示文字,控制它的寬度與SildeView總寬度的比例,最後爲這個View加上觸摸事件,按下以後能夠拖動,拖動到通常鬆開回到起點,拖到底觸發一個完成的回調。ide

整體方案就是這樣,是否是很簡單,下面讓咱們來一步步實現這個SlideView。佈局

可拖動的部分 - SlideIcon

SlideIcon是一個自定義View,它的主要功能就是拖動。其中咱們須要作的工做跟就是測量尺寸、添加觸摸事件、繪製背景圖和文字。字體

具體代碼以下:

/**
 * 可拖動的View
 */
private class SlideIcon extends View {
    // 用來控制觸摸事件是否可用
    private boolean mEnable;

    // 提示文字的Paint
    private Paint mTextPaint = null;

    // 提示文字的字體測量類
    private Paint.FontMetrics mFontMetrics;

    // 回調
    private MotionListener listener = null;

    // 手指按下時SlideIcon的X座標
    private float mDownX = 0;

    // SlideIcon在非拖動狀態下的X座標
    private float mX = 0;

    // SliedIcon在拖動狀態下X軸的偏移量
    private float mDistanceX = 0;

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

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

    public void setListener(MotionListener listener) {
        this.listener = listener;
    }

    public void setEnable(boolean enable) {
        this.mEnable = enable;
    }

    public boolean getEnable() {
        return mEnable;
    }

    private void init() {
        // 設置文字Paint
        mTextPaint = new Paint();
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setColor(mIconTextColor);
        mTextPaint.setTextSize(mIconTextSize);

        // 獲取字體測量類
        mFontMetrics = mTextPaint.getFontMetrics();

        // 設置背景圖
        setBackgroundResource(mIconResId);

        // 設置觸摸事件可用
        mEnable = true;
    }

    /**
     * 重置SlideIcon
     */
    public void resetIcon() {
        mDownX = 0;
        mDistanceX = 0;
        mX = 0;
        mEnable = true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 寬度和寬Mode
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        // 高度和高Mode
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        switch (heightMode) {
            case MeasureSpec.AT_MOST:   // layout_height爲"wrap_content"時顯示最小高度
                setMeasuredDimension(MeasureSpec.makeMeasureSpec((int)(widthSize * mIconRatio), widthMode),
                        MeasureSpec.makeMeasureSpec(mMinHeight, heightMode));
                break;
            default:    // layout_height爲"match_parent"或指定具體高度時顯示默認高度
                setMeasuredDimension(MeasureSpec.makeMeasureSpec((int)(widthSize * mIconRatio), widthMode),
                        MeasureSpec.makeMeasureSpec(heightSize, heightMode));
                break;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 獲取文字baseline的Y座標
        float baselineY = (getMeasuredHeight() - mFontMetrics.top - mFontMetrics.bottom) / 2;
        // 繪製文字
        canvas.drawText(mIconText == null ? "":mIconText, getMeasuredWidth() / 2, baselineY, mTextPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mEnable) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                // 記錄手指按下時SlideIcon的X座標
                mDownX = event.getRawX();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
                // 設置手指鬆開時SlideIcon的X座標
                mDownX = 0;
                mX = mX + mDistanceX;
                mDistanceX = 0;
                // 觸發鬆開回調並傳入當前SlideIcon的X座標
                if (listener != null) {
                    listener.onActionUp((int) mX);
                }
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
                // 記錄SlideIcon在X軸上的拖動距離
                mDistanceX = event.getRawX() - mDownX;
                // 觸發拖動回調並傳入當前SlideIcon的拖動距離
                if (listener != null) {
                    listener.onActionMove((int) mDistanceX);
                }
                return true;
            }
            return false;
        } else {
            return true;
        }
    }
}

這裏咱們定義了一個MotionListener,它是用來記錄觸摸操做的監聽類,主要用來監聽拖動和鬆開動做。

/**
 * 觸摸事件的回調
 */
private interface MotionListener {
    /**
     * 拖動時的回調
     * @param distanceX SlideIcon的X軸偏移量
     */
    void onActionMove(int distanceX);

    /**
     * 鬆開時的回調
     * @param x SlideIcon的X座標
     */
    void onActionUp(int x);
}

代碼邏輯很簡單,這裏有必要說明的地方就是onMeasure()方法。咱們須要根據heightMeasureSpec獲得heightMode。若是是wrap_content的狀況,那麼咱們就須要將SlideView設置爲最小高度(咱們須要指定的一個attr),這樣父view纔會根據SlideView的高度顯示成最小高度,不然在指定layout_height="wrap_content"時沒法顯示正確高度。

高度定製化 - SlideView的屬性

前面有提到,爲了知足高度可定製化才決定重寫。因此SlideView爲調用者提供多種可定製屬性必不可少。具體的提供的屬性以下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SlideView">
        <!--背景圖片-->
        <attr name="bg_drawable" format="reference"/>
        <!--按鈕的背景圖-->
        <attr name="icon_drawable" format="reference"/>
        <!--按鈕上顯示的文字-->
        <attr name="icon_text" format="string"/>
        <!--按鈕上文字的顏色-->
        <attr name="icon_text_color" format="color"/>
        <!--按鈕上文字的大小-->
        <attr name="icon_text_size" format="dimension"/>
        <!--按鈕寬佔總寬度的比例-->
        <attr name="icon_ratio" format="float"/>
        <!--背景文字-->
        <attr name="bg_text" format="string"/>
        <!--拖動完成的背景文字-->
        <attr name="bg_text_complete" format="string"/>
        <!--背景文字的顏色-->
        <attr name="bg_text_color" format="color"/>
        <!--背景文字的大小-->
        <attr name="bg_text_size" format="dimension"/>
        <!--控件最小高度-->
        <attr name="min_height" format="dimension"/>
        <!--已拖動部分的顏色-->
        <attr name="secondary_color" format="color"/>
        <!--拖動到一半鬆開是否重置按鈕-->
        <attr name="reset_not_full" format="boolean"/>
        <!--拖動結束後是否能夠再次操做-->
        <attr name="enable_when_full" format="boolean"/>
    </declare-styleable>
</resources>

控件的本質 - SlideView的實現

SlideView能夠說是SlideIcon的父view,是一個自定義ViewGroup。它主要的工做是測量控件的尺寸、根據觸摸事件的回調實時地計算子view的佈局、繪製控件背景圖和背景文字。

具體代碼以下:

public class SlideView extends ViewGroup {

    private static final String TAG = "SlideView";

    // SlideIcon在父view中的水平偏移量
    private static int MARGIN_HORIZONTAL = 4;

    // SlideIcon在父view中的水平便宜量
    private static int MARGIN_VERTICAL = 4;

    // SlideIcon實例
    private SlideIcon mSlideIcon;

    // SlideIcon的X座標
    private int mIconX = 0;

    // SlideIcon拖動時的X軸偏移量
    private int mDistanceX = 0;

    // 監聽
    private MotionListener mMotionListener = null;

    // 背景文字的Paint
    private Paint mBgTextPaint;

    // 背景文字的測量類
    private Paint.FontMetrics mBgTextFontMetrics;

    // 拖動過的部分的Paint
    private Paint mSecondaryPaint;

    // attr: 最小高度
    private int mMinHeight;

    // attr: 背景圖
    private int mBgResId;

    // attr: 背景文字
    private String mBgText = "";

    // attr: 拖動完成後的背景文字
    private String mBgTextComplete = "";

    // attr: 背景文字的顏色
    private int mBgTextColor;

    // attr: 背景文字的大小
    private float mBgTextSize;

    // attr: Icon背景圖
    private int mIconResId;

    // attr: Icon上顯示的文字
    private String mIconText = "";

    // attr: Icon上文字的顏色
    private int mIconTextColor;

    // attr: Icon上文字的大小
    private float mIconTextSize;

    // attr: Icon的寬度佔總長的比例
    private float mIconRatio;

    // attr: 滑動到一半鬆手時是否回到初始狀態
    private boolean mResetWhenNotFull;

    // attr: 拖動結束後是否能夠再次操做
    private boolean mEnableWhenFull;

    // attr: 拖動過的部分的顏色
    private int mSecondaryColor;

    private OnSlideListener mListener = null;

    // 控件滑動的回調
    public interface OnSlideListener {
        /**
         * 滑動完成的回調
         */
        void onSlideSuccess();
    }

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

    public SlideView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SlideView, 0, 0);
        try {
            mResetWhenNotFull = a.getBoolean(R.styleable.SlideView_reset_not_full, true);
            mEnableWhenFull = a.getBoolean(R.styleable.SlideView_enable_when_full, false);

            mBgResId = a.getResourceId(R.styleable.SlideView_bg_drawable, R.mipmap.ic_launcher);
            mIconResId = a.getResourceId(R.styleable.SlideView_icon_drawable, R.mipmap.ic_launcher);
            mMinHeight = a.getDimensionPixelSize(R.styleable.SlideView_min_height, 240);

            mIconText = a.getString(R.styleable.SlideView_icon_text);
            mIconTextColor = a.getColor(R.styleable.SlideView_icon_text_color, Color.WHITE);
            mIconTextSize = a.getDimensionPixelSize(R.styleable.SlideView_icon_text_size, 44);
            mIconRatio = a.getFloat(R.styleable.SlideView_icon_ratio, 0.2f);

            mBgText = a.getString(R.styleable.SlideView_bg_text);
            mBgTextComplete = a.getString(R.styleable.SlideView_bg_text_complete);
            mBgTextColor = a.getColor(R.styleable.SlideView_bg_text_color, Color.BLACK);
            mBgTextSize = a.getDimensionPixelSize(R.styleable.SlideView_bg_text_size, 44);

            mSecondaryColor = a.getColor(R.styleable.SlideView_secondary_color, Color.TRANSPARENT);
        } finally {
            a.recycle();
        }
        init();
    }

    private void init() {
        // 設置背景文字Paint
        mBgTextPaint = new Paint();
        mBgTextPaint.setTextAlign(Paint.Align.CENTER);
        mBgTextPaint.setColor(mBgTextColor);
        mBgTextPaint.setTextSize(mBgTextSize);

        // 獲取背景文字測量類
        mBgTextFontMetrics = mBgTextPaint.getFontMetrics();

        // 設置拖動過的部分的Paint
        mSecondaryPaint = new Paint();
        mSecondaryPaint.setColor(mSecondaryColor);

        // 設置背景圖
        setBackgroundResource(mBgResId);

        // 建立一個SlideIcon,設置LayoutParams並添加到ViewGroup中
        mSlideIcon = new SlideIcon(getContext());
        /**
         * Important:
         * 此處須要設置IconView的LayoutParams,這樣才能在佈局文件中正確經過wrap_content設置佈局
         */
        mSlideIcon.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
        addView(mSlideIcon);

        // 設置監聽
        mMotionListener = new MotionListener() {
            @Override
            public void onActionMove(int distanceX) {
                // SlideIcon拖動時根據X軸偏移量從新計算位置並繪製
                if (mSlideIcon != null) {
                    mDistanceX = distanceX;
                    requestLayout();
                    invalidate();
                }
            }

            @Override
            public void onActionUp(int x) {
                mIconX = x;
                mDistanceX = 0;
                if (mIconX + mSlideIcon.getMeasuredWidth() < getMeasuredWidth()) {  // SlideIcon爲拖動到底
                    if (mResetWhenNotFull) {  // 重置
                        mIconX = 0;
                        mSlideIcon.resetIcon();
                        requestLayout();
                        invalidate();
                    }
                } else {  // SlideIcon拖動到底
                    if (!mEnableWhenFull) {  // 鬆開後是否能夠繼續操做
                        mSlideIcon.setEnable(false);
                    }
                    if (mListener != null) {  // 觸發回調
                        mListener.onSlideSuccess();
                    }
                }
            }
        };

        mSlideIcon.setListener(mMotionListener);
    }

    /**
     * 添加滑動完成監聽
     */
    public void addSlideListener(OnSlideListener listener) {
        this.mListener = listener;
    }

    /**
     * 重置SlideView
     */
    public void reset() {
        mIconX = 0;
        mDistanceX = 0;
        if (mSlideIcon != null) {
            mSlideIcon.resetIcon();
        }
        requestLayout();
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 計算子View的尺寸
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        // 由於只有一個子View,直接取出來
        mSlideIcon = (SlideIcon) getChildAt(0);
        // 根據SlideIcon的高度設置ViewGroup的高度
        setMeasuredDimension(widthMeasureSpec, mSlideIcon.getMeasuredHeight());
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (mIconX + mDistanceX <= 0) { // 控制SlideIcon不能超過左邊界限
            mSlideIcon.layout(MARGIN_HORIZONTAL, MARGIN_VERTICAL,
                    MARGIN_HORIZONTAL + mSlideIcon.getMeasuredWidth(),
                    mSlideIcon.getMeasuredHeight() - MARGIN_VERTICAL);
        } else if (mIconX + mDistanceX + mSlideIcon.getMeasuredWidth() >= getMeasuredWidth()) { // 控制SlideIcon不能超過左邊界限
            mSlideIcon.layout(getMeasuredWidth() - mSlideIcon.getMeasuredWidth() - MARGIN_HORIZONTAL, MARGIN_VERTICAL,
                    getMeasuredWidth() - MARGIN_HORIZONTAL,
                    mSlideIcon.getMeasuredHeight() - MARGIN_VERTICAL);
        } else {  // 根據SlideIcon的X座標和偏移量計算位置
            mSlideIcon.layout(mIconX + mDistanceX + MARGIN_HORIZONTAL, MARGIN_VERTICAL,
                    mIconX + mDistanceX + mSlideIcon.getMeasuredWidth() + MARGIN_HORIZONTAL,
                    mSlideIcon.getMeasuredHeight() - MARGIN_VERTICAL);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 繪製已拖動過的區域
        if (mIconX + mDistanceX > 0) {
            canvas.drawRect(MARGIN_HORIZONTAL, MARGIN_VERTICAL, mIconX + mDistanceX + MARGIN_HORIZONTAL,
                    getMeasuredHeight() - MARGIN_VERTICAL, mSecondaryPaint);
        }

        // 繪製背景文字
        float baselineY = (getMeasuredHeight() - mBgTextFontMetrics.top - mBgTextFontMetrics.bottom) / 2;
        if (mIconX + mDistanceX + mSlideIcon.getMeasuredWidth() >= getMeasuredWidth()) {
            canvas.drawText(mBgTextComplete == null ? "":mBgTextComplete, getMeasuredWidth() / 2, baselineY, mBgTextPaint);
        } else {
            canvas.drawText(mBgText == null ? "":mBgText, getMeasuredWidth() / 2, baselineY, mBgTextPaint);
        }
    }
}

須要注意的是咱們在添加SlideIcon的時候,須要將SlideIcon的LayoutParams設置爲(WRAP_CONTENT,WRAP_CONTENT),這樣咱們才能在SlideIcon的onMeasure中正確地獲取到heightMode爲wrap_content的狀況,從而正確地計算控件的高度。

另外值得說明的一點是,SlideView是根據SlideIcon的X座標+X軸滑動的距離之和是否超出控件的右邊距來判斷是否滑動完成,同時在手指鬆開的時候(onActionUp)觸發滑動完成的回調OnSlideListener。固然也能夠在手指未鬆開滑動的動做中(onActionMove)進行檢測從而觸發回調,這個在代碼中稍做修改就能實現。

使用方式

到目前爲止,咱們的SlideView就已經完成了。下面讓咱們看看使用的效果

Layout佈局文件 - activity_slide_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_slide_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <com.gnepux.sdkusage.widget.SlideView
        android:id="@+id/slideview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        app:bg_drawable="@drawable/bg_slideview"
        app:bg_text="滑動解鎖"
        app:bg_text_complete="解鎖成功"
        app:bg_text_color="#0000ff"
        app:bg_text_size="22sp"
        app:icon_drawable="@drawable/icon_slideview"
        app:icon_text="滑"
        app:icon_text_color="@android:color/white"
        app:icon_text_size="20sp"
        app:icon_ratio="0.2"
        app:secondary_color="#00ff00"
        app:min_height="60dp"
        app:reset_not_full="true"
        app:enable_when_full="false"/>
    
</LinearLayout>

SlideView的背景圖 - bg_slideview.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="0dp"/>
    <solid android:color="@android:color/white"/>
    <stroke android:color="#0000ff" android:width="2dp"/>
</shape>

SlideIcon的背景圖 - bg_slideicon.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="0dp"/>
    <solid android:color="#ff0000"/>
    <stroke android:color="#ffffff" android:width="1dp"/>
</shape>

​

SlideViewActivity.java

public class SlideViewActivity extends AppCompatActivity {
    
    private SlideView mSlideView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_slide_view);
        mSlideView = (SlideView) findViewById(R.id.slideview);
        mSlideView.addSlideListener(new SlideView.OnSlideListener() {
            @Override
            public void onSlideSuccess() {
                Toast.makeText(SlideViewActivity.this, "解鎖成功!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

Code

完整代碼能夠參考: https://github.com/Gnepux/SlideView

Snapshot

寫在最後

SlideView和SlideIcon是比較典型的自定義ViewGroup和View。實現的流程也採用常規的自定義控件的方式:測量、佈局、繪製,再加上相應的邏輯控制。

有關Android自定義View能夠參考,http://www.javashuo.com/article/p-bnjbkrpi-bo.html

自定義ViewGroup能夠參考,http://www.javashuo.com/article/p-yrdxsidc-bu.html

相關文章
相關標籤/搜索