本文来源:支付宝体验科技公众号
🙋🏻♀️ 编者按:本文作者是蚂蚁集团客户端工程师博欢,介绍了支付宝锁屏组件新能力实现背后的技术细节。目前整个锁屏组件正在对接支付宝运动,听书业务。同时也可以接入其他支付宝外部的业务,欢迎交流与合作~
1.技术背景
现在的手机越来越希望将应用信息快速触达到用户,同时提供应用在外部的运营阵地,比如说手机的负一屏,桌面小组件,通知栏,华为的实况胶囊等,如下展示所示:
端外对用户的触达方式是多种多样的,不过对于用户而言, 用户在支付宝内,可以登陆运动小程序选择跑步骑行,也可以在出行中使用乘车码出行码等。对于用户而言,保持手机在 app 应用内部常亮是一件麻烦的事情,息屏后解锁再进入 app 查看同样麻烦。锁屏组件可以做到息屏后锁屏保持展示应用内数据,支持快速滑动删除锁屏组件。
也许这只是一个很简单的能力,但是在支付宝这个国民级 app 面前,一个微小的事情也会变得复杂起来。我们充分考虑了锁屏组件的生命周期感知,多业务注册过程中的优先级,耗电问题,快速接入等问题,为需要的业务方提供一个独立而完备的功能组件。
2.上机效果
我们依托支付宝运动(测试 case)和出行业务(测试 case),展示锁屏组件在息屏后重新唤起界面的效果(以下非正式上线效果,仅展示接入效果)。
左侧是运动小程序(测试 case)在息屏后展示在锁屏界面上的效果。其中会展示运动时间,里程,配速数据,与小程序跑步界面数据一致,同时支持用户暂停重启的按钮操作。底部的滑动解锁后,会进入解锁页面(用户有登陆密码锁或系统锁情况下),解锁后会进入息屏前的支付宝运动小程序跑步界面。
右侧是接入支付宝出行功能(测试 case)后,息屏后按下电源键拉起展示的锁屏界面。用户手机息屏后不用重新解锁进入到支付宝出行内展示二维码,对于用户来讲,极大提升了出行体验。对于出行二维码这种情形,支持二维码更新,电源键按下指定次数后不再展示等特殊业务逻辑。
3.技术细节
锁屏在技术实现上主要讨论锁屏监听,锁屏悬浮窗弹出,以及技术优化项。针对三方业务接入具有高度可定制化,可以浏览生命周期感知和多业务注册管理章节。
3.1 锁屏监听
监听系统的锁屏可以通过以下两种方式:1) 代码直接判定 2) 接收广播。代码直接判定是主动型,在需要的时候进行判断是否已锁屏。接收广播是被动型,当监听到锁屏时,才触发对应的操作。对于锁屏组件,接收锁屏广播是更为合理的方式。
当安卓系统锁屏或者屏幕亮起,或是屏幕解锁的时候,系统内部都会发送相应的广播,只需要对广播进行监听就可以。接收到广播之后再触发对应的业务逻辑 ;
private ScreenBroadcastReceiver mScreenReceiver; private class ScreenBroadcastReceiver extends BroadcastReceiver { private String action = null; @Override public void onReceive(Context context, Intent intent) { action = intent.getAction(); if (Intent.ACTION_SCREEN_ON.equals(action)) { // 开屏 } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { // 锁屏 } else if (Intent.ACTION_USER_PRESENT.equals(action)) { // 解锁 } } } private void startScreenBroadcastReceiver() { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); context.registerReceiver(mScreenReceiver, filter); }
3.2 锁屏悬浮窗弹出
在屏幕息屏按下电源键,手机展示锁屏界面后弹出悬浮窗页面,这个的实现方式同样也有两种:1) 使用WindowManager;2) 使用 Activity 。
WindowManager 在创建对象时,需要准备好悬浮窗页面的各项 UI 参数,如下图所示(不涉及具体业务);这种实现方式不满足三方业务的快速接入以及高度定制化。
WindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mWmParams = new WindowManager.LayoutParams(); // 设置图片格式,效果为背景透明 mWmParams.format = PixelFormat.RGBA_8888; // 设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作) mWmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; // 调整悬浮窗显示的停靠位置为左侧置 mWmParams.gravity = Gravity.LEFT | Gravity.TOP; mScreenHeight = mWindowManager.getDefaultDisplay().getHeight(); // 以屏幕左上角为原点,设置x、y初始值,相对于gravity mWmParams.x = 0; mWmParams.y = mScreenHeight / 2;
使用 Activity 方式,onCreate 方法中需要进行添加标志,才可以在锁屏状态下弹出悬浮窗页面。如下所示;四个标志位别是锁屏状态下显示,解锁,保持屏幕长亮,打开屏幕。这样当 Activity 启动的时候,它会解锁并亮屏显示。这种方式可以实现一个 Activity 绑定多个页面,理论上是可以支持多业务接入的需求。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Window win = getWindow(); win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); // 自己的代码 }
3.3 技术项优化
- 重复锁屏唤醒弹窗
- 解决问题:再次亮起屏幕,如果该 Activity 并未退出,但是被手动按了锁屏键,当前面的广播接收器再次去启动它的时候,屏幕并不会被唤起,所以需要在 activity 当中添加唤醒屏幕的代码,这里用的是电源锁。可以添加在 onNewIntent(Intent intent),因为它会被调用。也可以添加在其他合适的生命周期方法。
- 实现方式:重写 onNewIntent 方法
PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); if (!pm.isScreenOn()) { PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "bright"); wl.acquire(); wl.release(); }
- 滑动解锁屏
- 解决问题:在锁屏状态,给用户一个销毁当前页面的入口,也给用户一个进入手机解锁页面的入口;
- 实现方式:将 Activity 监听到的触摸事件委托给 GestureDetector 手势识别类的触摸事件处理。
GestureDetector mDetector = new GestureDetector(this, new SimpleOnGestureListener(){ //简单的手势侦听。 @Override //fling 监听手势滑动事件。在这里e1表示手势滑动的起点,e2 表示滑动手势的终点; // velocityX : 表示水平方向滑动的速度 //velocityY 竖直方向滑动的速度。 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO Auto-generated method stub return super.onFling(e1, e2, velocityX, velocityY); } });
4.快速接入
锁屏组件是一个能力,对于业务而言更为关注的是如何接入组件,如何完成数据更新,如何结束组件。
整体的业务调用流程图如下所示:
4.1 注册与取消注册
对于三方业务而言,提供一个统一的管理类,支持业务注册以及取消注册。业务注册组件后,就可以使用锁屏组件的能力。取消组册后便可以结束锁屏组件。注册与取消注册的代码如下:
// 三方业务注册组件,实现对应接口 public void registerCustomHandler(final LockScreenCustomInterface customInterface) { uiHandler.post(new Runnable() { @Override public void run() { try { // 如果已经注册,则根据业务ID取消已经注册的能力,重新为业务注册 unregisterCustomHandlerOnUI(customInterface.getBizId()); // 统一管理三方业务接口 customList.add(customInterface); // 注册锁屏广播 registerScreenBroadcast(); } catch (Exception e) { LoggerFactory.getTraceLogger().error(TAG, e); } } }); } // 三方业务取消注册 public void unregisterCustomHandler(final LockScreenCustomInterface customInterface) { uiHandler.post(new Runnable() { @Override public void run() { // 取消已经注册三方业务 unregisterCustomHandlerOnUI(customInterface.getBizId()); // 清理广播 if (customList == null || customList.isEmpty()) { unregisterScreenBroadcast(); } } }); }
4.2 UI 与数据
三方业务的定制化 UI 有两种方式,第一种方式是业务本身提供 UI 资源文件,第二种方式采用组件提供的通用 UI 样式文件。两种 UI 实现方式,三方业务在注册锁屏组件后,监听锁屏生命周期完成数据初始化和更新。
三方业务可以在定制区域内实现引导,动效等特殊需求。三方业务定制化的 UI 展示区域如下图所示:
对于三方的业务数据初始化和更新会有统一的数据管理类 BgServiceDataManager 来管理,数据类会与注册时的业务 ID 进行绑定。三方业务在对应的锁屏组件生命周期内,提供数据到管理类,由管理类完成数据的初始化和更新操作。
5.生命周期感知
为了能让业务有更多的扩展性,满足三方业务的特殊需求,必须要让三方业务感知到锁屏组件的生命周期,这样三方业务就可以有针对性的完成业务特殊逻辑,同时又可以避免不同业务之间的逻辑干扰。
目前提供的锁屏组件生命周期包括:
- onCreate:锁屏创建。三方业务注册后,会进行锁屏组件的创建,创建锁屏组件需要指定UI模板以及绑定数据类;
- onShow:锁屏界面展示。当三方业务进行锁屏界面展示时,会触发该方法;
- onHide:手机息屏。用户在锁屏界面按下电源键,手机界面进入息屏状态,则锁屏组件隐藏;
- onUpdateData:数据更新。该接口是定时触发接口,默认一秒内锁屏数据更新一次,时间支持定制;
- onSubscribed:注册。三方业务注册组件后会触发;
- onUnSubscribed:取消注册。三方业务取消注册后触发;
- onObserve:可观察。用户在锁屏界面上的操作,会由此接口透传给三方业务;
- onDestroy;销毁。滑动解锁,或者电源键按下指定次数后销毁锁屏组件;
有了生命周期时间,三方业务可以更好的控制锁屏界面。例如用户在锁屏界面暂停运动跑步,用户的操作由 onObserve 透传给三方业务感知,三方业务的数据更新操作便可暂停。
6.多业务注册管理
锁屏组件的能力并非是提供给单个业务的,同一个时间可能有多个业务同时注册。比如说,用户在支付宝听书小程序听书,但是准备下车,想刷出行码,在锁屏上如何展示?这就涉及到优先级的问题。
在锁屏组件的设计内,有一个三方业务展示原则:高优先级优先展示,同优先级先后倒序展示。
如何判断优先级?如何判断先后?在锁屏组件内会有是两个优先级:High 高优先级 > Low 低优先级。三方业务在接入后锁屏组件后,会为其分配一个优先级,默认为High高优先级。但是针对音频播放类会较长时间停留在锁屏页面的业务,这类业务默认为常驻业务,会强制分配低优先级,避免抢占高优先级业务界面。
针对同等优先级的三方业务,依据注册时间倒序来展示。新注册的三方业务会抢占锁屏页面,当新注册的三方业务完成锁屏展示之后会切换回原先注册的三方业务界面。同等优先级的抢占一般会发生在高优先级的三方业务中,针对低优先级的常驻业务一般不会发生抢占问题。
整体的三方业务由统一业务栈来管控,优先级高的,时间注册晚的三方业务会放在离栈顶更近的位置,如下图所示: 业务管理栈
7.耗电分析
国内 android 厂商会对耗电大的三方应用进行管控,所以锁屏组件的耗电问题也会受到质疑。一个长期保持屏幕亮度的行为对手机的耗电真实表现如何,我们对锁屏组件的耗电进行了监测分析。
我们使用的是 Battery Historian 工具。在真实的测试环境中,我们使用接入锁屏组件的 demo 持续亮屏 30 分钟,具体的电池分析数据如下分析。
- 首先是整体的手机设备各项指标如下,未见有明显的耗电下降异常;
- 手机设备的各项详细数据,第一行可见手机屏幕保持亮度时间为 30 分钟以上;
- 针对我们的测试 demo 使用手机设备的各项数据,在 30m 的锁屏过程中,耗电量占比 0.01%;
- 测试 demo 使用系统锁的情况,总共使用获取系统锁 2 个;
整体的耗电测试,手机未出现发热使用卡顿的问题,耗电占比在整个电池电量使用中几乎忽略不计。
8.行业案例
8.1 航旅纵横
登机所用的航旅纵横 app,在登机时手机展示登机牌页面。由于排队过程较长,手机会息屏。但是按下电源键,手机的锁屏面又可以快速展示登机牌页面,避免了用户重新进入 app 找到登机牌。
8.2 keep
不少人有用 keep 软件锻炼的习惯,比如跑步等。同样,在跑步的过程中在指定时间后手机会进入息屏状态,跑步数据页面也会展示消失。按下电源键,跑步数据界面会重新展示在锁屏界面上,同时滑动解锁便可以回到手机主界面。如下图所示:
9.业务招商
目前整个锁屏组件正在对接支付宝运动,听书业务。同时也可以接入其他支付宝外部的业务。锁屏组件支持高度定制化,对于需要保持持续输出数据到用户的业务是一个很好的功能体验。
欢迎大家与我们沟通,为更好的支付宝,更好的用户体验而努力。