Android 自定义锁屏的实现
2014-07-08 11:34
435 查看
最近公司无事,所以找点事干。刚好在研究view和viewgroup这部分的源码,也尝试重写一些view和viewgroup加深理解。看到网上有人写九宫格的手势锁屏,就自己试了试,坐下来感觉难度不大,倒是有很多细节上的东西,需要记录一下,而且过程中也确实学到了不少,进步了不少。
一. 思路
看到网上的同仁,大体是2种方式,一种是直接重写一个view,然后绘制所有的东西,另外一种是重写view绘制圆点,再重写一个viewgroup作为圆点的容器,当然貌似还有其他的实现方式,我没太注意。在这里我采用第二种实现方式。
二. 效果图
随便弄了个下图,感觉看起来顺眼,有时间还得好好再加工一下:
三. 重写view
DotView,其实这个view重写的逻辑很简单,就是设置好画笔,在onDraw()里面画圆圈就行了,代码如下:
STATE_NORMAL,STATE_TOUCHED和STATE_ERROR这3个常量,分别代表圆圈的正常状态,被触摸或点中后的状态,解锁失败以后要呈现的状态;
这2个方法是画内圈的实心圆点,和外圈的空心圆,逻辑也十分简单;
基本上主要代码就是这些了,所以说很简单;
二. 重写Viewgroup
网上很多实现方式都是重写的相对布局或者线性布局,好处就是不用写onLayout()了,我自己重写了viewgroup,不过布局也不难,因为我的锁屏是写死了九宫格,不像其他的实现,是可以任意宫的;
ScreenLockView的主要代码和逻辑就相对多一些:
我在学习过程中,看到2个没用过的方法:mPaint.setStrokeCap(Paint.Cap.ROUND)和mPaint.setStrokeJoin(Paint.Join.ROUND),在网上查找,居然得出很多不同的答案,自己试验了下,基本上是使绘制的线条比较平滑,还有转接的地方比较自然。
在onMeasure()里面把9个圆点视图添加到父容器中,尺寸大小也比较好计算;
至于布局的排列,逻辑上也很简单,按照我的排列方法,id除以3是行号,id取余3是列号,然后计算好位置也很简单;
另外发现一个问题,l,t,是实际使用时,可能是非0的值,因为容器内可能有其他控件占据位置,但是在布局时,从逻辑上要把他们当成坐标轴的x,y去看待,即要认为我们是在圆点为(l,t)的坐标轴内布局,否则视图就会发生偏移。这也是我遇到问题之后,研究了不少时间才解决的。
onTouchEvent()的逻辑相对复杂一些,不过分为按下,移动和抬起来看,调理仍很清晰;
-如果触摸到圆点有效范围,就计算出圆点圆心并设为path的起点;
-如果在移动过程中,就按照触摸点的位置,绘制路径;
-一旦抬起,就结束绘制,刷新界面;
setPath()是将已经触摸过的圆点,依次绘制路线连接起来;
重写dispatchDraw()来绘制路线;
error()方法被调用,当用户输入错误密码的时候,它将所有选中圆点和路径显示为红色,并在1秒后,全部清除;
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
一. 思路
看到网上的同仁,大体是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
相关文章推荐
- Android自定义锁屏实现----仿正点闹钟滑屏解锁
- Android实现自定义锁屏控制
- Android自定义锁屏实现----仿正点闹钟滑屏解锁
- Android自定义锁屏实现----仿正点闹钟滑屏解锁
- Android实现自定义锁屏控制
- Android实现自定义锁屏控制
- Android自定义锁屏的实现
- 自定义程序实现Android EditText只允许输入指定字符
- Android 新的锁屏接口的实现
- Android开发中自定义View设定到FrameLayout布局中实现多组件显示
- 控制Android系统 全屏并且 程序开机自动运行 并且实现程序运行中 开机不锁屏
- android 自定义ListView 实现 弹出自定义对话框(带EditText)实现 配置文件实现
- android 自定义SeekBarPreference 实现
- 【原创】Android中ImageButton自定义按钮的按下效果的代码实现方法,附网上2种经典解决方法。
- Android实现自定义铃音
- 【Android游戏开发二十三】自定义ListView【通用】适配器并实现监听控件!
- PopupWindow进阶用法——android上实现类似UCweb的自定义menu,完全模拟系统事件
- Android中ImageButton自定义按钮的按下效果的代码实现方法,附网上2种经典解决方法。
- Android自定义button的实现,未选中,按下,选中效果
- 【Android游戏开发二十三】自定义ListView【通用】适配器并实现监听控件!