1. 背景
在我们的业务场景中,用户在完成下单后大部分的概率不再需要进入App
做其他的操作,只需要知道当前的订单状态,为了方便用户在不解锁的情况下也能实时查看当前订单的状态,货拉拉用户 iOS端上线了灵动岛功能,用户的接受度较高,由于Android暂不支持灵动岛,所以我们自定义了一个锁屏页面。
2. 实践
2.1 方案选择
实现锁屏的方式有多种(锁屏应用、悬浮窗、普通Activity伪造锁屏等等),由于我们的业务场景简单只展示我们的订单状态,且不需要很强的保活干扰用户的操作,采用了普通的Activity伪造锁屏。
2.2 方案原理
锁屏的大概实现原理都很简单,监听系统的亮屏广播,在亮屏的时候展示自己的锁屏界面,自定义的锁屏界面会覆盖在系统的锁屏界面上,用户在自定义锁屏界面上进行一系列的动作后进入系统的解锁界面。
2.3 代码实现
2.3.1 锁屏页面
锁屏页Activity
在普通的Activity
需要加上一些配置
1. 在onCreate
中设置添加Flags
,让当前Activity
可以在锁屏时显示
FLAG_SHOW_WHEN_LOCKED
:使Activity
在锁屏时仍然能够显示FLAG_DISMISS_KEYGUARD
:去掉系统锁屏页,设置了系统锁屏密码是没有办法去掉的,现在手机一般都会设置锁屏密码,该配置可基本忽略。
kotlin
this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
)
2. 在AndroidManifest.xml
中进行对锁屏页Activity
进行配置
- 主题配置,
主要是配置锁屏Activity
的背景为透明和去除过度动画,让锁屏Activity
过渡到系统锁屏更自然
ini
<style name="LockScreenTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowContentOverlay">@null</item>
</style>
-
启动模式配置
- 在
BroadcastReceiver
中启动锁屏页Activity
,需要添加Intent.FLAG_ACTIVITY_NEW_TASK
的flag
,造成锁屏Activity
单独创建一个history stack
,会在最近任务中显示出来,通过配置excludeFromRecents
,noHistory
,taskAffinity
来规避这个问题。
ini<activity android:name=".lockscreen.LockScreenActivity" android:configChanges="uiMode" android:excludeFromRecents="true" android:exported="false" android:launchMode="singleInstance" android:noHistory="true" android:screenOrientation="portrait" android:taskAffinity="com.xxx.lockscreen" android:theme="@style/LockScreenTheme"> </activity> ```
- 在
3. Home键,Back键和Menu键事件的处理
-
Home
键,由于不是用来替代系统锁屏的锁屏软件,不需要处理Home
键事件. -
Back/Menu
键,重写onKeyDown
让锁屏页不处理这两个事件kotlinoverride fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { return when (event?.keyCode) { KeyEvent.KEYCODE_BACK -> true KeyEvent.KEYCODE_MENU -> true else -> super.onKeyDown(keyCode, event) } } ```
2.3.2 广播
LockScreenBroadcastReceiver
是普通的BroadcastReceiver
,不做其他的配置,需要注意两点:
- 动态注册/注销
- 在广播中启动
Activity
,需要添加FLAG_ACTIVITY_NEW_TASK
,否则会出现"Calling startActivity() from outside of an Activity"
的运行时异常
kotlin
class LockScreenBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let { handleCommandIntent(context, it) }
}
private fun handleCommandIntent(context: Context?, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_OFF -> {
val lockScreen = Intent(this, LockScreenActivity::class.java)
lockScreen.setPackage("com.xxx.xxx")
lockScreen.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK )
context?.startActivity(lockScreen)
}
Intent.ACTION_USER_PRESENT -> {
// 处理解锁后才显示自定义锁屏Activity
}
}
}
}
2.3.3 实现效果
3. 注意点
以下是在实践过程中的一些问题小结,供大家参考。
3.1 权限相关
不同手机系统上权限的名称,大体分为5种:
- 后台弹窗
- 悬浮窗
- 显示在其他应用的上层
- 锁屏展示
- 后台弹出界面
以及不同的组合效果也不同,以下是已测试过的手机,
品牌 | 型号 | 系统 | 系统版本 | 相关权限 | 权限截图 | 权限截图 |
---|---|---|---|---|---|---|
华为 | P50 | HarmonyOS | HarmonyOS 4.0.0 | 1. 悬浮窗 2.后台弹窗 | ||
oppo | OPPO K9 5G | ColorOS 13 | Android 13 | 1. 悬浮窗 2. 锁屏显示 | ||
vivo | Y52s | Funtouch OS 10.5 | Android 10 | 1. 悬浮窗 2. 锁屏显示 3. 后台弹出界面 | ||
一加 | OnePlus Ace Pro | ColorOS 13 | Android 13 | 1. 悬浮窗 2. 锁屏显示 | ||
荣耀 | honor 60 | magic ui 6.1 | Android 12 | 1. 显示在其他应用的上层 | ||
iQOO | Neo3 | Origin OS | Android 12 | 1. 悬浮窗 2. 锁屏显示 3. 后台弹出界面 | ||
Hi nova | Hi nova 9 | Emui 12 | Android 12 | 1. 后台弹窗 2. 悬浮窗 3. 显示在其他应用的上层 |
OPPO/一加 手机特殊说明 :在默认状态下在系统设置下找不到"锁屏显示 "的入口,需要先授权"悬浮窗"权限再次启动应用会在应用启动时弹窗提示授权在锁屏上显示,然后在系统设置中会出现"锁屏显示"的入口。
3.2 有些手机在未授权时,应用在前台时锁屏可以展示,但是应用退到后台不展示。
Android 10
(API
级别 29
) 及更高版本对后台应用可启动Activity
的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。具体见官方文档
3.3 在部分手机上,点亮屏幕后不会立即展示自定义的锁屏界面,在解锁系统锁屏后才会展示自定义的锁屏。1. 监听解锁事件主动finish
自定义的锁屏页面
scss
Intent.ACTION_USER_PRESENT->{
ActivityUtils.getActivityList()?.forEach {
if ("com.xxx.lockscreen.LockScreenActivity" == it.componentName.className) {
it.finish()
}
}
}
- 在自定义锁屏
Activity
的onResume
中监听设备是否已解锁并finish
锁屏页
kotlin
override fun onResume() {
super.onResume()
val isInteractive = (getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive
val isKeyguardLocked = (getSystemService(KEYGUARD_SERVICE) as KeyguardManager).isKeyguardLocked
if (isInteractive && !isKeyguardLocked) {
finish()
}
}
3.4 当在自定义锁屏页触发Home
键事件后,锁屏页Activity
不再显示
提示用户根据自己的系统去授予对应的权限,不同系统所需的权限参考上面第1点
3.5 Android 8.0 透明主题造成闪退
在Android 8.0
系统上Activity
满足了以下条件:
- targetSdkVersion > 26
- 透明主题
- 固定屏幕方向
会出现java.lang.IllegalStateException: Only fullscreen activities can request orientation
arduino
// ActivityRecord.java
void setRequestedOrientation(int requestedOrientation) {
if (ActivityInfo.isFixedOrientation(requestedOrientation) && !fullscreen
&& appInfo.targetSdkVersion > O) {
throw new IllegalStateException("Only fullscreen activities can request orientation");
}
....
}
建议针对Android 8.0
以外的系统才固定屏幕方向,可参考Android 8.0系统透明主题适配解决办法
4. 总结
从线上最新的数据来看,接近60%的订单在锁屏后可以通过自定义锁屏查看到订单状态。
功能上线后发现比较少用户会主动选择关闭,从最开始的出发点就是为用户提供一个便捷的状态查看的入口,用户下完单等待司机接单以及接单后司机的状态都是用户会重点关注的,同时我们会过滤掉一些不太重要的状态的显示避免对用户带来不必要的干扰。
从实现的角度上来说整体较简单,较麻烦的是国内的ROM对权限的管控越来越严,且不同的系统同一权限的命名和授予方式差异较大,需要用更吸引用户的体验去引导用户授权。