目前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