目前App上有不少对于按钮误操做的控制。好比点击按钮后弹出确认框,可是这样的模式略显死板。为了给App赋予更多的生命力,能够借鉴网站登陆滑动确认的方式。这种方式目前更多地用于web登陆。如下是某网站登陆时使用的滑动验证,用来取代以往的验证码模式。android
咱们的App能够借鉴上述的模式自定义一个滑动确认的控件,能够用于控制误操做点击的场景。固然其余的应用场景能够等待你们细细挖掘。git
咱们就暂且命名这个自定义的滑动控件叫SlideView。github
先来总结一下SlideView的主要功能:“按住一个进度条里的按钮往右滑,若是滑到通常松开按钮自动回到原位,若是滑到底则给出完成提示”。web
貌似就是这么一句话的事。固然还有更多属性设置,好比背景的文字、颜色、滑动按钮和进度条的比例等等。canvas
经过前面一句话的介绍,是否是让咱们想起了Andoird的SeekBar控件?确实重写SeekBar控件的确能够实现咱们想要实现的功能,但可定制稍微差了些,因此决定重头开始构建SlideView。app
既然要重构构建SlideView,那咱们就要实现一个自定义的ViewGroup。添加背景图和提示文字。以后再将能够拖动的按钮加入到这个ViewGroup中。那个所谓“能够拖动的按钮”咱们就叫它SlideIcon,这是一个自定义View。也能够添加背景图和提示文字,控制它的宽度与SildeView总宽度的比例,最后为这个View加上触摸事件,按下以后能够拖动,拖动到通常松开回到起点,拖到底触发一个完成的回调。ide
整体方案就是这样,是否是很简单,下面让咱们来一步步实现这个SlideView。布局
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为调用者提供多种可定制属性必不可少。具体的提供的属性以下:
<?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能够说是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(); } }); } }
完整代码能够参考: https://github.com/Gnepux/SlideView
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