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

相关文章
相关标签/搜索