前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android  View自定义锁屏图案

Android  View自定义锁屏图案

作者头像
砸漏
发布2020-11-05 15:25:36
8870
发布2020-11-05 15:25:36
举报
文章被收录于专栏:恩蓝脚本恩蓝脚本

前言

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

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

Github 地址:AndroidSample

LockView 介绍

自定义属性:

引用方式:

(1) 在布局文件中引入

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

代码语言:javascript
复制
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();
   }
  });

实现思路

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

实现步骤

自定义属性

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

代码语言:javascript
复制
<?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 

获取自定义属性

代码语言:javascript
复制
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);
  }

计算圆的半径

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

代码语言:javascript
复制
radius = Math.min(w, h) / (2 * rowCount + rowCount - 1) * 1.0f;

设置各圆坐标

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

代码语言:javascript
复制
// 各个圆设置坐标点
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

代码语言:javascript
复制
 @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() 触摸事件

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

代码语言:javascript
复制
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++) {
     // 是否触摸在圆的范围内
     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() {
     @Override
     public void run() {
      linePath.reset();
      linePaint.setColor(0xee0000ff);
      selectedList.clear();
      stateSparseArray.clear();
      postInvalidate();
     }
    }, 1000);
    break;
  }
  invalidate();
  return true;
 }

绘制各圆和各圆之间连接线段

代码语言:javascript
复制
@Override
 protected 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 代码:

代码语言:javascript
复制
/**
* 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);
}
@Override
protected 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);
}
}
@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,根据不同状态绘制圆点
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) {
// 重置linePath
linePath.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;
@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++) {
// 是否触摸在圆的范围内
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() {
@Override
public 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());
}
}

以上就是本文的全部内容,希望对大家的学习有所帮助。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-09-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档