一、自定义锁屏基本原理
二、重要步骤
1、广播注册
2、Activity设置
3、按键的屏蔽
4、滑屏解锁
5、Event bus的使用
三、出现的问题
1、小米和魅族等手机锁屏权限问题
2、透明栏与沉浸模式
3、手机适配
4、处理黑色闪屏
5、线控耳机
6、 Android 上的「安全音量」
一、自定义锁屏基本原理
先上效果图:
实现锁屏的方式有多种(锁屏应用、悬浮窗、普通Activity伪造锁屏等等)。通过网络查找资料与反编译云音乐apk,本项目使用了国内比较主流并且被广泛应用的Activity伪造锁屏方式。
Activity实现自定义锁屏页的思路很简单,即在听书模式开启时,启动一个service,在service中监听系统SCREEN_OFF的广播。当屏幕熄灭时service监听到广播,开启一个锁屏页Activity在屏幕最上层显示,该Activity创建的同时会去掉系统的锁屏(如果有密码是禁不掉的)。示意图如下:
二、重要步骤
1、广播注册
LockScreenService是普通的Service,在应用启动听书模式时候startService(ReadBookActivity),与应用同一个进程。
此外,SCREEN_OFF广播监听必须是动态注册的,如果在AndroidManifest.xml中静态注册将无法接收到SCREEN_OFF广播。
标志位FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,是为了避免在最近使用程序列表出现Service所启动的Activity。
启动Activity时Intent的Flag,如果不添加FLAG_ACTIVITY_NEW_TASK的标志位,会出现“Calling startActivity() from outside of an Activity”的运行时异常,因为我们是从Service启动的Activity。Activity要存在于activity的栈中,而Service在启动activity时必然不存在一个activity的栈,所以要新起一个栈,并装入启动的activity。使用该标志位时,也需要在AndroidManifest中声明taskAffinity,即新task的名称,否则锁屏Activity实质上还是在建立在原来App的task栈中。
2、Activity设置
锁屏的activity内部也要做相应的配置,让activity在锁屏时也能够显示,同时去掉系统锁屏。当然如果设置了系统锁屏密码,系统锁屏是没有办法去掉的,这里考虑没有设置密码的情况。我们在自定义锁屏Activity的onCreate()方法里设定以下标志位就能完全实现相同的功能:
FLAG_DISMISS_KEYGUARD用于去掉系统锁屏页,FLAG_SHOW_WHEN_LOCKED使Activity在锁屏时仍然能够显示。另外需要在Manifest中加入适当的权限:
3、按键的屏蔽
当自定义锁屏页最终出现在手机上时,我们希望它像系统锁屏页那样屹立不倒,所有的按键都不能触动它,只有通过划屏或者指纹才能解锁,因此有必要对按键进行一定程度上的屏蔽。针对只有虚拟按键的手机,我们可以通过隐藏虚拟按键的方式部分解决这个问题。但是当用户在锁屏页底部滑动,隐藏后的虚拟按键还是会滑出,而且如果用户是物理按键的话就必须进行屏蔽了。需要重写Activity的onBackPressed()方法即可。
Home键 与Recent键的点击事件是在framework层进行处理的,因此onKeyDown()与dispatchKeyEvent()都捕获不到点击事件。关于这两个按键的屏蔽方法,网上相关的资料有很多,有的用到了反射,有的通过改变Window的标志位和Type等,总的来说这些方法只对部分android版本有效,有的则完全无法编译通过。其实这么做的目的无非是为了实现一个纯粹的锁屏页,但是这种做法容易造成锁屏页的异常崩溃,我们要满足的是用户在锁屏页的快捷操作,Home键和Recent键无关痛痒,基本可以不管。
4、滑屏解锁
做完以上几步,当屏幕熄灭后,再打开屏幕就能够看到我们的自定义锁屏页了。接下来要实现划屏解锁。划瓶解锁的基本思路很简单,当手指在屏幕上滑动时,拦截并处理滑动事件,使锁屏页面随着手指运动,当运动到达一定的阈值时,用户手指松开手指,锁屏页自动滑动到屏幕边界消失,如果没有达到运动阀值,就会自动滑动到起始位置,重新覆盖屏幕。 为了将划屏逻辑与页面内容隔离开来,我们在锁屏页面布局中添加一个自定义的UnderView,这个UnderView填充整个屏幕,位于锁屏内容View(将其引用称之为mMoveView,并传入到UnderView中)的下方,所有划屏相关的事件都在这里拦截并处理。
mMoveView是锁屏页的显示内容,除了处理一些简单的点击事件,其他非点击事件序列都由底层的UnderView进行处理。只需要重写UnderView的onTouchEvent方法就能够实现
其中,mStartX记录滑动操作起始的x坐标,handleMoveView方法控制mMoveView随手指的移动,doTriggerEvent处理手指离开后mMoveView的移动动画。两个方法的定义如下:
在handleMoveView()中,首先计算当前触点x坐标与初始x坐标mStartX的差值movex,然后调用mMoveView的setTranslationX方法移动。此外,我们可以通过getBackground()获取UnderView的背景,并根据已划开屏幕占整个屏幕的百分比调用setAlpha方法改变背景的透明度,做出抽屉拉开时的光影变化效果。
当手指离开屏幕,doTraiggerEvent方法会对滑动的距离与阀值进行一个比较,此处的阀值为0.4*屏幕宽度,如果低于阀值,则通过ObjectAnimator在0.25s将mMoveView移动到初始位置,同时在ObjectAnimator的AnimatorUpdateListener的onAnimationUpdate方法中更新背景透明度;如果低于阀值,以同样的方式将mMoveView移出屏幕右边界,然后将Activity干掉,具体做法是为animator增加一个AnimatorListenerAdapter的监听器,在该监听器的onAnimationEnd方法中使用在Activity中定义的mHandler发送finish消息,完成解锁。
5、Event bus的使用
锁屏Activity中的Buttun,可以通过EventBus远程控制ReadBookActivity中的播放、暂停、下一首等操作。
二、出现的问题
1、小米和魅族等手机锁屏权限问题
例如小米手机(miui 6.8.18 开始)有锁屏权限的问题,权限未开的情况下锁屏会在系统锁屏的下方。目前只能手动打开(个别应用在MIUI是默认打开)。
在设置中:
2、透明栏与沉浸模式
透明栏与沉浸模式总共用到了5个Flag,SYSTEM_UI_FLAG_LAYOUT_STABLE保持整个View稳定,使View不会因为SystemUI的变化而做layout。SYSTEM_UI_FLAG_IMMERSIVE_STIKY,能够在隐藏的bar被呼出时(比如从屏幕下边缘开始向上做滑动手势),使bar在无相关操作的情况下自动再次隐藏。对于SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,我们容易被其中的HIDE_NAVIGATION所迷惑,其实这个Flag没有隐藏导航栏的功能,只是控制导航栏浮在屏幕上层,不占据屏幕布局空间。SYSTEM_UI_FLAG_HIDE_NAVIGATION,才是能够隐藏导航栏的Flag。SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN也不能隐藏状态栏,只是使状态栏浮在屏幕上层。
需要注意的是,这段代码除了需要加在Activity的OnCreate()方法中,也要加在重写的onWindowFocusChanged()方法中,在窗口获取焦点时再将Flag设置一遍,否则可能导致无法达到预想的效果。
在 Android 5.0 之后状态栏和导航栏也有更多的特点。除了原有的“半透明”模式以外,还有“全透明”以及“变色”模式,一种会完全隐藏背景,另一种可以取色作为背景颜色等。对于Android 4.4以上5.0以下的版本,设置透明状态栏的方式如下:
除了要清理掉4.4的FLAG_TRANSLUCENT_STATUS外,还要配合SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE,添加标志位FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,并调用setStatusBarColor设置状态栏的颜色为透明。
3、手机适配
由于Android 手机屏幕高度差异比较大,所以有可能会存在要显示的控件高度比屏幕还高的问题。所以需要通过代码对布局中某些控件宽高进行等比例调整。
例如在本项目中通过固定音量键以上的高度和最下方滑动解锁的位置,来动态等比例动态调整中间的专辑封面图片。mMain.post(new Runnable() 。
4、处理黑色闪屏
我们的锁屏Activity在滑动”解锁”之后, 理论上是直接进入下面的界面, 但有时如果下面不是launcher, 而是一个app, 有可能会闪一下黑屏, 这个其实是底下activity的入场动画导致的, 某些Android版本会对顶部activity透明时处理有些奇怪, 不能保证其他的应用不闪黑屏, 但是对自己的的应用还是可以的, 只需要在主体activity的style中加上
5、线控耳机
我们只要定义一个广播接收者来接收到这个广播。这个广播的意图是 android.intent.action.MEDIA_BUTTON 。注册普通的广播接收器有两个常见的办法,一种是在代码中动态注册,另一种是在项目Manifest里面注册。但是这个广播,要注册两遍、两遍、两遍,Manifest里一遍(常规办法),代码中一遍(借助多媒体服务注册),否则没有效果。
因为两种注册方式缺一不可,所以解除了一种,它的监听作用也就失效了。
6、 Android 上的「安全音量」
当 Android 设备插上耳机,为了避免音量过高伤害用户听力,会触发其“安全音量”(Safe Media Volume)机制,如果在未经用户确认允许使用大音量时,且这时设置音量 newIndex 超过其推荐阈值,则这段代码执行完你会发现毫无反应,播放的声音依然不会很大。
解决问题的关键在于被忽略的最后一个参数 flags 。只要在设置音量后,复查一次当前值是否相当,如果比较小,则交由系统来显示音量提示对话框。而此时因欲设定的值超过推荐值,一般会触发音量过高警告,提示用户用户确认后即可设置成功。
本文来自网易实践者社区,经作者吴思博授权发布。