當前位置:
首頁 > 知識 > 教你編寫一個手勢解鎖控制項

教你編寫一個手勢解鎖控制項

前言

最近學習了一些自定義控制項的知識,想著趁熱多做些練習來鞏固,上周自定義了一個等級進度條,是一個自定義View,這周就換一個類型,做一個自定義的ViewGroup。這周自定義ViewGroup的是一個鎖屏控制項,效果如下:

正文

效果分析

仔細分析效果圖發現,鎖屏控制項需要繪製的有三個部分,分別是:

  • 圖案點,圖案點有四種狀態,分別是默認、選中、正確和錯誤

  • 圖案點之間的連線

連線會根據1中點的狀態改變發生顏色上的變化

  • 懸空線段

就是圖案點和懸空點之間的線段

整體思路

  1. 自定義一個LockScreenView來表示圖案點,LockScreenView有四種狀態
  2. 自定義一個LockScreenViewGroup,在onMeasure中獲取到寬度以後(根據寬度算圖案點之間的間距),動態地將LockScreenView添加進來
  3. 在LockScreenViewGroup的onTouchEvent中消耗觸摸事件,根據觸摸點的軌跡來更新LockScreenView、圖案點連線和懸空線段

實現

  • 自定義LockScreenView

由於沒有和這個自定義View比較類似的原生控制項,因此自定義的時候直接繼承自View。首先,需要的屬性通過構造函數傳入:

private int smallRadius; // LockScreenView小圈的半徑
private int bigRadius; // LockScreenView中大圓圈的半徑
private int normalColor; // LockScreenView中默認的顏色
private int rightColor; // LockScreenView中圖形碼正確時的顏色
private int wrongColor; // LockScreenView中圖形碼錯誤時的顏色

public LockScreenView(Context context, int normalColor, int smallRadius, int bigRadius, int rightColor, int wrongColor)

View的狀態用一個枚舉類型來表示

enum State { // 四種狀態,分別是正常狀態、選中狀態、結果正確狀態、結果錯誤狀態
STATE_NORMAL, STATE_CHOOSED, STATE_RESULT_RIGHT, STATE_RESULT_WRONG
}

View的狀態通過暴露一個方法給LockScreenViewGroup來進行設置。在onDraw方法中判斷類型,進行繪製:

@Override
protected void onDraw(Canvas canvas) {
switch(mCurrentState) {
case STATE_NORMAL:
//
break;
case STATE_CHOOSED:
//
break;
case STATE_RESULT_RIGHT:
//
break;
case STATE_RESULT_WRONG:
//
break;
}
}

這裡在選中時用屬性動畫做了一個放大效果,在下次恢復正常的時候要將大小恢復回去:

private void zoomOut() {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "scaleX", 1, 1.2f);
animatorX.setDuration(50);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "scaleY", 1, 1.2f);
animatorY.setDuration(50);
AnimatorSet set = new AnimatorSet();
set.playTogether(animatorX, animatorY);
set.start();
needZoomIn = true;
}

private void zoomIn() {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "scaleX", 1, 1f);
animatorX.setDuration(0);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "scaleY", 1, 1f);
animatorY.setDuration(0);
AnimatorSet set = new AnimatorSet();
set.playTogether(animatorX, animatorY);
set.start();
needZoomIn = false;
}

在LockScreenViewGroup中,我將LockScreenView的寬高設置為wrap_content,因此需要在onMeasure方法做一些特殊的處理,至於為什麼要做特殊處理,在上一篇博文《等級進度條》中已經提到過了。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);

if (widthMode == MeasureSpec.AT_MOST) {
widthSize = (int) Math.round(bigRadius*2);
}
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = (int) Math.round(bigRadius*2);
}
setMeasuredDimension(widthSize, heightSize);
}

  • 自定義LockScreenViewGroup

為了方便確定子View的位置,LockScreenViewGroup繼承自RelativeLayout。在xml中賦予了如下屬性:

<declare-styleable name="LockScreenViewGroup">
<attr name="itemCount" format="integer"/>
<attr name="smallRadius" format="dimension"/>
<attr name="bigRadius" format="dimension"/>
<attr name="normalColor" format="color"/>
<attr name="rightColor" format="color"/>
<attr name="wrongColor" format="color"/>
</declare-styleable>

其中itemCount表示一行有幾個LockScreenView,其它屬性都已經提到過了。在構造函數中解析xml中的自定義屬性:

public LockScreenViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

// 從xml中獲取自定義屬性
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.LockScreenViewGroup);
itemCount = array.getInt(R.styleable.LockScreenViewGroup_itemCount, 3);
smallRadius = (int) array.getDimension(R.styleable.LockScreenViewGroup_smallRadius, 20);
bigRadius = (int) array.getDimension(R.styleable.LockScreenViewGroup_bigRadius, 2);
normalColor = array.getInt(R.styleable.LockScreenViewGroup_normalColor, 0xffffff);
rightColor = array.getColor(R.styleable.LockScreenViewGroup_rightColor, 0x00ff00);
wrongColor = array.getColor(R.styleable.LockScreenViewGroup_wrongColor, 0x0000ff);

array.recycle();

在onMeasure方法中,獲取到LockScreenViewGroup的寬以後,算出LockScreenView之間的間隙,並動態地將LockScreenView添加進來(每個LockScreenView添加進來的時候,設置id作為唯一標識,後面在判斷圖案是否正確時會用到):

// 動態添加LockScreenView
if (lockScreenViews == null) {
lockScreenViews = new LockScreenView[itemCount * itemCount];
for (int i = 0; i < itemCount * itemCount; i++) {
lockScreenViews[i] = new LockScreenView(getContext(), normalColor, smallRadius, bigRadius,
rightColor, wrongColor);
lockScreenViews[i].setId(i + 1);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
);
// 這裡不能通過lockScreenViews[i].getMeasuredWidth()來獲取寬高,因為這時它的寬高還沒有測量出來
int marginWidth = (getMeasuredWidth() - bigRadius * 2 * itemCount) / (itemCount + 1);

// 除了第一行以外,其它的View都在在某個LockScreenView的下面
if (i >= itemCount) {
params.addRule(BELOW, lockScreenViews[i - itemCount].getId());
}

// 除了第一列以外,其它的View都在某個LockScreenView的右邊
if (i % itemCount != 0) {
params.addRule(RIGHT_OF, lockScreenViews[i - 1].getId());
}

// 為LockScreenView設置margin
int left = marginWidth;
int top = marginWidth;
int bottom = 0;
int right = 0;
params.setMargins(left, top, right, bottom);
lockScreenViews[i].setmCurrentState(LockScreenView.State.STATE_NORMAL);
addView(lockScreenViews[i], params);
}
}

這裡有兩個地方需要注意一下:

  1. LockScreenView的寬不能用getMeasuredWidth方法來獲取,因為這裡只是把LockScreenView創建了出來,還沒有對它進行測量,故通過getMeasuredWidth方法只能得到0,這裡直接把LockScreenView中大圓的直徑當作它的寬(因為這裡動態添加的時候用了wrap_content, 並且沒有設padding)
  2. 重寫onMeasure方法的時候不能把super.onMeasure方法刪掉,因為這裡面會進行子View寬高的測量,刪了子View就畫不出來了

觸摸事件的消耗在onTouchEvent中處理(在這個案例中也可以在dispatchTouchEvent方法中處理,因為子View的狀態由LockScreenViewGroup告訴它了,子View不需要處理觸摸事件)。在onTouchEvent方法中對Down、Move、Up三種不同的觸摸狀態分別做了處理。

首先,在Down狀態時,需要對之前的狀態做一些重置:

private void resetView() {
if (mCurrentViews.size() > 0) {
mCurrentViews.clear();
}
if (!mCurrentPath.isEmpty()) {
mCurrentPath.reset();
}

// 重置LockScreenView的狀態
for (int i = 0; i < itemCount * itemCount; i++) {
lockScreenViews[i].setmCurrentState(LockScreenView.State.STATE_NORMAL);
}

skyStartX = -1;
skyStartY = -1;
}

其中,mCurrentViews用來保存當前選中的LockScreenView的id,mCurrentPath用來保存圖像點間線段的路徑,skyStartX、skyStartY分別是懸空線段起始的x和y。

在Move狀態時,判斷是否在LockScreenView區域,如果在某個LockScreenView區域且這個LockScreenView之前沒有被選中,則將這個LockScreenView設置為選中狀態。另外在onMove中還做了圖案點間線段路徑和懸空線段起點和終點(mTempX、mTempY)的更新,懸空線段的起點就是上一個被選中的LockScreenView的中心點。

case MotionEvent.ACTION_MOVE:
mPaint.setColor(normalColor);
LockScreenView view = findLockScreenView(x, y);
if (view != null) {
int id = view.getId();
// 當前LockScreenView不在選中列表中時,將其添加到列表中,並設置其狀態為選中
if (!mCurrentViews.contains(id)) {
mCurrentViews.add(id);
view.setmCurrentState(LockScreenView.State.STATE_CHOOSED);
skyStartX = (view.getLeft() + view.getRight()) / 2;
skyStartY = (view.getTop() + view.getBottom()) / 2;

// path中線段的添加
if (mCurrentViews.size() == 1) {
mCurrentPath.moveTo(skyStartX, skyStartY);
} else {
mCurrentPath.lineTo(skyStartX, skyStartY);
}
}
}
// 懸空線段末端的更新
mTempX = x;
mTempY = y;
break;

在Up狀態時,根據答案的正確與否,對LockScreenView設置不同的狀態,並且對懸空線段起始點進行重置。

case MotionEvent.ACTION_UP:
// 根據圖案正確與否,對LockScreenView設置不同的狀態
if (checkAnswer()) {
setmCurrentViewsState(LockScreenView.State.STATE_RESULT_RIGHT);
mPaint.setColor(rightColor);
} else {
setmCurrentViewsState(LockScreenView.State.STATE_RESULT_WRONG);
mPaint.setColor(wrongColor);
}
// 抬起手指後對懸空線段的起始點進行重置
skyStartX = -1;
skyStartY = -1;

在onTouchEvent方法最後會調用invalidate方法對視圖進行重繪,這時會調用dispatchDraw方法進行子View的繪製。

在dispatchDraw方法中進行圖像點間的線段路徑以及懸空線段的繪製:

@Override
protected void dispatchDraw(Canvas canvas) {
// 進行子View的繪製
super.dispatchDraw(canvas);

// path線段的繪製
if (!mCurrentPath.isEmpty()) {
canvas.drawPath(mCurrentPath, mPaint);
}

// 懸空線段的繪製
if (skyStartX != -1) {
canvas.drawLine(skyStartX, skyStartY, mTempX, mTempY, mPaint);
}
}

這裡要注意,在重寫dispatchDraw方法時,不能把super.dispatchDraw方法刪掉,因為這裡會繪製LockScreenViewGroup的子View(即,LockScreenView們),如果刪了,動態添加的LockScreenView就會顯示不出來(重寫的時候不小心刪了,排查好久才發現是這裡的問題,都是淚orz)

總結

文章到這裡就結束了。最後,奉上源碼地址:


https://github.com/shonnybing/LockScreenView

教你編寫一個手勢解鎖控制項

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

TensorFlow中的Eager Execution和自動微分
藉助Jackson的JsonTypeInfo註解實現多態類的解析

TAG:程序員小新人學習 |