小米开发平台刘海屏、水滴屏 Android O 适配

小米开发平台刘海屏、水滴屏 Android O 适配

1. 背景

1.1. 目前已上市的小米 Notch 设备(俗称刘海屏手机)如下,其宽度、高度和形状均略有差异

机型modeldevice分辨率Notch高度Notch宽度DPI
小米8MI 8dipper1080*224889560440
小米8 SEMI 8 SEsirius1080*224485540440
小米8 透明探索版MI 8 Explorer Editionursa1080*224889560440
小米屏幕指纹版MI 8 UDequuleus1080*224889560440
小米青春版MI8Liteplatina1080*228082296440
小米POCO F1POCO F1beryllium1080*224686588440
红米6 ProRedmi 6 Prosakura1080*228089352440

注:以上设备,由于MIUI调整了DPI值,因此DP值与像素值的转换关系是 1dp = 2.75 px 。

1.2. 含 Notch 往往都是全面屏手机,即屏幕比例可能是18:9、18.7:9 等不同的值

全面屏设备的适配建议详见:https://dev.mi.com/console/doc/detail?pId=1160

1.3. Android P 提供了 刘海屏的标准适配接口

MIUI 也将在 Android P 上采用标准接口,因此,下文提及的接口仅用于 Android O 上。关于 Android P 的接口说明,详见https://dev.mi.com/console/doc/detail?pId=1341

1.4. 如下图,为便于说明,我们将顶部区域定义为 Notch 和耳朵区

上述两种屏幕都可以统称为刘海屏,不过对于右侧较小的刘海,业界一般称为水滴屏或美人尖。为便于说明,后文提到的「刘海屏」「刘海区」都同时指代上图两种屏幕。

2. 系统级适配规则

Notch 机型在界面上会带来两个问题:

  • 顶部内容会被 Notch 遮挡
  • 如何处理耳朵区的显示区域

为了保证绝大部分应用都能正常显示,同时尽可能利用屏幕的显示区域。MIUI System UI 制定了以下全局规则

  • status bar 略高于 Notch 高度,对于应用来说,相当于一个更高的 status bar。
  • 当应用显示 status bar 时(如微信首页),允许应用使用耳朵区(背后的逻辑是:因为 status bar 区域本身不可交互,且会显示信号、电池等信息,因此我们假定应用不会在该区域放置重要的内容和可交互的控件)。
  • 当应用不显示 status bar 时(如全屏游戏),不允许应用使用耳朵区,系统默认填黑。
  • 横屏时,默认均不允许使用耳朵区,系统默认填黑。
  • 不允许应用180度倒转显示。

注:上述规则的模拟效果对比图,可以参见文末的附录“Notch 屏系统默认规则介绍”。

3. 开发者适配

系统规则只能解决最基础的可用性问题,在系统规则下,开发者仍需要检查以下内容:

  • 检查系统默认规则是否有可用性问题,考虑是否做针对性优化。
  • 检查 status bar 的显示策略。重新考虑是否隐藏 status bar
  • 尽量避免某些页面显示 status bar,某些页面又隐藏,否则会出现页面跳变的情况(应用的可用高度变了)。
  • 检查横屏的情况,确定是否需要利用横屏的Notch,若使用,需兼顾 Notch 出现在左边/右边的情况。
  • 检查是否写死了状态栏的高度值。Notch机器状态栏的值是变化的,建议改为读取系统的值(后有相关方法说明)。
  • 检查开启「隐藏屏幕刘海」后,应用是否显示异常(详见后文)。
  • 检查普通屏幕的显示,保证应用在普通屏幕和 Notch 屏幕下都能正常显示 。

4. 系统接口说明

若开发者对系统规则下的效果不满意,可以调用以下接口,做针对性的优化。

4.1. 如何判断设备为 Notch 机型

系统增加了 property ro.miui.notch,值为1时则是 Notch 屏手机。

SystemProperties.getInt("ro.miui.notch", 0) == 1;

4.2. 如何获取 Notch / 凹口 / 刘海 的高度和宽度(截至2018.6.26)

MIUI 10 新增了获取刘海宽和高的方法,需升级至8.6.26开发版及以上版本。

以下是获取当前设备刘海高度的方法:

int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}

以下是获取当前设备刘海宽度的方法:


int
resourceId = context.getResources().getIdentifier("notch_width", "dimen", "android"); if (resourceId > 0) { result = context.getResources().getDimensionPixelSize(resourceId); }

4.3. Application 级别的控制接口

如果开发者认为应用的所有页面统一处理就行,可以使用该接口。在 Application 下增加一个 meta-data,用以声明该应用是否使用耳朵区。示例如下:

<meta-data
 android:name="notch.config"
 android:value="portrait|landscape"/>

其中,value 的取值可以是以下4种:

"none" 横竖屏都不绘制耳朵区

"portrait" 竖屏绘制到耳朵区

"landscape" 横屏绘制到耳朵区

"portrait|landscape" 横竖屏都绘制到耳朵区

注:一旦开发者声明了meta-data,系统就会优先遵从开发者的声明。

4.4. Window 级别的控制接口

如果开发者希望对特定 Window 作处理,可以使用该接口。 在 WindowManager.LayoutParams 增加 extraFlags 成员变量,用以声明该 window 是否使用耳朵区。

其中,extraFlags 有以下变量:

0x00000100 开启配置
0x00000200 竖屏配置
0x00000400 横屏配置

组合后表示 Window 的配置,如:

0x00000100 | 0x00000200 竖屏绘制到耳朵区
0x00000100 | 0x00000400 横屏绘制到耳朵区
0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区

控制 extraFlags 时注意只控制这几位,不要影响其他位。可以用 Window 的 addExtraFlags 和 clearExtraFlags 来修改, 这两个方法是 MIUI 增加的方法,需要反射调用。

int flag = 0x00000100 | 0x00000200 | 0x00000400;
try {
    Method method = Window.class.getMethod("addExtraFlags",
            int.class);
    method.invoke(getWindow(), flag);
} catch (Exception e) {
    Log.i(TAG, "addExtraFlags not found.");
}

4.5. 状态栏高度获取方法

由于 Notch 设备的状态栏高度与正常机器不一样,因此在需要使用状态栏高度时,不建议写死一个值,而应该改为读取系统的值。

以下是获取当前设备状态栏高度的方法:

int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}

4.6. 如何测试和联系我们

使用 小米 8 等任意有刘海的小米设备,升级到  MIUI 10 最新开发版即可(若开发版为Android P,请参见 Android P 的刘海适配文档),支持的机型和下载链接如下(找到对应机型的最新开发版):

4.7. 如何联系我们

可以邮件给我们的项目组 miuishell@xiaomi.com,会有同事解答相关疑问。

5. “隐藏屏幕刘海”适配

MIUI 针对 Notch 设备,有一个“隐藏屏幕刘海”的设置项(设置-全面屏-隐藏屏幕刘海),具体表现是:系统会强制盖黑状态栏(无视应用的Notch使用声明),视觉上达到隐藏刘海的效果。但会给某些应用带来适配问题(控件/内容遮挡或过于靠边等)。

因此开发者在适配时,还需要检查开启“隐藏屏幕刘海”后,应用的页面是否显示正常。针对有问题的页面,我们建议:

  • 通过以下方法获取系统状态栏高度,然后据此调整布局,而不是写死布局:
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
	result = context.getResources().getDimensionPixelSize(resourceId);
}
  • 如有需要,可以通过查询以下 Global settings 来确定「隐藏屏幕刘海」是否开启了,然后再作针对性优化。
Settings.Global.getInt(mContext.getContentResolver(), "force_black", 0) == 1

6. 关于 Android P 的适配

由于 Android P 已针对异形屏幕提供了标准的接口,因此 MIUI 在后续升级到 Android P 时,将会完全复用 Android P 的接口和逻辑。不过仍有几点值得注意:

  • 由于 MIUI Android O 的接口能力与 Android P 有差异,无法二者完全兼容。所以上述提到的接口,在升级到 Android P 后将不再生效,开发者需要针对 Android P 重新适配,望各位开发者谅解。
  • 同时,由于运行 MIUI Android O 的刘海屏设备还会存在好几年,开发者在代码上,仍需要保留对 MIUI Android O 的适配逻辑。

由于 Android P 的接口公布得较晚,我们没法做到和 Android P 完全兼容。给各位开发者造成不便,再次表示歉意。小米刘海屏 Android P 的适配文档详见 https://dev.mi.com/console/doc/detail?pId=1341。 

7. 附录 – 系统默认规则的说明

如果开发者未做任何声明,系统也会有一套默认的显示规则,下图说的就是这套规则,增加示意图方便大家理解。

小米手机 MIUI 10 通知样式及能力适配说明

小米手机 MIUI 10 通知样式及能力适配说明

MIUI 10 通知栏基于 Android O 做了重构,大大增强了通知栏的能力,分别是:

  • 支持 6 种通知样式
  • 支持 Large icon
  • 支持 Actions
  • 支持快捷回复 (Direct reply) 

1. 前言

  • 以下图片,仅用于示意,线上效果可能会有变化,请在最新 MIUI 版本中测试样式及逻辑。
  • 以下图片示例的都是 MIUI 10 国内版的效果,国际版的布局将会沿用原生 Android 的样式。
  • 以下特性,全部需要开发者自行适配。通知栏只是提供基础能力, 但是否使用由开发者决定。
  • 以下提及的接口均为 Android 标准接口,可在官方文档中查询,但也要注意是从哪个 Android 版本开始支持。
  • 对于使用推送通道的开发者,需要确认相关推送通道是否支持以下能力及相关标准接口,若不支持,则无法使用相关能力。

2. 六种通知样式 (template)

原生 Android O 支持6种通知样式(或称 template),MIUI 10 也将完全兼容这些样式。

(关于原生 Android O 的通知样式介绍,详见 https://material.io/design/platform-guidance/android-notifications.html#templates )

2.1. 标准样式 (Standard template)

标准样式是最常见的样式,也是通知栏的默认样式,有以下特点:

  • 一般包含“标题”、“内容”、“图标”、“时间戳”4个元素
  • 一般无大视图

2.2. 大文本样式 (Big text template)

自 API 16 (Android 4.1) 引入,用以展示更多的文本内容,有以下特点:

  • 有标准视图 (standard view),看起来与标准样式 (Standard template) 一样。
  • 有大视图 (expanded view),有更大的面积,允许展示更多的内容。
  • 通过长按通知来切换标准视图和大视图。

代码示例:

Notification notif = new Notification.Builder(mContext)
   .setContentTitle("特斯拉召回两千余辆 ModelX")
   .setContentText("本次召回范围仅限部分车辆")
   .setSmallIcon(R.drawable.ic_phonelink_ring_primary_24dp)
   .setColor(ContextCompat.getColor(context, R.color.primary_light))
   .setPriority(Notification.PRIORITY_MAX)
   .setVibrate(new long[0])
   .setStyle(new Notification.BigTextStyle()
       .bigText("据上周美联社信息报道,特斯拉已宣布计划在全球范围内召回大约1.1万辆ModelXSUV,原因是后座可能无法锁定。"))
   .build();

该样式还有更多的接口供开发者使用,详见官方文档 https://developer.android.com/reference/android/app/Notification.BigTextStyle。

2.3. 大图样式 (Big picture template)

自 API 16 (Android 4.1) 引入,用以在通知里展示大图,适用于富媒体的内容:

  • 有标准视图 (standard view),看起来与标准样式 (Standard template) 一样。
  • 有大视图 (expanded view),支持展示一张大图,但无法与Big text template 共用,即无法展示长文本。
  • 通过长按通知来切换标准视图和大视图。

代码示例:

Notification notif = new Notification.Builder(mContext)
    .setContentTitle("Unsplash Curation")
    .setContentText("The best new high-res photos from Unsplash. Here’s a few of our team’s favourites from today.")
    .setSmallIcon(R.drawable.new_post)
    .setStyle(new Notification.BigPictureStyle()
        .bigPicture(aBigBitmap))
    .build();

更多的接口详见官方文档 https://developer.android.com/reference/android/app/Notification.BigPictureStyle

2.4. 进度条样式 (Progress template)

自 API 14 (Android 4.0) 引入,有以下特点:

  • 类型1:“无明确进度 (indeterminate)”,即无法预估结束时间,类似于 loading 动画。
  • 类型2:“有明确进度 (determinate)”,可以预估结束时间。
  • 支持标准视图 (standard view),类似标准样式加上了进度条。
  • 支持大视图 (expanded view),一般是展示按钮,如「取消」。
  • 进度条样式可以和 Big text、Big picture 混用(留待各位尝试)

代码示例:

代码层面上,Progress template 并不是一种 Style,无法通过 setStyle 来设置,而是通过 setProgress 来配置,因此才可以和 Big text、Big picture 混用。

以下是两种进度类型的代码示例:

#类型1:indeterminate
Notification notif = new Notification.Builder(mContext)
    .setContentTitle("正在下载安装包")
    .setContentText("即将更新至 MIUI 10")
    .setSmallIcon(R.drawable.ic_phonelink_ring_primary_24dp)
    .setProgress(100,50,true)
    .addAction(cancelAction)
    .build();
#类型2:determinate
Notification notif = new Notification.Builder(mContext)
    .setContentTitle("正在下载安装包")
    .setContentText("下载进度35%")
    .setSubText("还有27分钟")
    .setSmallIcon(R.drawable.ic_phonelink_ring_primary_24dp)
    .setProgress(100,35,false)
    .addAction(cancelAction)
    .build();

更详细的实现说明,也可以看官方文档 https://developer.android.com/training/notify-user/build-notification#progressbar。

2.5. 媒体样式 (Media template)

自 API 21 (Android 5.0) 引入,为音乐、广播等媒体播放设计的通知样式,详细介绍见这个文档 https://dev.mi.com/console/doc/detail?pId=1300 。

2.6. 对话样式 (Messaging template)

自 API 24 (Android 7.0) 引入,为对话式内容设计的通知样式,适用于 IM / 邮件等涉及会话的应用。有以下特点:

  • 支持单聊和群聊两种会话模式。
  • 支持普通视图 (standard view),类似标准样式。
  • 支持大视图 (expanded view),支持显示会话的上下文。

代码示例:

#类型1:单聊
Notification notif = new Notification.Builder()
    .setSmallIcon(R.drawable.ic_phonelink_ring_primary_24dp)
    .setLargeIcon(userAvatar)
    .setStyle(new MessagingStyle("我")
            .addMessage("爱乐之城怎么样", 1, "我")
            .addMessage("很不错啊,金球奖得主呢", 2, "韩梅梅")
            .addMessage("什么时候上映的?最近老在别的地方看到这个电影的介绍,被吊起胃口了", 3, "我")
            .addMessage("中国要情人节才上映", 4, "韩梅梅"))
    .addAction(replyAction)
    .build();
#类型2:群聊
Notification notif = new Notification.Builder()
    .setSmallIcon(R.drawable.ic_phonelink_ring_primary_24dp)
    .setLargeIcon(groupAvatar)
    .setStyle(new MessagingStyle("我")
        .setConversationTitle("这里是群聊名称")
        .addMessage("威少3节41分也是666", 1, "winson")
        .addMessage("落后好多咧", 2, "超")
        .addMessage("其他的不给力", 3, "winson")
        .addMessage("勇士赢是正常,但这不是一场普通的比赛了,大家都打出火气来了", 4, "我"))
    .addAction(replyAction)
    .build();

更多的接口详见官方文档 https://developer.android.com/reference/android/app/Notification.MessagingStyle

上面的例子,大家应该注意到 setLargeIcon 和 addAction 这两个方法,这是用来显示「用户头像」和「回复」按钮的方法。但这两个方法,不限于 Messaging template,可用于所有通知样式中,下面将仔细说明。

3. 支持 Large icon(测试性功能)

Android 通知默认没有 large icon,但开发者可以通过 setLargeIcon 方法 (自 API 11 – Android 3.0 引入) 给通知增加 large icon,丰富通知的内容,尤其适用于 IM / 邮件等场景。

自 MIUI 10 开始,MIUI 通知栏正式支持显示 large icon,示例效果如下。

代码示例:

Notification notif = new Notification.Builder()
    .setContentTitle("韩梅梅")
    .setContentText("中国要情人节才上映")
    .setSmallIcon(R.drawable.ic_chat_black_24dp)
    .setLargeIcon(userAvatar)
    .build();

未设置large icon时,会默认用应用桌面图标代替。有几点值得说明:

  • setLargeIcon 方法适用于所有通知样式,不限于上述例子。开发者可以配合 setStyle 方法,灵活搭配不同的样式。
  • MIUI 系统会自动在 large icon 的右下角添加应用 icon,以标示该通知的来源。因此不要把 large icon 设置为应用图标。
  • 请不要滥用 large icon,我们后续会视情况而定是否收紧这个能力。

4. 支持 Actions

Actions 指的是通知的快捷按钮,该特性自 API 16 (Android 4.1) 引入,并在 API 20 (Android 4.4W) 中更新。

在 MIUI 10,Actions 的样式如下(除媒体通知外,最多可以设置3个 Actions)。

有几点值得说明:

  • 默认不会有 Actions,需要开发者通过 addAction 主动声明。
  • 任何通知样式都可以添加 Actions(效果如下图),开发者可以自由搭配
  • Actions 只能在大视图时显示,在 MIUI 10,除了媒体通知,所有通知默认展示标准视图(标准视图不显示 Actions )。

5. 支持快捷回复 (Direct reply)

快捷回复 (Direct reply) 是 Android 7.0 引入的新功能,开发者适配后,可以支持在通知栏直接回复消息,而不用调起应用,示意图如下:

Direct reply 也不限通知样式(效果如下图),开发者可与不同的通知样式组合搭配。但 Direct reply 依赖于 Actions,因此需要先显示 Actions,才能触发 Direct reply。

关于 Direct reply 的实现方式,可以参考以下资料:

6. FAQ

6.1. 如何判断 MIUI 版本

有以下方法:

android.os.SystemProperties.get("ro.miui.ui.version.code", "7");// 如果返回值是「8」,就是 MIUI 10

android.os.SystemProperties.get("ro.miui.ui.version.name", "");// 如果返回值是「V10」,就是 MIUI 10

6.2. 如何测试

升级到  MIUI 10 最新开发版即可,支持的机型和下载链接如下:

小米MIUI 10 通知类别 (Channel) 适配说明

小米MIUI 10 通知类别 (Channel) 适配说明

1. 什么是通知类别 / Channel

通知类别 (channel) 是 Android O 引入的新功能,旨在解决以下问题:

  • 应用的通知越来越多,给用户造成明显打扰。
  • 但用户只能全局屏蔽这个应用的全部通知,不能屏蔽部分,然后留下对自己有用的。

为了解决这个问题,Android 要求开发者给自己的通知分成若干类,然后允许用户单独屏蔽这个类别的通知。以爱奇艺(v9.5.0) 为例,将通知分成了 4 种 Channel,3 个 Channel Group,如下图:

简单解释一下:

  • Channel:实际的通知类别,爱奇艺分成了4类通知。
  • Channel Group:一组 channel,仅用于做分组区分,没有更多的逻辑。
  • 每个通知类别的权限互相独立,互不影响。例如,通知屏蔽了爱奇艺的「新群聊消息」类通知,「常规推送」类通知依然可以发出来。

Android 已将 Channel 的逻辑纳入 Android Compatibility Definition Document (CDD) 中,意味着所有 Android 厂商都必须支持。因此 MIUI 10 也将兼容和支持相关逻辑。

2.如何适配通知类别(Channel)

官方文档已有非常详细的描述,适配步骤不再赘述,详见 https://developer.android.com/training/notify-user/channels 。

有几点值得注意:

2.1. 不要滥设类别,建议控制在 7 类以内

2.2. Channel 的名字要有可读性、有具体含义,同时避免名字重复

2.3. 如果 Channel 名字不好理解,可以使用 setDescription 来补充说明 Channel 的含义

2.4. 最好不要只设一个 Channel,那样就没有设置 Channel 的意义了。以猫眼为例,可以考虑将通知拆成多个 Channel,如下图

3. FAQ

3.1. 一定要适配通知 Channel 吗

取决于你的应用的 target API:

  • target API ≥ 26(Android 8.0):必须适配,而且必须给每条通知指定一个 Channel,否则无法发出通知。
  • target API ≤ 25(Android 7.1):可以不适配。在8.0及以上的设备,通知也能正常发出。

3.2. 一定要有通知 Channel Group 吗

不是必须的。没必要强行设置 Channel Group,应该视自己的业务需求而定。

3.3. 适配 Channel 后,在 Android 8.0 以前的设备会怎么表现

Android 8.0 以前的设备,会完全无视这个功能,因此不会带来任何兼容性问题。

3.4. 创建好 Channel 后,在代码里修改其配置,为什么会不生效

这个是 Android 对此功能增加的限制:当 Channel 已经存在时,后面的 createNotificationChannel 方法仅能更新其 name/description,以及对 importance 进行降级,其余配置均无法更新。先删除旧的 Channel,再创建新的 Channel 也无法实现对其 importance 的升级。

相关逻辑在 com/android/server/notificaton/RankingHelper.java:

@Override
public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
        boolean fromTargetApp) {
//......
    NotificationChannel existing = r.channels.get(channel.getId());
    // Keep most of the existing settings
    if (existing != null && fromTargetApp) { // 如果已经存在同channel-id的Channel
//......
        existing.setName(channel.getName().toString()); // 则更新name和description
        existing.setDescription(channel.getDescription());
        existing.setBlockableSystem(channel.isBlockableSystem());
 
        // Apps are allowed to downgrade channel importance if the user has not changed any
        // fields on this channel yet.
        if (existing.getUserLockedFields() == 0 &&  // 并且仅当更新的importance小于原来设置的值时才更新importance
                channel.getImportance() < existing.getImportance()) {
            existing.setImportance(channel.getImportance());
        }
 
        updateConfig();
        return;
    }
//......
}


3.5. 如何判断 MIUI 版本

有以下方法:

android.os.SystemProperties.get("ro.miui.ui.version.code", "7");// 如果返回值是「8」,就是 MIUI 10

android.os.SystemProperties.get("ro.miui.ui.version.name", "");// 如果返回值是「V10」,就是 MIUI 10

3.6. 如何测试

升级到  MIUI 10 最新开发版即可,支持的机型和下载链接如下(找到对应机型的最新开发版):