在进行功能模块开发时,必然会有功能需求,但是在此之前一定尽量的去了解业务需求和用户需求。以屏幕锁定功能为例,属于小模块功能,业务需求较为宏观,一般在项目落地之初,业务需求会随之明确,这里主要分析用户需求。有的时候功能需求和用户需求会非常明确,明确到我们只需要按照分析文档照本宣科地实现功能就行,但是有的时候我们也会遇到不知所谓的要求,不论是那种情况,我们在需求分析的时候,一定要多转换身份来多方位思考,以用户的角度来代入他的使用场景,寻找产品解决的痛点,分析用户的真正需求。为什么我说真正的需求呢?我认为用户说的可能未必是他们想要的,可能是基于自身角色说出一些并非真正需求的话。比如:索尼在发布新款音响时,想了解用户真正喜欢的是那款颜色,在调研的时候,被邀请的测试者大部分回答黄色,但是在结束时,主办方提供免费的音响作为礼物,但是大部分测试者拿走的却是黑色。
屏幕锁定
用户人群:老师
使用场景:学校
功能的作用:预防学生操作
功能需求:设置背景图片,设置数字密码,视觉体验佳
针对以上需求,必然需要用到悬浮窗,而且悬浮窗的TYPE级别要很高,且提供选择图库让用户选择图片设置背景,使用EditText来满足设置密码,考虑用户体验,自由使用的前提下,在进入锁定界面时,逻辑判断应该允许客户在不设置背景图片和密码的情况下进入锁屏界面,在操作体验佳的要求下,可自定义绘制一个半透明圆,让用户拖动锁到圆的范围外的操作来解锁的设计。基本就这些,先上成品效果图。
功能非常简单,只描述自定义解锁View,这个功能其实也很简单,只是判断手是否按在锁上,拖动时,判断锁是否有超出圆的范围,如果超出,有设置密码的话就弹出密码解锁界面,没有就直接解锁。通过求平方根获得移动的范围的距离d,如果d>r代表移动的距离已经超出半径可以解锁。
首先在layout中确定自定义View的宽高和中心坐标点,iconX、iconY是锁图标的左上角坐标,centerRectX、centerRectY是锁图标的中心坐标。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = right - left;
mHeight = bottom - top;
centerX = mWidth / 2;
centerY = mHeight / 2;
iconX = centerX - iconBitmap.getWidth() / 2;
iconY = centerY - iconBitmap.getHeight() / 2 - 30;
centerRectX = iconX + iconBitmap.getWidth() / 2;
centerRectY = iconY + iconBitmap.getHeight() / 2;
centerPintF.set(centerX, centerY);
}
在手势响应按下事件时,应该判断当前坐标是否处于锁图标的范围
//判断一个点是否在矩形内部
public boolean isInsider(double x, double y) {
int x1 = iconX;
int y1 = iconY;
int x4 = iconX + iconBitmap.getWidth();
int y4 = iconY + iconBitmap.getHeight();
//默认:1点在左上,4点在右下
if (x < x1) {//在矩形左侧
return false;
}
if (y < y1) {//在矩形上侧
return false;
}
if (x > x4) {//在矩形右侧
return false;
}
if (y > y4) {//在矩形下侧
return false;
}
return true;
}
如果是按在锁上,在移动的时候,锁也需要跟着我们的手指移动,另外在移动的时候,需要注意下X轴分左移和右移,Y轴分上移和下移,在这里我们的坐标点获取需要另作计算,在移动的时候如果超出范围就通过接口回调通知。同时需要注意的时,在抬起事件中,如果没有超出反应,锁需要重绘返回中心位置。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
isInside = isInsider(x, y);
break;
case MotionEvent.ACTION_MOVE:
if (isInside && iconBitmap != null) {
int moveX = (int) event.getX();
int moveY = (int) event.getY();
if (moveX < centerRectX) {
pointF = new PointF(moveX - iconBitmap.getWidth() / 2, moveY);
}
if (moveX > centerRectX) {
pointF = new PointF(moveX + iconBitmap.getWidth() / 2, moveY);
}
if (moveY < centerRectY) {
pointF = new PointF(moveX, moveY - iconBitmap.getHeight() / 2);
}
if (moveY > centerRectY) {
pointF = new PointF(moveX, moveY + iconBitmap.getHeight() / 2);
}
iconX = moveX - iconBitmap.getWidth() / 2;
iconY = moveY - iconBitmap.getHeight() / 2;
centerRectX = moveX;
centerRectY = moveY;
if (!isPointInCircle(pointF, centerPintF, centerX)) {
if (mListener != null) {
mListener.onUnlockSuccess();
recycleBitmap();
}
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (isInside) {
int upX = (int) event.getX();
int upY = (int) event.getY();
if (isPointInCircle(new PointF(upX, upY), centerPintF, centerX)) {
iconX = centerX - iconBitmap.getWidth() / 2;
iconY = centerY - iconBitmap.getHeight() / 2 - 30;
invalidate();
}
}
break;
}
return true;
}
/**
* 判断点是否在圆内
*
* @param pointF 待确定点
* @param circle 圆心
* @param radius 半径
* @return true在圆内
*/
private boolean isPointInCircle(PointF pointF, PointF circle, float radius) {
return Math.pow((pointF.x - circle.x), 2) + Math.pow((pointF.y - circle.y), 2) <= Math.pow(radius, 2);
}
请注意:Android6.0及以上请自行添加动态申请悬浮窗权限
项目代码
谢谢观看!