Android View自定義鎖屏圖案
前言
Android 自定義 View 技能是成為高級(jí)工程師所必備的,筆者覺(jué)得自定義 View 沒(méi)有什么捷徑可走,唯有經(jīng)常練習(xí)才能解決產(chǎn)品需求。筆者也好久沒(méi)有寫自定義 View 了,趕緊寫個(gè)控件找點(diǎn)感覺(jué)回來(lái)。
本文實(shí)現(xiàn)的是一個(gè) 鎖屏圖案的自定義控件。效果圖如下:
Github 地址:AndroidSample

LockView 介紹
自定義屬性:

引用方式:
(1) 在布局文件中引入
<com.xing.androidsample.view.LockView android:id="@+id/lock_view" app:rowCount="4" app:normalColor="" app:moveColor="" app:errorColor="" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="40dp" />
(2) 在代碼中設(shè)置正確的圖案,用于校驗(yàn)是否匹配成功,并在回調(diào)中獲取結(jié)果
List<Integer> intList = new ArrayList<>();
intList.add(3);
intList.add(7);
intList.add(4);
intList.add(2);
lockView.setStandard(intList);
lockView.setOnDrawCompleteListener(new LockView.OnDrawCompleteListener() {
@Override
public void onComplete(boolean isSuccess) {
Toast.makeText(CustomViewActivity.this, isSuccess ? "success" : "fail", Toast.LENGTH_SHORT).show();
}
});
實(shí)現(xiàn)思路
- 以默認(rèn)狀態(tài)繪制 rowCount * rowCount 個(gè)圓,外圓顏色需要在內(nèi)圓顏色上加上一定的透明度。
- 在 onTouchEvent() 方法中,判斷當(dāng)前觸摸點(diǎn)與各個(gè)圓的圓心距離是否小于圓的半徑,決定各個(gè)圓此時(shí)處于哪個(gè)狀態(tài)(normal,move,error),調(diào)用 invalidate() 重新繪制,更新顏色。
- 將手指滑動(dòng)觸摸過(guò)的圓的坐標(biāo)添加到一個(gè) ArrayList 中,使用 Path 連接該集合中選中的圓,即可繪制出劃過(guò)的路徑線。
實(shí)現(xiàn)步驟
自定義屬性
在 res/values 目錄下新建 attrs.xml 文件:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="LockView"> <attr name="normalColor" format="color|reference" /> <!--默認(rèn)圓顏色--> <attr name="moveColor" format="color|reference" /> <!--手指觸摸選中圓顏色--> <attr name="errorColor" format="color|reference" /> <!--手指抬起錯(cuò)誤圓顏色--> <attr name="rowCount" format="integer" /> <!--每行每列圓的個(gè)數(shù)--> </declare-styleable> </resources>
獲取自定義屬性
public LockView(Context context) {
this(context, null);
}
public LockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
readAttrs(context, attrs);
init();
}
/**
* 獲取自定義屬性
*/
private void readAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR);
moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR);
errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR);
rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT);
typedArray.recycle();
}
/**
* 初始化
*/
private void init() {
stateSparseArray = new SparseIntArray(rowCount * rowCount);
points = new PointF[rowCount * rowCount];
innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
innerCirclePaint.setStyle(Paint.Style.FILL);
outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
outerCirclePaint.setStyle(Paint.Style.FILL);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setStrokeJoin(Paint.Join.ROUND);
linePaint.setStrokeWidth(30);
linePaint.setColor(moveColor);
}
計(jì)算圓的半徑
設(shè)定外圓半徑和相鄰兩圓之間間距相同,內(nèi)圓半徑是外圓半徑的一半,所以半徑計(jì)算方式為:
radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;
設(shè)置各圓坐標(biāo)
各圓坐標(biāo)使用一維數(shù)組保存,計(jì)算方式為:
// 各個(gè)圓設(shè)置坐標(biāo)點(diǎn)
for (int i = 0; i < rowCount * rowCount; i++) {
points[i] = new PointF(0, 0);
points[i].set((i % rowCount * 3 + 1) * radius, (i / rowCount * 3 + 1) * radius);
}
測(cè)量 View 寬高
根據(jù)測(cè)量模式設(shè)置控件的寬高,當(dāng)布局文件中設(shè)置的是 wrap_content ,默認(rèn)將控件寬高設(shè)置為 600dp
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getSize(widthMeasureSpec);
int height = getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int getSize(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (mode == MeasureSpec.EXACTLY) {
return size;
} else if (mode == MeasureSpec.AT_MOST) {
return Math.min(size, dp2Px(600));
}
return dp2Px(600);
}
onTouchEvent() 觸摸事件
在手指滑動(dòng)過(guò)程中,根據(jù)當(dāng)前觸摸點(diǎn)坐標(biāo)是否落在圓的范圍內(nèi),更新該圓的狀態(tài),在重新繪制時(shí),繪制成新的顏色。手指抬起時(shí),將存放狀態(tài)的 list,選中圓的 list ,linePath 重置,并將結(jié)果回調(diào)出來(lái)。
private PointF touchPoint;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
reset();
case MotionEvent.ACTION_MOVE:
if (touchPoint == null) {
touchPoint = new PointF(event.getX(), event.getY());
} else {
touchPoint.set(event.getX(), event.getY());
}
for (int i = 0; i < rowCount * rowCount; i++) {
// 是否觸摸在圓的范圍內(nèi)
if (getDistance(touchPoint, points[i]) < radius) {
stateSparseArray.put(i, STATE_MOVE);
if (!selectedList.contains(points[i])) {
selectedList.add(points[i]);
}
break;
}
}
break;
case MotionEvent.ACTION_UP:
if (check()) { // 正確圖案
if (listener != null) {
listener.onComplete(true);
}
for (int i = 0; i < stateSparseArray.size(); i++) {
int index = stateSparseArray.keyAt(i);
stateSparseArray.put(index, STATE_MOVE);
}
} else { // 錯(cuò)誤圖案
for (int i = 0; i < stateSparseArray.size(); i++) {
int index = stateSparseArray.keyAt(i);
stateSparseArray.put(index, STATE_ERROR);
}
linePaint.setColor(0xeeff0000);
if (listener != null) {
listener.onComplete(false);
}
}
touchPoint = null;
if (timer == null) {
timer = new Timer();
}
timer.schedule(new TimerTask() {
@Override
public void run() {
linePath.reset();
linePaint.setColor(0xee0000ff);
selectedList.clear();
stateSparseArray.clear();
postInvalidate();
}
}, 1000);
break;
}
invalidate();
return true;
}
繪制各圓和各圓之間連接線段
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCircle(canvas);
drawLinePath(canvas);
}
private void drawCircle(Canvas canvas) {
// 依次從索引 0 到索引 8,根據(jù)不同狀態(tài)繪制圓點(diǎn)
for (int index = 0; index < rowCount * rowCount; index++) {
int state = stateSparseArray.get(index);
switch (state) {
case STATE_NORMAL:
innerCirclePaint.setColor(normalColor);
outerCirclePaint.setColor(normalColor & 0x66ffffff);
break;
case STATE_MOVE:
innerCirclePaint.setColor(moveColor);
outerCirclePaint.setColor(moveColor & 0x66ffffff);
break;
case STATE_ERROR:
innerCirclePaint.setColor(errorColor);
outerCirclePaint.setColor(errorColor & 0x66ffffff);
break;
}
canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint);
canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint);
}
}
完整 View 代碼:
/**
* Created by star.tao on 2018/5/30.
* email: xing-java@foxmail.com
* github: https://github.com/xing16
*/
public class LockView extends View {
private static final int DEFAULT_NORMAL_COLOR = 0xee776666;
private static final int DEFAULT_MOVE_COLOR = 0xee0000ff;
private static final int DEFAULT_ERROR_COLOR = 0xeeff0000;
private static final int DEFAULT_ROW_COUNT = 3;
private static final int STATE_NORMAL = 0;
private static final int STATE_MOVE = 1;
private static final int STATE_ERROR = 2;
private int normalColor; // 無(wú)滑動(dòng)默認(rèn)顏色
private int moveColor; // 滑動(dòng)選中顏色
private int errorColor; // 錯(cuò)誤顏色
private float radius; // 外圓半徑
private int rowCount;
private PointF[] points; // 一維數(shù)組記錄所有圓點(diǎn)的坐標(biāo)點(diǎn)
private Paint innerCirclePaint; // 內(nèi)圓畫筆
private Paint outerCirclePaint; // 外圓畫筆
private SparseIntArray stateSparseArray;
private List<PointF> selectedList = new ArrayList<>();
private List<Integer> standardPointsIndexList = new ArrayList<>();
private Path linePath = new Path(); // 手指移動(dòng)的路徑
private Paint linePaint;
private Timer timer;
public LockView(Context context) {
this(context, null);
}
public LockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
readAttrs(context, attrs);
init();
}
private void readAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView);
normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR);
moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR);
errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR);
rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT);
typedArray.recycle();
}
private void init() {
stateSparseArray = new SparseIntArray(rowCount * rowCount);
points = new PointF[rowCount * rowCount];
innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
innerCirclePaint.setStyle(Paint.Style.FILL);
outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
outerCirclePaint.setStyle(Paint.Style.FILL);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setStrokeJoin(Paint.Join.ROUND);
linePaint.setStrokeWidth(30);
linePaint.setColor(moveColor);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 外圓半徑 = 相鄰?fù)鈭A之間間距 = 2倍內(nèi)圓半徑
radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;
// 各個(gè)圓設(shè)置坐標(biāo)點(diǎn)
for (int i = 0; i < rowCount * rowCount; i++) {
points[i] = new PointF(0, 0);
points[i].set((i % rowCount * 3 + 1) * radius, (i / rowCount * 3 + 1) * radius);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getSize(widthMeasureSpec);
int height = getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int getSize(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (mode == MeasureSpec.EXACTLY) {
return size;
} else if (mode == MeasureSpec.AT_MOST) {
return Math.min(size, dp2Px(600));
}
return dp2Px(600);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCircle(canvas);
drawLinePath(canvas);
}
private void drawCircle(Canvas canvas) {
// 依次從索引 0 到索引 8,根據(jù)不同狀態(tài)繪制圓點(diǎn)
for (int index = 0; index < rowCount * rowCount; index++) {
int state = stateSparseArray.get(index);
switch (state) {
case STATE_NORMAL:
innerCirclePaint.setColor(normalColor);
outerCirclePaint.setColor(normalColor & 0x66ffffff);
break;
case STATE_MOVE:
innerCirclePaint.setColor(moveColor);
outerCirclePaint.setColor(moveColor & 0x66ffffff);
break;
case STATE_ERROR:
innerCirclePaint.setColor(errorColor);
outerCirclePaint.setColor(errorColor & 0x66ffffff);
break;
}
canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint);
canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint);
}
}
/**
* 繪制選中點(diǎn)之間相連的路徑
*
* @param canvas
*/
private void drawLinePath(Canvas canvas) {
// 重置linePath
linePath.reset();
// 選中點(diǎn)個(gè)數(shù)大于 0 時(shí),才繪制連接線段
if (selectedList.size() > 0) {
// 起點(diǎn)移動(dòng)到按下點(diǎn)位置
linePath.moveTo(selectedList.get(0).x, selectedList.get(0).y);
for (int i = 1; i < selectedList.size(); i++) {
linePath.lineTo(selectedList.get(i).x, selectedList.get(i).y);
}
// 手指抬起時(shí),touchPoint設(shè)置為null,使得已經(jīng)繪制游離的路徑,消失掉,
if (touchPoint != null) {
linePath.lineTo(touchPoint.x, touchPoint.y);
}
canvas.drawPath(linePath, linePaint);
}
}
private PointF touchPoint;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
reset();
case MotionEvent.ACTION_MOVE:
if (touchPoint == null) {
touchPoint = new PointF(event.getX(), event.getY());
} else {
touchPoint.set(event.getX(), event.getY());
}
for (int i = 0; i < rowCount * rowCount; i++) {
// 是否觸摸在圓的范圍內(nèi)
if (getDistance(touchPoint, points[i]) < radius) {
stateSparseArray.put(i, STATE_MOVE);
if (!selectedList.contains(points[i])) {
selectedList.add(points[i]);
}
break;
}
}
break;
case MotionEvent.ACTION_UP:
if (check()) { // 正確圖案
if (listener != null) {
listener.onComplete(true);
}
for (int i = 0; i < stateSparseArray.size(); i++) {
int index = stateSparseArray.keyAt(i);
stateSparseArray.put(index, STATE_MOVE);
}
} else { // 錯(cuò)誤圖案
for (int i = 0; i < stateSparseArray.size(); i++) {
int index = stateSparseArray.keyAt(i);
stateSparseArray.put(index, STATE_ERROR);
}
linePaint.setColor(0xeeff0000);
if (listener != null) {
listener.onComplete(false);
}
}
touchPoint = null;
if (timer == null) {
timer = new Timer();
}
timer.schedule(new TimerTask() {
@Override
public void run() {
linePath.reset();
linePaint.setColor(0xee0000ff);
selectedList.clear();
stateSparseArray.clear();
postInvalidate();
}
}, 1000);
break;
}
invalidate();
return true;
}
/**
* 清除繪制圖案的條件,當(dāng)觸發(fā) invalidate() 時(shí)將清空?qǐng)D案
*/
private void reset() {
touchPoint = null;
linePath.reset();
linePaint.setColor(0xee0000ff);
selectedList.clear();
stateSparseArray.clear();
}
public void onStop() {
timer.cancel();
}
private boolean check() {
if (selectedList.size() != standardPointsIndexList.size()) {
return false;
}
for (int i = 0; i < standardPointsIndexList.size(); i++) {
Integer index = standardPointsIndexList.get(i);
if (points[index] != selectedList.get(i)) {
return false;
}
}
return true;
}
public void setStandard(List<Integer> pointsList) {
if (pointsList == null) {
throw new IllegalArgumentException("standard points index can't null");
}
if (pointsList.size() > rowCount * rowCount) {
throw new IllegalArgumentException("standard points index list can't large to rowcount * columncount");
}
standardPointsIndexList = pointsList;
}
private OnDrawCompleteListener listener;
public void setOnDrawCompleteListener(OnDrawCompleteListener listener) {
this.listener = listener;
}
public interface OnDrawCompleteListener {
void onComplete(boolean isSuccess);
}
private float getDistance(PointF centerPoint, PointF downPoint) {
return (float) Math.sqrt(Math.pow(centerPoint.x - downPoint.x, 2) + Math.pow(centerPoint.y - downPoint.y, 2));
}
private int dp2Px(int dpValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
}
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- android實(shí)現(xiàn)一鍵鎖屏和一鍵卸載的方法實(shí)例
- Android喚醒、解鎖屏幕代碼實(shí)例
- 設(shè)置Android系統(tǒng)永不鎖屏永不休眠的方法
- Android屏幕鎖屏彈窗的正確姿勢(shì)DEMO詳解
- Android編程實(shí)現(xiàn)一鍵鎖屏的方法
- Android編程實(shí)現(xiàn)禁止系統(tǒng)鎖屏與解鎖亮屏的方法
- android禁止鎖屏保持常亮(示例代碼)
- Android如何實(shí)現(xiàn)鎖屏狀態(tài)下彈窗
- Android 監(jiān)聽(tīng)鎖屏、解鎖、開(kāi)屏 功能代碼
- Android實(shí)現(xiàn)帶頁(yè)面切換的鎖屏功能
相關(guān)文章
Kotlin?this關(guān)鍵字的使用實(shí)例詳解
這篇文章主要介紹了Kotlin?this關(guān)鍵字的使用實(shí)例,在Kotlin中,this關(guān)鍵字允許我們引用一個(gè)類的實(shí)例,該類的函數(shù)恰好正在運(yùn)行。此外,還有其他方式可以使this表達(dá)式派上用場(chǎng)2023-02-02
Android BroadcastReceiver常見(jiàn)監(jiān)聽(tīng)整理
這篇文章主要介紹了Android BroadcastReceiver常見(jiàn)監(jiān)聽(tīng)整理的相關(guān)資料,需要的朋友可以參考下2016-10-10
Android應(yīng)用實(shí)現(xiàn)點(diǎn)擊按鈕震動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android應(yīng)用實(shí)現(xiàn)點(diǎn)擊按鈕震動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
仿網(wǎng)易新聞客戶端頭條ViewPager嵌套實(shí)例
正確使用requestDisallowInterceptTouchEvent(boolean flag)方法,下面為大家介紹下外層ViewPager布局的實(shí)例,感興趣的朋友可以參考下哈2013-06-06
Emoji表情在Android JNI中的兼容性問(wèn)題詳解
這篇文章主要給大家介紹了關(guān)于Emoji表情在Android JNI中的兼容性問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Android JNI具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Android開(kāi)發(fā)實(shí)現(xiàn)刪除聯(lián)系人通話記錄的方法
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)刪除聯(lián)系人通話記錄的方法,較為詳細(xì)的分析了Android刪除通話記錄的原理、步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-10-10
利用Android畫圓弧canvas.drawArc()實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于利用Android畫圓弧canvas.drawArc()的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的理解和學(xué)習(xí)具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
Android自定義View實(shí)現(xiàn)字母導(dǎo)航欄
通常手機(jī)通訊錄都會(huì)有索引欄,這篇文章主要介紹了Android自定義View實(shí)現(xiàn)字母導(dǎo)航欄,現(xiàn)在分享給大家。2016-10-10

