前言

Android 自定义 View 技能是成为高级工程师所必备的,笔者觉得自定义 View 没有什么捷径可走,唯有经常练习才能解决产品需求。笔者也好久没有写自定义 View 了,赶紧写个控件找点感觉回来。

本文实现的是一个 锁屏图案的自定义控件。效果图如下:

Github 地址:https://github.com/xing16/AndroidSample

LockView 介绍

自定义属性:
属性 描述
app:rowCount=”3” 每行每列圆的个数
app:normalColor=”0xee776666” 圆的默认颜色
app:moveColor=”0xee0000ff” 圆的选中颜色
app:errorColor=”0xeeff0000” 圆的错误颜色
引用方式:

(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) 在代码中设置正确的图案,用于校验是否匹配成功,并在回调中获取结果

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() {@Overridepublic void onComplete(boolean isSuccess) {Toast.makeText(CustomViewActivity.this, isSuccess ? "success" : "fail", Toast.LENGTH_SHORT).show();}});

实现思路

  1. 以默认状态绘制 rowCount * rowCount 个圆,外圆颜色需要在内圆颜色上加上一定的透明度。
  2. 在 onTouchEvent() 方法中,判断当前触摸点与各个圆的圆心距离是否小于圆的半径,决定各个圆此时处于哪个状态(normal,move,error),调用 invalidate() 重新绘制,更新颜色。
  3. ​将手指滑动触摸过的圆的坐标添加到一个 ArrayList 中,使用 Path 连接该集合中选中的圆,即可绘制出划过的路径线。

实现步骤

自定义属性

在 res/values 目录下新建 attrs.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="LockView"><attr name="normalColor" format="color|reference" />    <!--默认圆颜色--><attr name="moveColor" format="color|reference" />      <!--手指触摸选中圆颜色--> <attr name="errorColor" format="color|reference" />     <!--手指抬起错误圆颜色--> <attr name="rowCount" format="integer" />               <!--每行每列圆的个数-->  </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);}
计算圆的半径

设定外圆半径和相邻两圆之间间距相同,内圆半径是外圆半径的一半,所以半径计算方式为:


radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;

设置各圆坐标

各圆坐标使用一维数组保存,计算方式为:

// 各个圆设置坐标点
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);
}
测量 View 宽高

根据测量模式设置控件的宽高,当布局文件中设置的是 wrap_content ,默认将控件宽高设置为 600dp

    @Overrideprotected 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() 触摸事件

在手指滑动过程中,根据当前触摸点坐标是否落在圆的范围内,更新该圆的状态,在重新绘制时,绘制成新的颜色。手指抬起时,将存放状态的 list,选中圆的 list ,linePath 重置,并将结果回调出来。

private PointF touchPoint;@Overridepublic 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++) {// 是否触摸在圆的范围内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 {     // 错误图案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() {@Overridepublic void run() {linePath.reset();linePaint.setColor(0xee0000ff);selectedList.clear();stateSparseArray.clear();postInvalidate();}}, 1000);break;}invalidate();return true;}
绘制各圆和各圆之间连接线段
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawCircle(canvas);drawLinePath(canvas);}private void drawCircle(Canvas canvas) {// 依次从索引 0 到索引 8,根据不同状态绘制圆点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; // 无滑动默认颜色private int moveColor;   // 滑动选中颜色private int errorColor;  // 错误颜色private float radius;    // 外圆半径private int rowCount;private PointF[] points;   // 一维数组记录所有圆点的坐标点private Paint innerCirclePaint; // 内圆画笔private Paint outerCirclePaint; // 外圆画笔private SparseIntArray stateSparseArray;private List<PointF> selectedList = new ArrayList<>();private List<Integer> standardPointsIndexList = new ArrayList<>();private Path linePath = new Path();    // 手指移动的路径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);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);// 外圆半径 = 相邻外圆之间间距 = 2倍内圆半径radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;// 各个圆设置坐标点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);}}@Overrideprotected 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);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawCircle(canvas);drawLinePath(canvas);}private void drawCircle(Canvas canvas) {// 依次从索引 0 到索引 8,根据不同状态绘制圆点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);}}/*** 绘制选中点之间相连的路径** @param canvas*/private void drawLinePath(Canvas canvas) {// 重置linePathlinePath.reset();// 选中点个数大于 0 时,才绘制连接线段if (selectedList.size() > 0) {// 起点移动到按下点位置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);}// 手指抬起时,touchPoint设置为null,使得已经绘制游离的路径,消失掉,if (touchPoint != null) {linePath.lineTo(touchPoint.x, touchPoint.y);}canvas.drawPath(linePath, linePaint);}}private PointF touchPoint;@Overridepublic 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++) {// 是否触摸在圆的范围内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 {     // 错误图案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() {@Overridepublic void run() {linePath.reset();linePaint.setColor(0xee0000ff);selectedList.clear();stateSparseArray.clear();postInvalidate();}}, 1000);break;}invalidate();return true;}/*** 清除绘制图案的条件,当触发 invalidate() 时将清空图案*/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());}}

Android 自定义锁屏图案 View相关推荐

  1. 浅谈 Android 自定义锁屏页的发车姿势

    作者:blowUp ,原文链接:http://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653577446&idx=2&sn ...

  2. 【腾讯Bugly干货分享】浅谈Android自定义锁屏页的发车姿势

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57875330c9da73584b025873 一.为什么需要自定义锁屏页 锁屏 ...

  3. 浅谈Android自定义锁屏页的发车姿势

    一.为什么需要自定义锁屏页 锁屏作为一种黑白屏时代就存在的手机功能,至今仍发挥着巨大作用,特别是触屏时代的到来,锁屏的功用被发挥到了极致.多少人曾经在无聊的时候每隔几分钟划开锁屏再关上,孜孜不倦,其酸 ...

  4. 【腾讯Bugly干货分享】浅谈 Android 自定义锁屏页的发车姿势

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57875330c9da73584b025873 一.为什么需要自定义锁屏页 锁屏 ...

  5. Android 自定义锁屏_三星Key Cafe 2021最新版下载-三星Key Cafe自定义键盘输入app v1.0.00.26...

    这次小编要为大家带来一款由三星官方正式推出的自定义键盘输入工具"Key Cafe",帮助大家能够自由定制手机输入法的主题和键位,允许用户能够自由定制键盘布局,增删案件等,设计一套最 ...

  6. Android 4.0 自定义锁屏

    在Android 4.0上做锁屏有一段时间了,期间改了很多bug,也按不同需求做了不少锁屏,其中比较满意的作品包括两个.一是,添加一个锁屏可以和原生锁屏进行切换:二是,自己写一个锁屏view去替换原生 ...

  7. Android实现自定义锁屏控制

    当在Android手机上需要实现自定义的锁屏,  往往在进入自定义的锁屏界面界面之前需要先解开屏幕锁, 以顺利的进入自定义锁屏界面 ,并能方便用户即时的做其他操作,下面用代码来实现这一功能: 1.点亮 ...

  8. jQuery仿Android锁屏图案应用插件

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

  9. jQuery仿Android锁屏图案应用

    jQuery仿Android锁屏图案应用 在线演示 本地下载 posted @ 2018-12-03 14:08 栖息地 阅读(...) 评论(...) 编辑 收藏

  10. android 8 忘记图案,安卓手机忘记锁屏图案密码六种解决办法

    安卓手机忘记锁屏图案密码六种解决办法,小编就来一一说明. 第一种方法:用别人的手机打你的电话, 然后手机就会进入系统,再进设置里去掉自动锁屏.当提示输入gmail密码的时候填写:"null& ...

最新文章

  1. 一看就会的20个“非常有用”的python小技巧,你一定要试试
  2. CCleaner v5.12.5431 单文件汉化版
  3. 如何做好新一年的产品规划?
  4. 代码管理 ,git 命令整理
  5. Go搭建静态页面server笔记
  6. javascript二维数组
  7. php mysql设计中验证码的实现_利用PHP绘图函数实现简单验证码功能
  8. 事务和锁机制是什么关系
  9. 用dockers实现mysql主从同步
  10. PHP处理海量样本相似度聚类算法
  11. 三星固态硬盘ssd产品线收集
  12. 在NS2(2.35版本)中添加 Ping协议
  13. 一名技术的原则—美团工作六年的认知(2020年)
  14. 我的时间管理及未来两年IT规划
  15. 从应用迁移到平台微认证:鲲鹏技术解读
  16. 花卉世界大观园和杂技之游(r12笔记第97天)
  17. 利用定时器1实现流水灯
  18. java cup_java高cup占用解决方案
  19. linux m2硬盘驱动,Linux R720挂载M.2固态硬盘
  20. The nearest taller cow

热门文章

  1. BlenderGIS:No ImageIO解决办法 天地图地图资源
  2. 什么是即时通讯?即时通讯的发展
  3. pvr与png的内存占用
  4. ultravnc使用,ultravnc如何进行使用
  5. linux创建文档并且打字,与 Linux 一起学习:学习打字
  6. 企业微信裂变玩法有哪些?需要使用哪些工具?
  7. 无线模块数据加密,反码校验,发送字符串ABCDEFGHIJKLMNOP LED频闪
  8. 应用宝上线应用后一直处于审核状态问题解决
  9. windows下文件名太长无法删除
  10. Windows 文件夹或文件名 太长无法删除怎么办? 一招教你怎样解决.