首先來介紹一下這個自定義View:
- (1)這個自定義View的名稱叫做 LockView ,繼承自View類;
- (2)這個自定義View實現了應用中常見的九宮格手勢解鎖功能,可以用於保證應用安全;
- (3)用戶可以自定義控件在不同狀態下顯示的顏色、什么情況算解鎖成功、解鎖成功或失敗回調的方法等。
接下來介紹一下在這個自定義View中用到的技術點:
- (1)自定義屬性;
- (2)在 onMeasure() 方法中對控件進行測量,保證九宮格顯示在屏幕的中央;
- (3)在 onDraw() 方法中根據用戶的手勢繪制圓圈和連線;
- (4)在 onTouchEvent() 方法中接收用戶的觸摸事件並進行相應的處理;
- (5)將判斷解鎖是否成功以及解鎖成功或失敗的情況下回調的方法抽取成接口,供用戶自定義。
下面是這個自定義View—— LockView 的實現代碼:
自定義View類 LockView.java 中的代碼:
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; /** * 自定義手勢解鎖控件 * Created by ITGungnir on 2017/5/1. */ public class LockView extends View { // 狀態常量 private static final int STATE_NORMAL = 0x001; // 默認狀態 private static final int STATE_SELECT = 0x002; // 選中狀態 private static final int STATE_CORRECT = 0x003; // 正確狀態 private static final int STATE_WRONG = 0x004; // 錯誤狀態 // 自定義屬性 private int normalColor = Color.GRAY; // 默認顯示的顏色 private int selectColor = Color.YELLOW; // 選中時顯示的顏色 private int correctColor = Color.GREEN; // 正確時顯示的顏色 private int wrongColor = Color.RED; // 錯誤時顯示的顏色 private int lineWidth = -1; // 連線的寬度 // 寬高相關 private int width; // 父布局分配給這個View的寬度 private int height; // 父布局分配給這個View的高度 private int rectRadius; // 每個小圓圈的寬度(直徑) // 元素相關 private List<CircleRect> rectList; // 存儲所有圓圈對象的列表 private List<CircleRect> pathList; // 存儲用戶繪制的連線上的所有圓圈對象 // 繪制相關 private Canvas mCanvas; // 用於繪制元素的畫布 private Bitmap mBitmap; // 用戶繪制元素的Bitmap private Path mPath; // 用戶繪制的線條 private Path tmpPath; // 記錄用戶以前繪制過的線條 private Paint circlePaint; // 用戶繪制圓圈的畫筆 private Paint pathPaint; // 用戶繪制連線的畫筆 // 觸摸相關 private int startX; // 上一個節點的X坐標 private int startY; // 上一個節點的Y坐標 private boolean isUnlocking; // 是否正在解鎖(手指落下時是否剛好在一個節點上) // 結果相關 private OnUnlockListener listener; public LockView(Context context) { this(context, null); } public LockView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LockView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 初始化一些對象(List等) rectList = new ArrayList<>(); pathList = new ArrayList<>(); // 獲取自定義屬性 TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LockView, defStyleAttr, 0); int count = array.getIndexCount(); for (int i = 0; i < count; i++) { int attr = array.getIndex(i); switch (attr) { case R.styleable.LockView_normalColor: normalColor = array.getColor(attr, Color.GRAY); break; case R.styleable.LockView_selectColor: selectColor = array.getColor(attr, Color.YELLOW); break; case R.styleable.LockView_correctColor: correctColor = array.getColor(attr, Color.GREEN); break; case R.styleable.LockView_wrongColor: wrongColor = array.getColor(attr, Color.RED); break; case R.styleable.LockView_lineWidth: lineWidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, context.getResources().getDisplayMetrics())); break; } } if (lineWidth == -1) { lineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, context.getResources().getDisplayMetrics()); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 獲取到控件的寬高屬性值 width = getMeasuredWidth(); height = getMeasuredHeight(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 初始化繪制相關的元素 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); circlePaint = new Paint(); circlePaint.setAntiAlias(true); circlePaint.setDither(true); mPath = new Path(); tmpPath = new Path(); pathPaint = new Paint(); pathPaint.setDither(true); pathPaint.setAntiAlias(true); pathPaint.setStyle(Paint.Style.STROKE); pathPaint.setStrokeCap(Paint.Cap.ROUND); pathPaint.setStrokeJoin(Paint.Join.ROUND); pathPaint.setStrokeWidth(lineWidth); // 初始化一些寬高屬性 int horizontalSpacing; int verticalSpacing; if (width <= height) { horizontalSpacing = 0; verticalSpacing = (height - width) / 2; rectRadius = width / 14; } else { horizontalSpacing = (width - height) / 2; verticalSpacing = 0; rectRadius = height / 14; } // 初始化所有CircleRect對象 for (int i = 1; i <= 9; i++) { int x = ((i - 1) % 3 * 2 + 1) * rectRadius * 2 + horizontalSpacing + getPaddingLeft() + rectRadius; int y = ((i - 1) / 3 * 2 + 1) * rectRadius * 2 + verticalSpacing + getPaddingTop() + rectRadius; CircleRect rect = new CircleRect(i, x, y, STATE_NORMAL); rectList.add(rect); } } @Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, 0, 0, null); for (int i = 0; i < rectList.size(); i++) { drawCircle(rectList.get(i), rectList.get(i).getState()); } canvas.drawPath(mPath, pathPaint); } @Override public boolean onTouchEvent(MotionEvent event) { int currX = (int) event.getX(); int currY = (int) event.getY(); CircleRect rect = getOuterRect(currX, currY); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 保證手指按下后所有元素都是初始狀態 this.reset(); // 判斷手指落點是否在某個圓圈中,如果是則設置該圓圈為選中狀態 if (rect != null) { rect.setState(STATE_SELECT); startX = rect.getX(); startY = rect.getY(); tmpPath.moveTo(startX, startY); pathList.add(rect); isUnlocking = true; } break; case MotionEvent.ACTION_MOVE: if (isUnlocking) { mPath.reset(); mPath.addPath(tmpPath); mPath.moveTo(startX, startY); mPath.lineTo(currX, currY); if (rect != null) { rect.setState(STATE_SELECT); startX = rect.getX(); startY = rect.getY(); tmpPath.lineTo(startX, startY); pathList.add(rect); } } break; case MotionEvent.ACTION_UP: isUnlocking = false; if (pathList.size() > 0) { mPath.reset(); mPath.addPath(tmpPath); StringBuilder result = new StringBuilder(); for (int i = 0; i < pathList.size(); i++) { result.append(pathList.get(i).getCode()); } if (listener.isUnlockSuccess(result.toString())) { listener.onSuccess(); setWholePathState(STATE_CORRECT); } else { listener.onFailure(); setWholePathState(STATE_WRONG); } } break; } invalidate(); return true; } /** * 根據狀態(解鎖成功/失敗)改變整條路徑上所有元素的顏色 * * @param state 狀態(解鎖成功/失敗) */ private void setWholePathState(int state) { pathPaint.setColor(getColorByState(state)); for (CircleRect rect : pathList) { rect.setState(state); } } /** * 通過狀態得到應顯示的顏色 * * @param state 狀態 * @return 給定狀態下應該顯示的顏色 */ private int getColorByState(int state) { int color = normalColor; switch (state) { case STATE_NORMAL: color = normalColor; break; case STATE_SELECT: color = selectColor; break; case STATE_CORRECT: color = correctColor; break; case STATE_WRONG: color = wrongColor; break; } return color; } /** * 根據參數中提供的圓圈參數繪制圓圈 * * @param rect 存儲圓圈所有參數的CircleRect對象 * @param state 圓圈的當前狀態 */ private void drawCircle(CircleRect rect, int state) { circlePaint.setColor(getColorByState(state)); mCanvas.drawCircle(rect.getX(), rect.getY(), rectRadius, circlePaint); } /** * 判斷參數中的x、y坐標對應的點是否在某個圓圈內,如果在則返回這個圓圈,否則返回null * * @param x 給定的點的X坐標 * @param y 給定的點的Y坐標 * @return 給定點所在的圓圈對象,如果不在任何一個圓圈內則返回null */ private CircleRect getOuterRect(int x, int y) { for (int i = 0; i < rectList.size(); i++) { CircleRect rect = rectList.get(i); if ((x - rect.getX()) * (x - rect.getX()) + (y - rect.getY()) * (y - rect.getY()) <= rectRadius * rectRadius) { if (rect.getState() != STATE_SELECT) { return rect; } } } return null; } /** * 解鎖,手指抬起后回調的借口 */ interface OnUnlockListener { // 由用戶來判斷解鎖是否成功 boolean isUnlockSuccess(String result); // 當解鎖成功時回調的方法 void onSuccess(); // 當解鎖失敗時回調的方法 void onFailure(); } /** * 為當前View設置結果監聽器 */ public void setOnUnlockListener(OnUnlockListener listener) { this.listener = listener; } /** * 重置所有元素的狀態到初始狀態 */ public void reset() { setWholePathState(STATE_NORMAL); pathPaint.setColor(selectColor); mPath.reset(); tmpPath.reset(); pathList = new ArrayList<>(); } }
自定義屬性文件 res/values/attr.xml 中的代碼:
<resources> <attr name="normalColor" format="color" /> <!-- 正常狀態下圓圈的顏色 --> <attr name="selectColor" format="color" /> <!-- 選中狀態下圓圈的顏色 --> <attr name="correctColor" format="color" /> <!-- 正確狀態下圓圈的顏色 --> <attr name="wrongColor" format="color" /> <!-- 錯誤狀態下圓圈的顏色 --> <attr name="lineWidth" format="dimension" /> <!-- 連線的寬度 --> <declare-styleable name="LockView"> <attr name="normalColor" /> <attr name="selectColor" /> <attr name="correctColor" /> <attr name="wrongColor" /> <attr name="lineWidth" /> </declare-styleable> </resources>
存儲每個圓圈屬性的實體類 CircleRect.java 中的代碼:
/** * 存儲圓圈的各種屬性的實體類 * Created by ITGungnir on 2017/5/1. */ public class CircleRect { // 圓圈所代表的數字(1~9) private int code; // 圓心的X坐標 private int x; // 圓心的Y坐標 private int y; // 圓圈的當前狀態 private int state; public CircleRect() { } public CircleRect(int code, int x, int y, int state) { this.code = code; this.x = x; this.y = y; this.state = state; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getState() { return state; } public void setState(int state) { this.state = state; } }
主界面布局文件 activity_main.xml 中的代碼:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <my.itgungnir.lockview.LockView android:id="@+id/lockview_main_lv_lockview" android:layout_width="match_parent" android:layout_height="match_parent" app:correctColor="#00FF00" app:lineWidth="5.0dip" app:normalColor="#888888" app:selectColor="#FFFF00" app:wrongColor="#FF0000" /> </RelativeLayout>
主界面JAVA文件 MainActivity.java 中的代碼:
import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private LockView lockView; // 自定義九宮格手勢解鎖控件 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); // 通過 ID 找到控件 lockView = (LockView) findViewById(R.id.lockview_main_lv_lockview); // 初始化事件 initEvents(); } /** * 初始化事件 */ private void initEvents() { // 為LockView設置監聽器 lockView.setOnUnlockListener(new LockView.OnUnlockListener() { // 設置在什么情況下視為解鎖成功 @Override public boolean isUnlockSuccess(String result) { return "7415369".equals(result); } // 當解鎖成功時回調的方法 @Override public void onSuccess() { Toast.makeText(MainActivity.this, "Unlock Success!", Toast.LENGTH_SHORT).show(); } // 當解鎖失敗時回調的方法 @Override public void onFailure() { Toast.makeText(MainActivity.this, "Unlock Failed!", Toast.LENGTH_SHORT).show(); } }); } }
運行效果圖如下所示: