您的位置:首页 > 移动开发 > Android开发

Android 自定义锁屏的实现

2014-07-08 11:34 435 查看
最近公司无事,所以找点事干。刚好在研究view和viewgroup这部分的源码,也尝试重写一些view和viewgroup加深理解。看到网上有人写九宫格的手势锁屏,就自己试了试,坐下来感觉难度不大,倒是有很多细节上的东西,需要记录一下,而且过程中也确实学到了不少,进步了不少。

一. 思路

看到网上的同仁,大体是2种方式,一种是直接重写一个view,然后绘制所有的东西,另外一种是重写view绘制圆点,再重写一个viewgroup作为圆点的容器,当然貌似还有其他的实现方式,我没太注意。在这里我采用第二种实现方式。

二. 效果图

随便弄了个下图,感觉看起来顺眼,有时间还得好好再加工一下:





三. 重写view

DotView,其实这个view重写的逻辑很简单,就是设置好画笔,在onDraw()里面画圆圈就行了,代码如下:

protected void onDraw(Canvas canvas) {
mWidth = getWidth();
mHeight = getHeight();

centerX = mWidth / 2;
centerY = mHeight / 2;
/**radius of the outter circle */
mRadius = mWidth > mHeight ? centerY: centerX;
innerRadius = (float) (mRadius * 0.2);
outterRadius = (float) (mRadius * 0.5);
canvas.save();

switch (mState) {
case STATE_NORMAL:
drawDot(canvas);
break;
case STATE_TOUCHED:
canvas.restore();
mPaint.setColor(CORLOR_SELECTED);
drawCircle(canvas);
break;
case STATE_ERROR:
canvas.restore();
mPaint.setColor(CORLOR_ERROR);
drawCircle(canvas);
break;
}
}


STATE_NORMAL,STATE_TOUCHED和STATE_ERROR这3个常量,分别代表圆圈的正常状态,被触摸或点中后的状态,解锁失败以后要呈现的状态;

/**
* draw the smallest inner dot
* @param canvas  draw on which
*/
private void drawDot(Canvas canvas) {
canvas.restore();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(1);
mPaint.setColor(COLOR_NORMAL_BACK);
canvas.drawCircle(centerX, centerY, outterRadius, mPaint);

mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(WHITE);
canvas.drawCircle(centerX, centerY, innerRadius, mPaint);
}

/**
* draw two circles when selected
* @param canvas
*/
private void drawCircle(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(centerX, centerY, innerRadius, mPaint);

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(innerRadius);
canvas.drawCircle(centerX, centerY, outterRadius, mPaint);
}


这2个方法是画内圈的实心圆点,和外圈的空心圆,逻辑也十分简单;

基本上主要代码就是这些了,所以说很简单;

二. 重写Viewgroup

网上很多实现方式都是重写的相对布局或者线性布局,好处就是不用写onLayout()了,我自己重写了viewgroup,不过布局也不难,因为我的锁屏是写死了九宫格,不像其他的实现,是可以任意宫的;

ScreenLockView的主要代码和逻辑就相对多一些:

public ScreenLockView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mSeletedViews = new ArrayList<Integer>();
mDotViews = new DotView[TOTAL_DOTS];
unlockPath = new Path();

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(DotView.CORLOR_SELECTED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setAlpha(150);
}


我在学习过程中,看到2个没用过的方法:mPaint.setStrokeCap(Paint.Cap.ROUND)和mPaint.setStrokeJoin(Paint.Join.ROUND),在网上查找,居然得出很多不同的答案,自己试验了下,基本上是使绘制的线条比较平滑,还有转接的地方比较自然。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);

if (mDotViews != null) {
/**
* layout the dotviews :3 rows and 3 columns
*/
mViewWidth = width / 3;
mViewHeight = height / 3;
/**
* add to the parent
*/
ViewGroup.LayoutParams params;
params = new LayoutParams(mViewWidth, mViewHeight);
for (int i = 0 ; i < TOTAL_DOTS ; i ++) {
mDotViews[i] = new DotView(getContext());
mDotViews[i].setId(i);
addView(mDotViews[i], params);
}
}
setMeasuredDimension(width, height);
}


在onMeasure()里面把9个圆点视图添加到父容器中,尺寸大小也比较好计算;

protected void onLayout(boolean changed, int l, int t, int r, int b) {
int row = 0;
int column = 0;
for (int i = 0 ; i < 9 ; i ++) {
/**
* the layout like this:
*    row |   0   1   2
* --------------------
* column |
*    0   |   0   1   2
*    1   |   3   4   5
*    2   |   6   7   8
*/
row = i / 3;
column = i % 3;
getChildAt(i).layout(column * mViewWidth, row * mViewHeight
, (column + 1) * mViewWidth, (row + 1) * mViewHeight);
}
}


至于布局的排列,逻辑上也很简单,按照我的排列方法,id除以3是行号,id取余3是列号,然后计算好位置也很简单;

另外发现一个问题,l,t,是实际使用时,可能是非0的值,因为容器内可能有其他控件占据位置,但是在布局时,从逻辑上要把他们当成坐标轴的x,y去看待,即要认为我们是在圆点为(l,t)的坐标轴内布局,否则视图就会发生偏移。这也是我遇到问题之后,研究了不少时间才解决的。

public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
/** calculate the location according to the touching point*/
int id = calculateIdByXY(x, y);
if (MotionEvent.ACTION_DOWN == action) {
/**
* if touching a valid point
*/
if (!unlockPath.isEmpty()) {
clear();
} else {
if (INVALID_REGION != id) {
isUnlocking = true;
DotView view = (DotView) findViewById(id);
view.setState(DotView.STATE_TOUCHED);
mSeletedViews.add(id);

/** calculate the center x&y according to view id*/
float centerX = (float) ((id % 3 + 0.5) * mViewWidth);
float centerY = (float) ((id / 3 + 0.5) * mViewHeight);
unlockPath.moveTo(centerX,centerY);
}
}
} else if (MotionEvent.ACTION_MOVE == action) {
if (isUnlocking) {
if (INVALID_REGION != id) {
DotView view = (DotView) findViewById(id);
view.setState(DotView.STATE_TOUCHED);

if (!mSeletedViews.contains(id)) {
mSeletedViews.add(id);
}
}
setPath();
unlockPath.lineTo(x, y);
}
} else if (MotionEvent.ACTION_UP == action) {
if (isUnlocking) {
isUnlocking = false;
setPath();
mListener.onComplete(mSeletedViews);
}
}
invalidate();
return true;
}


onTouchEvent()的逻辑相对复杂一些,不过分为按下,移动和抬起来看,调理仍很清晰;

-如果触摸到圆点有效范围,就计算出圆点圆心并设为path的起点;

-如果在移动过程中,就按照触摸点的位置,绘制路径;

-一旦抬起,就结束绘制,刷新界面;

setPath()是将已经触摸过的圆点,依次绘制路线连接起来;

protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
int width = (int) (mViewHeight > mViewWidth ? (mViewWidth * 0.5 * 0.25) : (mViewHeight * 0.5 * 0.25));
mPaint.setStrokeWidth(width);

canvas.drawPath(unlockPath, mPaint);
}


重写dispatchDraw()来绘制路线;

/**
* if the wrong pw is inputed,draw in error color and clear all after one second
*/
public void error(){
for (int i = 0 ; i < mSeletedViews.size() ; i ++) {
((DotView) findViewById(mSeletedViews.get(i))).setState(DotView.STATE_ERROR);
}
mPaint.setColor(DotView.COLOR_ERROR);
mPaint.setAlpha(150);
invalidate();
new Thread(new Runnable(){

@Override
public void run() {
try {
Thread.sleep(1000);
mHandler.sendEmptyMessage(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}


error()方法被调用,当用户输入错误密码的时候,它将所有选中圆点和路径显示为红色,并在1秒后,全部清除;

/**
* calculate if a touching point is on an available position
*
* @param x
* @param y
* @return id -the view's id if it is , -1 if not
*/
private int calculateIdByXY(float x , float y) {
int column = (int) (x / mViewWidth);
int row = (int) (y / mViewHeight);
float radius = mViewWidth > mViewHeight ? (mViewHeight / 2) : (mViewWidth / 2);
radius = (float) (radius * 0.4);
float xBottomBorder = (float) ((column + 0.5) * mViewWidth - radius);
float xTopBorder = xBottomBorder + 2 * radius;
float yBottomBorder = (float) ((row + 0.5) * mViewHeight - radius);
float yTopBorder = yBottomBorder + 2 * radius;

if ( x >= xBottomBorder && x <= xTopBorder &&
y >= yBottomBorder && y <= yTopBorder) {
return row * 3 + column;
}
return INVALID_REGION;
}


calculateIdByXY()通过xy坐标计算触摸点是否在圆点有效位置,如果是就返回该view的id;

行号和列号,在一开始很容易就可以计算出来,有个行列号,下来的工作其实很简单了,Row*3+column就是view的id号了,其他的计算是在确定是否属于有效区域;

以上基本全是是关于重写方面的工作了,接下来的知识点全是关于锁屏的了;

四. 锁屏逻辑实现



这张图显示了主要的逻辑关系:

MainActivity:设置密码的界面,在应用安装之后启动,可以设置和修改密码;它包含了一个ScreenLockView,当应用第一次安装后,设置密码成功,会启动LockService服务;

LockActivity:锁屏的真正界面;

LockService:服务开机启动,接收屏蔽亮灭的广播,来开启LockActivity;

BootCompletedReceiver:接收开机完成广播,并启动LockService;

1. 密码存储:

在设置密码界面,将密码存储在SharedPreferences中;

2. 保持服务常驻

通过网络查阅和sdk阅读,我总共使用了3种方式来防止服务被杀死,以及被杀死后自动重启;

在配置文件中,设置服务的优先级为最高;



在onStartCommand()方法中,使用startForeground(0, null)方法将服务设置为前台服务,方法参数用来设置要显示的通知,这里不显示,所以设置为0,并且在onDestroy()方法中,使用stopForeground(true);

在onStartCommand()方法中,保存服务启动的intent,然后在onDestroy中重新启动:



3. 相关广播

一个外部广播接收器在开机完成后启动服务,一个内部广播接收器接收屏幕亮灭广播,从而启动锁屏界面;

4. 屏蔽系统锁屏



这是目前采用的方法,也是网上流传很多的方法,是管用的,需要在配置文件中增加使用权限android.permission.RECEIVE_BOOT_COMPLETED;

然而我在sdk当中看到,这种方法已经被弃用,官方有推荐的新方法,并且网上也说这种方法会在新版本失效,我在我的4.0的手机上测试时,仍然是有效的;

官方推出的新方法是:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);

getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);

但是我在LockActivity中使用这种方法,并没有产生作用,所以我只好采用了旧办法;

5. 屏蔽状态栏

我目前的做法是使LockActivity全屏显示了,但是很多人说这样不好,得让状态栏能显示,但是又不能下拉,这个我暂且留着研究;

续:

在网上找到一个简单,效果还不错的实现屏蔽的方法:



在锁屏界面重写此方法。

6.屏蔽home键

网上流传的屏蔽home键的方法有好几个版本,包括旧版本中有效,而新版本失效的方法,我只提供最新最近的方法,应该是向下兼容的:

private static final int FLAG_HOMEKEY_DISPATCHED = 0x80000000;

getWindow().setFlags(FLAG_HOMEKEY_DISPATCHED, FLAG_HOMEKEY_DISPATCHED);

而据说android4.0+已经无法屏蔽home键,我未亲测,关于这一点的处理,可以参照以下博客:(Android锁屏页实现原理及技术要点);

7.屏蔽返回键,菜单键,音量键



但是这样的问题是把开关键也屏蔽了,而系统锁屏,是没有屏蔽开关键的,这个问题我还得好好研究一下,有了结果再更新。

附上完整工程:http://download.csdn.net/detail/free092875/7614735
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: