文档章节

Android自定义滑动确认控件SlideView

Gnepux
 Gnepux
发布于 2017/01/02 03:38
字数 2902
阅读 898
收藏 3

项目GitHub

 https://github.com/Gnepux/SlideView

前言

目前App上有很多对于按钮误操作的控制。比如点击按钮后弹出确认框,但是这样的模式略显死板。为了给App赋予更多的生命力,可以借鉴网站登录滑动确认的方式。这种方式目前更多地用于web登录。以下是某网站登录时使用的滑动验证,用来取代以往的验证码模式。

我们的App可以借鉴上述的模式自定义一个滑动确认的控件,可以用于控制误操作点击的场景。当然其他的应用场景可以等待大家细细挖掘。

设计思路

我们就暂且命名这个自定义的滑动控件叫SlideView

先来总结一下SlideView的主要功能:“按住一个进度条里的按钮往右滑,如果滑到一般松开按钮自动回到原位,如果滑到底则给出完成提示”。

貌似就是这么一句话的事。当然还有更多属性设置,比如背景的文字、颜色、滑动按钮和进度条的比例等等。

通过前面一句话的介绍,是不是让我们想起了Andoird的SeekBar控件?确实重写SeekBar控件的确可以实现我们想要实现的功能,但可定制稍微差了些,所以决定重头开始构建SlideView。

方案

既然要重构构建SlideView,那我们就要实现一个自定义的ViewGroup。添加背景图和提示文字。之后再将可以拖动的按钮加入到这个ViewGroup中。那个所谓“可以拖动的按钮”我们就叫它SlideIcon,这是一个自定义View。也可以添加背景图和提示文字,控制它的宽度与SildeView总宽度的比例,最后为这个View加上触摸事件,按下之后可以拖动,拖动到一般松开回到起点,拖到底触发一个完成的回调。

总体方案就是这样,是不是很简单,下面让我们来一步步实现这个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可以参考,https://my.oschina.net/u/3026396/blog/812942

自定义ViewGroup可以参考,https://my.oschina.net/u/3026396/blog/813027

© 著作权归作者所有

Gnepux
粉丝 3
博文 96
码字总数 77557
作品 0
南京
私信 提问
高仿微信对话列表滑动删除效果

转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/17515543 前言 用过微信的都知道,微信对话列表滑动删除效果是很不错的,这个效果我们也可以有。思路其实很简单,弄个...

长平狐
2013/12/25
6.9K
1
关于高仿微信对话列表滑动删除效果代码优化

原文:http://blog.csdn.net/singwhatiwanna/article/details/17515543 最近公司项目需用到微信滑动拉出按钮的效果,发现一位牛人已经实现了相关效果,但控件仍与业务代码存有耦合,于是花了...

FutureListener
2014/07/22
1K
0
一个App完成入门篇(五)- 完成新闻页面

本节教程将介绍如何用DeviceOne简单而高效的完成一个新闻页面。 导入项目 数据模板分离MVVM模型 自定义事件 展示新闻 九宫格展示 将要学习的demo效果图如下所示 1. 导入完整项目 本节示例dem...

jonh_felix
2016/04/07
1K
0
[DeviceOne开发]-轮播图和多模板的简单示例

一、简介 这个例子是利用Slideview组件实现循环轮播的效果,同时这个slideview作为一个listview的最上面的一行数, 1. listview有2个模板,一个是以slideview为核心的模板,一个是普通的cel...

DoProject
2016/10/09
100
0
android 仿猿题库答题UI

需要做类似猿题库答题板效果,网上没有找到Android的,就自己写一个,浮层上滑底层UI跟着改变的UI public class DragLinearLayoutextends LinearLayout { private int screenWidth; private...

天涯_a67d
2018/05/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

前端面试题汇总

一. HTML常见的兼容性 1.HTML5 标签在低版本浏览器不兼容 解决办法:使用html5shiv库,引入下列语句 <!--[if lte IE 8]> <script src="https://cdn.bootcss.com/html5shiv/r29/html5.js"></sc......

蓝小驴
38分钟前
10
0
OSChina 周四乱弹 —— 我气的脸都黑了!

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 小小编辑推荐《Red Battle》- 高橋李依 / 豊崎愛生 《Red Battle》- 高橋李依 / 豊崎愛生 手机党少年们想听歌,请使劲儿戳(这里) @丶Lion ...

小小编辑
51分钟前
653
23
找OSG教程, B站就有

https://www.bilibili.com/video/av64849038?from=search&seid=11632913960900279653

洛克人杰洛
今天
6
0
学习记录(day07-Vue组件、自定义属性、自定义事件)

[TOC] 1.1.1什么是组件 一个vue文件就是一个组件 组件将html标签/css样式/对应JS打包成一个整体,也可以理解钻进一个具有样式和特效的自定义标签。 一、编写组件(提供方)<template> <di...

庭前云落
今天
5
0
使用Prometheus监控SpringBoot应用

通过之前的文章我们使用Prometheus监控了应用服务器node_exporter,数据库mysqld_exporter,今天我们来监控一下你的应用。(本文以SpringBoot 2.1.9.RELEASE 作为监控目标) 编码 添加依赖 使...

JAVA日知录
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部