小米MIUI小部件审核规范

小米MIUI小部件审核规范

本文档介绍了MIUI收录小部件的一些原则,您可通过阅读本文档,对MIUI小部件审核规范有一定了解。本文档会及时更新并完善,每一次的修改都是基于优化用户的体验、公平对待所有开发者出发。我们重视用户体验如同珍视生命一般,希望您也如我们一样。

1.MIUI 小部件所属应用规范

  • 确保小部件所属应用遵循对应的审核规范 https://dev.mi.com/distribute/doc/details?pId=1080
  • 确保应用信息及开发者账号信息填写完整和正确
  • 确保应用正常运行且不会发生崩溃或错误
  • 确保您在应用商店提交的联系信息真实有效,以便小部件管理人员在需要时与您取得联系

2.MIUI 小部件视觉规范

2.1.尺寸规范

2.1.1.设计稿尺寸

小部件需适配不同的设备和屏幕(1080p屏和2k屏),并兼容负一屏和不同布局的桌面(4*6和5*6)。对应的尺寸像素值倍数关系:1080:2k=1:1.333,详细尺寸如下表:

2.1.2.自适应布局的实际尺寸

因小部件采用的是自适应布局,在不同设备屏幕及不同样式的桌面上,小部件的尺寸会相应变化。以下是需考虑的小部件适配兼容场景(以1080*2340分辨率为基础):

  • 桌面支持4*6和5*6布局,需保证4*6布局下完美显示,5*6布局下无显示缺陷问题 (设置方式:设置—桌面—桌面布局规则)
  • 需保证有搜索框下完美显示,无搜索框下无显示缺陷问题(设置方式:设置—桌面—桌面搜索框)
  • 需保证无虚拟键下完美显示,有虚拟键下无显示缺陷问题(设置方式:设置—桌面—系统导航方式)
  • 需保证1080p与2k分辨率的屏幕上无显示问题

基于以上场景,我们列出了各尺寸的小部件在不同设备屏幕上实际显示的最小尺寸和最大尺寸,供设计师参考。(无需单独输出设计方案,设计和验收时注意不同布局下显示是否正常即可)

2.2.其他规范

  • 圆角:1080p屏下的交付资源圆角为38px,2k屏下的交付资源圆角为50px
  • 内容安全区:1080p屏下小部件的主要内容需远离边缘约40px,2k屏下为54px
  • 小部件在浅色模式和深色模式下需能完美显示
  • 小部件在无内容场景下需使用正确的占位符和文字说明
  • 小部件的字号大小设计需让界面信息清晰易读、层次分明
  • 小部件的字色层级需符合规范
  • 小部件的字高和行高需符合规范
  • 小部件预览图: 预览图圆角为38px;若应用适配了深色模式,请同时提供深色模式的预览图

具体规范和示例详见《MIUI小部件规范》中第四部分-设计规范

3.MIUI 小部件功能规范

3.1.必须适配

  • 不支持在小部件内进行文字输入
  • 不支持在小部件内上下滑动或左右滑动
  • 调整字体显示大小后,小部件需正常显示,无遮挡、截断文字的现象
  • 小部件在无网络、无数据、无内容、未授权、未添加情况下需展示相应的异常状态
  • 小部件不得出现内容无法正常显示或无法获取等情况
  • 小部件不得出现按钮或者链接点击无反应或报错的情况
  • 支持编辑的小部件,编辑功能必须能正常使用
  • 小部件不得安装或者运行前提示或者强制用户重启设备
  • 小部件显示的实际状态应与设定的优先级对应状态相符
  • 同一id的小部件不得在前后两个版本承接不同类型的功能
  • 小部件需使用独立的后台进程,且内存未超40M
  • 小部件介绍或更新日志中介绍的功能应与实际跳转相符
  • 用户未同意应用的隐私协议时,小部件应显示兜底图,点击小部件后应跳转应用,应用内出现授权弹窗
  • 已安装的小部件在加载数据时显示占位符内容。可以通过将 UI 的静态部分与代表其内容的半透明形状相结合来创建占位符外观

3.2.建议适配

  • 小部件可选择适配无障碍talkback功能,帮助无障碍人士更好的使用小部件
  • 在无障碍“视觉”功能中调整字体大小后,小部件需正常显示,无遮挡、截断现象
  • 小部件可跟随所属应用适配多语言,选择适配多语言里的部分或全部语言(除简体中文外有繁体中文、英文、维吾尔语、藏语),并保证切换语言后功能仍可以正常使用
  • 小部件可跟随所属应用适配深色模式,并保证切换深色模式后,功能可以正常使用

4.MIUI 小部件内容规范

4.1.小部件名称

  • 小部件名称与应用名称不得相同
  • 小部件名称必须贴合小部件功能,不得添加与小部件内容无关的词语
  • 概括小部件主要功能信息,中文长度不得超过10个中文字符
  • 小部件名称与其功能相对应,不同功能的小部件应该有不同的名称
  • 小部件名称不得存在占位符文本、空格、乱码等非法字符(如:#、*、& 等)
  • 小部件名称不得包含政治敏感、色情、恐怖、暴力血腥等内容,以及国家法律法规禁止的违法内容
  • 小部件名称不得使用其他热门小部件名称或别称,也不得混有商业化用语或流行用语等与小部件功能无关的词语
  • 小部件名称不得含有小米或MIUI等其他小米元素
  • 小部件名称不得涉及赌博、彩票的内容

4.2.小部件介绍

  • 中文长度不得超过22个中文字符,其他语言长度不超过44个中文字符(1个中文字符 = 2个特殊字符 / 2个非中文字符)

举例:所属“相册”应用的小部件

小部件名称小部件介绍
自选图片添加至桌面后,点击小部件来选择图片
精选回忆为你推荐精彩照片与回忆
  • 小部件介绍应符合小部件功能,不同功能的小部件要对应不同的简介
  • 小部件介绍不得添加其他热门词语或商业化用语等与应用无关的关键字
  • 小部件介绍不得违反国家法律法规,包括但不限于法律法规中禁止使用的词语
  • 小部件介绍不得添加具有侵权内容的关键字
  • 小部件介绍不得存在占位符文本、空格、乱码等非法字符(如:#、*、& 等)
  • 小部件介绍中不得使用极限词或虚假承诺等违反新广告法的内容(如“最”“第一”“唯一”“NO.1”“必备”“免费送”“100%” “全球”“顶尖”“首”等)
  • 小部件介绍不得使用疑问、反问等句式,请用陈述语句进行描述
  • 小部件介绍的末尾禁止添加任何标点符号

小部件展示图片说明

4.3.小部件中心预览素材及跳转落地页

4.3.1.预览素材图

  • 需符合MIUI 小部件设计规范,并与实际使用场景相符合
  • 预览素材图要与实际添加到桌面上的展示保持一致
  • 不得包含任何推荐相关的角标(如“推荐”、“荐”、“热门”、“最新”、“优惠”、“惠”等)
  • 不得涉及抄袭内容
  • 不得含非法金钱交易、赌博、反政府、反社会及其他法律禁止内容
  • 不得含诽谤、人身攻击或者侮辱性的内容
  • 不得含强烈的暴力暗示,包括但不限于虐待,聚众斗殴
  • 不得含过度暴露、情色、低俗内容
  • 不得含种族歧视,破坏民族团结内容
  • 不得过度宣传酒精或者危险物品(如毒药、爆炸物等)或者鼓励未成年人消费香烟和酒精饮料
  • 不得包含具有贩卖、购买违禁物品的内容

4.3.2.跳转落地页

  • 不得涉及抄袭内容
  • 不得含非法金钱交易、赌博、反政府、反社会及其他法律禁止内容
  • 不得含诽谤、人身攻击或者侮辱性的内容
  • 不得含强烈的暴力暗示,包括但不限于虐待,聚众斗殴
  • 不得含过度暴露、情色、低俗内容
  • 不得含种族歧视,破坏民族团结内容
  • 不得过度宣传酒精或者危险物品(如毒药、爆炸物等)或者鼓励未成年人消费香烟和酒精饮料
  • 不得包含具有贩卖、购买违禁物品的内容

MIUI Widget适配建议及示例

1.MIUI Widget布局自适应方案建议及示例

1.1.概述

背景见:《MIUI小部件技术规范与系统能力说明》中「小部件技术规范和要求」第八项。

本模块介绍了常用的几类Widget布局实现自适应的方案。

1.2.方案与示例

1.2.1.快捷卡片的自适应方案实践

快捷功能卡片中每个icon的大小是固定的,但是在widget中无法获取具体显示大小,因此如果只写死一套尺寸,在某些模式下,icon会显示不全。

目前采用的方案是,使用2套布局资源,在布局中写死大小,根据Google小部件开发指南中提到的,使用OPTION_APPWIDGET_MIN_WIDTH或者OPTION_APPWIDGET_MAX_WIDTH返回的数值决定使用哪套布局:

先定义2套不同尺寸的layout资源:

//layout_shortcut_large
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/shortcut_icon"
        android:layout_width="53dp"
        android:layout_height="53dp"
        android:layout_gravity="center"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/shortcut_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dp_4"
        android:textSize="24sp" />
</LinearLayout>


//layout_shortcut_small
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/shortcut_icon"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:layout_gravity="center"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/shortcut_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/dp_4"
        android:textSize="14sp"/>
</LinearLayout>

在代码中动态判断使用哪一套布局:

class SimpleWidgetProvider : AppWidgetProvider() {

    override fun onUpdate(
    context: Context,
    appWidgetManager: AppWidgetManager,
    appWidgetIds: IntArray) {
        appWidgetIds.forEach { appWidgetId ->
        updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    fun updateAppWidget(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int,
    ) {
        val appWidgetManager = AppWidgetManager.getInstance(context)
        val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
        if (options == null) {
            return null
        }
        val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
        val widgetLayoutRes = if (minWidth >= 300)R.layout.layout_shortcut_large else R.layout.layout_shortcut_small
        val remoteViews = RemoteViews(context.packageName, widgetLayoutRes)
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
    }
    
    /**
     * 任何option的更改都会触发,无法通过newOptions来判断是否是大小发生了变化
     * 需要将本次的MAX_HEIGHT/MAX_WIDHT记录下来,在下一次触发时比对
     */
    @CallSuper
    override fun onAppWidgetOptionsChanged(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int,
        newOptions: Bundle
    ) {
        //需要在options change通知里判断,min和max是否发生了变化,如果发生了变化,需要更新布局
    }
}

1.2.2.股票卡片的自适应方案实践

RemoteViews无法动态设置宽高,但是可以在布局中设置宽高,为了实现卡片的自适应,我们不建议在布局中给layout_width和layout_height设置固定值,但是可以通过设置最大和最小宽高来达到自适应的效果;

例如股票通过调整ListView中每个item的maxHeight和maxHeight,来实现自适应(以下按1080*2340分辨率为例):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/stock_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxHeight="43.64dp"
    android:minHeight="40.97dp"
    android:background="@drawable/pa_bg_stock_widget_item"
    android:paddingHorizontal="@dimen/dp_7">

    <include layout="@layout/pa_widget_stock_item" />
</RelativeLayout>
  • android:minHeight计算:(418-40*2)/ 3 / 2.75=40.97
  • android:maxHeight计算:(440-40*2)/ 3/ 2.75 = 43.64

计算过程:

在MIUI小部件设计规范中,列出了Widget Host在不同模式下的尺寸:

如表格所示,4*2规格的卡片,其最小显示高度是418px,最大显示高度是440px,我们要求内容区距离卡片外边距最小是40px,因此,对于4*2widget的内容区的最小显示高度为418-40*2;

股票4*2最多显示3条数据,因此每条数据的最小显示高度为(418-40*2)/ 3,换算成dp为40.97dp;最大显示高度类似;

1.2.3.其他方案建议

使用LinearLayout等分布局:

疫情卡片的一种自适应布局实现:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="40px">

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="bottom">

        <LinearLayout
            android:id="@+id/item1"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_marginEnd="10dp"
            android:layout_weight="1">

        </LinearLayout>

        <LinearLayout
            android:id="@+id/item2"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_marginEnd="10dp"
            android:layout_weight="1">

        </LinearLayout>

        <LinearLayout
            android:id="@+id/item3"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_marginEnd="10dp"
            android:layout_weight="1">

        </LinearLayout>

        <LinearLayout
            android:id="@+id/item4"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_weight="1">

        </LinearLayout>
    </LinearLayout>

</FrameLayout>

利用LinearLayout的android:layout_height属性,动态分配每个item的尺寸;

注:股票卡片也可以采用该方案进行适配,事实上,所有使用ListView或者GridView的卡片布局,都可以用该方案代替,来达到自适应效果,但是相应地需要进行更多自定义的封装,开发成本更高,但是效果比较理想;

使用RelativeLayout相对布局:

手机管理卡片的一种自适应布局实现:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:padding="40px">

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/score_description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_alignParentBottom="true"
            android:text="手机很安全,继续保持"/>

        <TextView
            android:id="@+id/score"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="100"/>
    </RelativeLayout>


    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="手机垃圾"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="内存占用"/>
    </LinearLayout>

</LinearLayout>

1.3.总结

  • 以上各个方案只是目前为止,可能的部分自适应实践建议,业务需要根据自身情况来判断如何适配
  • 以上各个方案,部分方案可以通过组合的方式,来实现更多效果
  • 以上示例代码中的数值和布局,只具有演示效果,业务需要根据具体情况调整,请勿原样复制

2.小部件加载态建议及示例

2.1.概述

背景见:《MIUI小部件规范》中「无内容场景」。没有配置加载态手机重启或者桌面/负一屏重启会呈现空白卡片,影响整体体验,本模块介绍了推荐的加载态实现方案。

2.2.方案

默认展示加载态(initialLayout指定加载态布局资源),当收到update广播时更新为实际的布局

# 工程结构
.
└── src
    ├── main
    │   └── java
    └── res
        ├── drawable
        │   └── loading.webp
        ├── drawable-night
        │   └── loading.webp
        ├── layout
        │   ├── layout_example.xml
        │   └── layout_loading.xml
        └── xml
            └── app_widget_simple.xml

layout_example.xml 正常的布局资源

...

layout_loading.xml 加载态布局资源

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="fitXY"
    android:src="@drawable/loading.webp" />

app_widget_simple.xml 小部件配置资源

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="xxx"
    android:minHeight="xxx"
    ...
    android:initialLayout="@layout/layout_loading.xml"/>

AppWidgetProvider 使用正常布局文件创建RemoteViews

public class ExampleWidget extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        for (int appWidgetId : appWidgetIds) {          
            // 正常布局文件创建RemoteViews 
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), 
            R.layout.example);
            ...        
        }
    }
}

备注:以上方案仅做参考。

小米MIUI 小部件系统能力说明

小米MIUI 小部件系统能力说明

1.调起 MIUI Widget 商店里的详情页,添加应用的 MIUI Widget

1.1.描述

启动 Widget 详情页,详情页会包含该 App 通过审核的Widget。用户可以左右滑动预览,并选择其中一个添加到桌面。

1.2.调用方式

关键方法
appWidgetManager.requestPinAppWidget(myProvider, extras, null)
使用 extras 携带参数 
addType: appWidgetDetail
widgetName: 小部件name,可选参数,用来指定打开详情页后定位到的组件。如果不填,默认定位到第一个。
widgetExtraData: 用于携带自定义参数,携带自定义参数类型只能是String,最多携带5个,超过5个所有携带的自定义参数都将被抛弃。

注意:该方法仅支持 Android 8.0 及以上系统。不支持 MIUI Widget 的手机调用requestPinAppWidget 方法不会调起 Widget 商店里的详情页。

1.3.示例

public void startAppWidgetPage(Context context){
    AppWidgetManager appWidgetManager =  
    AppWidgetManager.getInstance(context);
    ComponentName myProvider = new ComponentName(this,  
    ExampleWidgetProvider.class);
    if (appWidgetManager.isRequestPinAppWidgetSupported()) {
        Bundle extras = new Bundle();
        extras.putString("addType","appWidgetDetail");
        // packageName 为应用真实包名
⁣        extras.putString("widgetName",
            "packageName/com.miui.ExampleWidgetProvider");
        Bundle dataBundle = new Bundle();
        dataBundle.putString("dataKey1","data1xxx");
        dataBundle.putString("dataKey2","data2xxx");
        dataBundle.putString("dataKey3","data3xxx");
        dataBundle.putString("dataKey4","data4xxx");
        dataBundle.putString("dataKey5","data5xxx");
        extras.putBundle("widgetExtraData", dataBundle);
        appWidgetManager.requestPinAppWidget(myProvider, extras, null);
    }
}

// 获取自定义参数
public class ExampleWidgetProvider extends AppWidgetProvider {
 
   @Override
   public void onAppWidgetOptionsChanged(Context context, AppWidgetManager
     appWidgetManager, int appWidgetId, Bundle newOptions) {
         super.onAppWidgetOptionsChanged(context, appWidgetManager, 
         appWidgetId, newOptions);
        if(extras != null){
            Bundle dataBundle = extras.getBundle("widgetExtraData");
            dataBundle.getString("xxxkey");
        }   
    }    
}

2.设置 MIUI Widget卡片状态

2.1.描述

MIUI Widget 也可以在智能助理(负一屏)进行展示。应用可以选择向系统发送卡片的优先级状态,MIUI智能助理(负一屏)会根据卡片状态进行动态的排序。

2.2.调用方式

key:    miuiWidgetEventCode     
类型:    String
描述:   事件code命名规则请参考code事件表
key:    miuiWidgetTimestamp
类型:   String
描述:   状态变化时的时间戳 
appWidgetManager.updateAppWidgetOptions(widgetId, options);
appWidgetManager.updateAppWidget(widgetId, views);

2.3.code 事件表

事件code命名规则请参考下表,具体事件与code对应关系以双方沟通结果为准
*新增或修改事件需与MIUI商务同学联系,上报未确认的事件code不会生效,恶意错报被系统识别后会降低推荐权重
核心场景事件事件code备注
金融
股票证券
小部件内有股票/基金在交易opening
小部件内无股票/基金在交易closing
出行通勤时间commuting
非通勤时间other
直播/电台小部件内展示内容“直播中”live1不同的事件可用live1、live2…区分
小部件内展示内容“重播中”replay
小部件内展示内容“未开播”other
连接状态已连接connected
未连接disconnect
通知提醒小部件内展示有通知提醒提醒notice1不同的事件可用notice1、notice2…区分
进度状态如“打车进度”“下单进度”等progress1不同的事件可用progress1、progress2…区分
内容资讯如“今日热门”“话题榜”“热搜榜”等info1不同的事件可用info1、info2…区分
功能跳转小部件为纯工具类的功能跳转state1不同的事件可用state1、state2…区分
其他小部件的兜底展示方案或其他状态other

2.4.示例

  • 普通 Widget :在 AppWidgetProvider 的 onUpdate 方法,调用 updateAppWidgetOptions 方法。
public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // todo 准备待更新数据
        ....
        // 更新数据
        for (int appWidgetId : appWidgetIds) {
            RemoteViews views = new RemoteViews(context.getPackageName(), 
            R.layout.widget_ui);
            Bundle options = new Bundle();
            options.putString("miuiWidgetEventCode", "notice1");
            long currentTimeMillis = System.currentTimeMillis();
            options.putString("miuiWidgetTimestamp",
            String.valueOf(currentTimeMillis));
            appWidgetManager.updateAppWidgetOptions(widgetId, options);
            appWidgetManager.updateAppWidget(widgetId, views);
        }
    }
}
  • 列表 Widget : notifyAppWidgetViewDataChanged 更新后调用 updateAppWidget 和updateAppWidgetOptions 方法
public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            // todo 准备待更新数据
            ...
            appWidgetManager.notifyAppWidgetViewDataChanged (mAppWidgetId,R.id.content);
            // 更新数据后进行状态更新
            Bundle options = new Bundle();
            options.putString("miuiWidgetEventCode", "notice1");
            long currentTimeMillis = System.currentTimeMillis();
            options.putString("miuiWidgetTimestamp",
            String.valueOf(currentTimeMillis));
            appWidgetManager.updateAppWidgetOptions(mAppWidgetId, options);
            RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),             R.layout.widget_list_example);
            appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
        }
    }
}

注意:updateAppWidgetOptions 需要在 updateAppWidget ()方法前,并且两者需要同一线程。

3.设置编辑页

3.1.描述

MIUI Widget 额外提供进入编辑页的入口。用户点击编辑按钮,可以进入编辑页进行相应的设置。

3.2.调用方法与参数

Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
String uriPath = "widgetdemo://com.miui.widgetdemo/widget/WidgetEditActivity";
options.putString("miuiEditUri", uriPath);
appWidgetManager.updateAppWidgetOptions(appWidgetId, options);
appWidgetManager.updateAppWidget(widgetId, views);

3.3.示例

public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager 
        appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {          
            Bundle options =   
            appWidgetManager.getAppWidgetOptions(appWidgetId);
            String uriPath =            "widgetdemo://com.miui.widgetdemo/widget/WidgetEditActivity";
            options.putString("miuiEditUri", uriPath);
            appWidgetManager.updateAppWidgetOptions(appWidgetId, options);
            RemoteViews views =   
            new RemoteViews(context.getPackageName(), 
            R.layout.widget_ui);
            appWidgetManager.updateAppWidget(widgetId, views);
        }
        ....
    }
}
// 通过 intent 可以获取点击的appWidgetId
public class WidgetEditActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Intent intent = getIntent();
        int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    }
}

4.同功能 MIUI Widget 聚合

4.1.描述

MIUI Widget 可以把不同尺寸相同功能的Widget 在详情页中聚合在一块显示,用户可以根据需求添加相应尺寸的Widget。

4.2.调用方式

在AndroidManifest.xml 文件中 AppWidgetProvider 对应的receiver 如果label 的名称相同的话将会被认为是同一功能的不同尺寸。

4.3.示例

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider1"
    android:label="@string/app_widget_group">
    ...
</receiver>

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider2"
    android:label="@string/app_widget_group">
   ...
</receiver>

5.点击  MIUI Widget 跳转应用页面

5.1.描述

在 AppWidgetProvider 的onUpdate方法中,使用 RemoteViews 的 setOnClickPendingIntent 设置启动的 Intent 。使用 AppWidgetManager 的 updateAppWidget方法关联RemoteViews 和Widget。

5.2.参数

// viewId Widget 布局 id
// PendingIntent Intent的封装
// appWidgetId Widget 的 id,可在 onUpdate 方法参数中获取
// appWidgetManager widget 的管理对象,可在 onUpdate 方法参数获取
remoteViews.setOnClickPendingIntent(int viewId, PendingIntent pendingIntent)
appWidgetManager.updateAppWidget(int appWidgetId, RemoteViews views)

5.3.示例

// step1: 构建跳转页面的 PendingIntent
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// step2: 生成 RemoteViews 关联 PendingIntent
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
// step3: 关联 widget 和 RemoteViews
appWidgetManager.updateAppWidget(appWidgetId, views);

6.判断 MIUI Widget 是否已经添加

6.1.描述

使用系统 AppWidgetManager 的 getAppWidgetIds方法判断某个Widget是否已添加。

6.2.示例

ComponentName componentName = new ComponentName(getPackageName(), "com.miui.ExampleAppWidgetProvider");
int[] appWidgetIds = AppWidgetManager.getInstance(getApplicationContext()).getAppWidgetIds(componentName);
if(appWidgetIds.length > 0){
    // 已添加
} else {
    // 未添加
}

7.判断是否支持MIUI Widget

7.1.描述

开发者可以通过示例方式判断当前手机是否支持MIUI Widget。

7.2.示例

@WorkerThread
public boolean isMiuiWidgetSupported() {
    Uri uri = 
    Uri.parse("content://com.miui.personalassistant.widget.external");
    boolean isMiuiWidgetSupported = false;
    try {
        Bundle bundle = getContentResolver().call(uri, 
        "isMiuiWidgetSupported", null, null);
        if (bundle != null) {
            isMiuiWidgetSupported = bundle.getBoolean("isMiuiWidgetSupported");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return isMiuiWidgetSupported;
}

8.判断是否支持MIUI Widget 小部件详情页

8.1.描述

开发者可以通过示例方式判断当前手机是否支持MIUI Widget 详情页。为了提升用户体验,部分机型支持 MIUI Widget(包含MIUI Widget 特性,例如曝光刷新等), 但不支持调起MIUI Widget 详情页。这部分机型添加小部件的方式与旧版系统一致。

8.2.示例

@WorkerThread
public boolean isMiuiWidgetDetailPageSupported() {
    Uri uri = 
    Uri.parse("content://com.miui.personalassistant.widget.external");
    boolean isMiuiWidgetDetailPageSupported = false;
    try {
        Bundle bundle = getContentResolver().call(uri, 
        "isMiuiWidgetDetailPageSupported", null, null);
        if (bundle != null) {
            isMiuiWidgetDetailPageSupported = bundle.getBoolean("isMiuiWidgetDetailPageSupported");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return isMiuiWidgetDetailPageSupported;
}

9.MIUI Widget 与 Activity 切换动画

9.1.描述

MIUI 系统为MIUI Widget 和 Activity 切换时增加了过渡动画。开发者可根据自身业务场景决定是否使用过渡动画。动画默认开启,如果不使用,设置”miuiWidgetTransitionAnimation”为false。

9.2.示例

<receiver android:name="com.miui.ExampleWidgetProvider" >
    <meta-data
    android:name="miuiWidget"
    android:value="true" />
    // 关闭动画
    <meta-data
    android:name="miuiWidgetTransitionAnimation"
    android:value="false" />

    ...
</receiver>

10.App主动刷新小部件

10.1.描述

当应用在前台使用或在后台存活时,可调用AppWidgetManager. updateAppWidget()方法,主动刷新小部件。这样可以提高小部件内容的实时性和准确性。

10.2.示例

// 通过组件名更新
public void updateWidget(Context context) {
    // R.layout.demo_appwidget_normal_example 为小部件布局文件,这里仅示例
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
            R.layout.demo_appwidget_normal_example);
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    // NormalExampleWidgetProvider 为小部件组件名字,这里仅示例
    ComponentName componentName = new ComponentName(context, NormalExampleWidgetProvider.class);
    appWidgetManager.updateAppWidget(componentName, remoteViews);
}
// 通过widgetId更新
public void updateWidgetByWidgetId(Context context, int widgetId) {
    // R.layout.demo_appwidget_normal_example 为小部件布局文件,这里仅示例
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
            R.layout.demo_appwidget_normal_example);
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.updateAppWidget(widgetId, remoteViews);
}

11.Push透传刷新服务

11.1.描述

当MIUI Widget状态发生改变且Widget不可见、主应用未启动时,开发者可调用小部件服务端提供的更新API,经由MI PUSH发送透传消息给负一屏/桌面客户端,由负一屏/桌面拉起Widget独立进程(不唤醒应用主进程),以实现MIUI Widget实时刷新的目的。

流程图:

11.2.接口规范

域名: https://developer.assistant.miui.com
路径: /openapi/widget/{cpCode}/refresh
     cpCode值由小部件开发人员提供
参数:需包含Header以及Body
Header参数表
头域名称描述是否必选类型取值范围
Content-Type固定值,填application/jsonStringapplication/json
app-id在小米开放平台注册的程序编号String
access-token应用级token(校验请求权限,通过帐号服务获取)String最大长度:259
sign签名(验证请求完整性,下方有签名生成方法)String
timestamp当前时间戳(防止请求重放,会根据该字段判断请求有效期)long13位 毫秒时间戳
trace-id请求的唯一标识(用来定位每次请求)String只能包含数字和大小写字母,最大长度64
Body参数表
字段名描述必须类型
oaid设备oaidString
widgetId设备 widgetId (安卓生成的id)String
widgetProviderName小部件providerNameString
extra额外透传到小部件内容String

注意:

  • 获取access-token:access-token的获取方式请联系相关技术支持。
  • 处理签名
签名生成: 
    假设: appId = "111111111" , 当前时间戳是 1606206667013 ,秘钥是 "testSecret"
    step 1: 先将body进行md5,再转化为16进制小写字符串
            公式:md5Body= Lowercase(hexEncode(md5(body)))
    step 2: 将appId、毫秒时间戳和body的md5Body值,按照参数名称进行字典排序,用‘&’连接获得strToDigest
            如:strToDigest="appId=111111111&body=md5Body&timestamp=1606206667013"          
    step 3: 将获得到的摘要字符串(strToDigest)进行HMAC_SHA_256,再转化为16进制小写字符串得到签名
            公式:Lowercase(hexEncode(HMAC_SHA_256("testSecret", strToDigest)))
// 签名java demo: 
// demo引用了apache的工具类进行处理,若不想引用可以参考其逻辑自行实现
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>

@Slf4j
public class SignatureUtils {
    
    /**
     * 生成签名
     *
     * @param appId     应用id
     * @param secret    秘钥
     * @param timestamp 请求的时间戳
     * @param body      请求的body内容
     * @return 签名信息
     */
    public static String generateSignature(String appId, String secret, String timestamp, String body) {
        String sign = null;
        try {
            String md5Body = DigestUtils.md5Hex(body);
            Map<String, String> paramMap = new TreeMap<>();
            paramMap.put("appId", appId);
            paramMap.put("timestamp", timestamp);
            paramMap.put("body", md5Body);
            String strToDigest = paramMap.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
            sign = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmacHex(strToDigest);
        } catch (Exception e) {
            log.error("generate signature exception !", e);
        }
        return sign;
    }
    
    /**
     * 验证签名
     *
     * @param appId     应用id
     * @param secret    秘钥
     * @param timestamp 请求的时间戳
     * @param body      请求的body内容
     * @param sign      请求的签名
     * @return 签名信息
     */
    public static boolean verifySignature(String appId, String secret, String timestamp, String body, String sign) {
        return sign.equals(generateSignature(appId, secret, timestamp, body));
    }
}
  • 返回状态码定义
状态码描述
成功0
服务异常1000
无效参数1001
缺失参数1002
token验证失败3000
签名验证失败4000
流量限制5000
  • 开发者MIUI Widget可从刷新广播的Intent中得到extra信息。

11.3.示例

// Curl示例:
curl --location --request POST 'http://developer.assistant.miui.com/openapi/widget/testCp/refresh' 
    --header 'app-id: 111111111' 
    --header 'access-token: A1_oKs19iiWArgij6qFYaEAooAMqG3bhXUgS0MZQf63KQTgWju-oj9YuccqR1EhbugrqnFmooNr6mKdkfEdKN740fUYpxk0o9ZHE5QpFvR1fhaoJ7xELYD1byNnYZb-tB-y5DPXRLIp8ikod5lUZGnayuLVPePa7uB1LlVzw-qPS-U' 
    --header 'sign: 664d528f398af20f23b6e4ec43d4e9662476ee8fc9c5cee42dd897b1af0152e7' 
    --header 'timestamp: 1636341480000' 
    --header 'trace-id: 123123' 
    --header 'Content-Type: application/json' 
    --data-raw '{
        "oaid": "f29eb7d12222fb6b",
        "widgetId": "666",
        "widgetProviderName": "com.miui.test",
        "extra": "{"test":123}"
    }'

小米MIUI小部件技术规范与系统能力说明

MIUI Widget 数据恢复适配

5.1.描述

Android 系统提供了 onRestored 方法用于数据从云端备份恢复时的 Widget 配置迁移。在此基础之上,MIUI Widget 新增了一个 Widget 配置迁移的时机。开发者无需关心何时回调,只需将新的WidgetId与数据绑定并更新UI。

注意:当 MIUI Widget 卡片有配置信息且存在多张卡片的配置信息不一致时需要进行相应适配(例如股票卡片,用户可以添加多张卡片,且每张卡片展示不同的股票),其他情况无需适配。

5.2.示例

public class ExampleWidgetProvider extends AppWidgetProvider {
  @Override
  public void onRestored(Context context, int[] oldWidgetIds, int[] 
    newWidgetIds) {
       super.onRestored(context, oldWidgetIds, newWidgetIds);
       onIdRemap(oldWidgetIds, newWidgetIds, null);
    }

   @Override
   public void onAppWidgetOptionsChanged(Context context, AppWidgetManager
     appWidgetManager, int appWidgetId, Bundle newOptions) {
         super.onAppWidgetOptionsChanged(context, appWidgetManager, 
         appWidgetId, newOptions);
         // MIUI Widget 新增配置迁移时机
         if (newOptions.getBoolean("miuiIdChanged") &&  
                !newOptions.getBoolean("miuiIdChangedComplete")) {
             onIdRemap(newOptions.getIntArray("miuiOldIds"),
             newOptions.getIntArray("miuiNewIds"), newOptions);              
         }
    }

    private void onIdRemap(int[] oldWidgetIds, int[] newWidgetIds, Bundle options) {
        int length = oldWidgetIds.length;
        for (int i = 0; i < length; i++) {   
            int newWidgetId= newWidgetIds[i];
            RemoteViews views = new RemoteViews(context.getPackageName(),    
            R.layout.appwidget_provider_layout);
            //开发者进行数据迁移,并完成新的数据获取
            ...
            //以上操作完成后,先调用updateOptions,再调用updateAppWidget
            if(options != null) {
                //这一步必须执行
                options.putBoolean("miuiIdChangedComplete", true);                             
    AppWidgetManager.getInstance(context)
                .updateAppWidgetOptions(newWidgetId, options);
            }
            AppWidgetManager.getInstance(context)
            .updateAppWidget(newWidgetId, views);
        }
    }
}

6.页面跳转规范

6.1.描述

点击MIUI Widget 跳转应用页面时,推荐使用 PendingIntent 设置相应的 Activity 进行跳转。若业务有分发逻辑可以使用Activity进行中转。

不建议使用PendingIntent 启动BroadcastReceiver/Service,然后在BroadcastReceiver/Service里面启动 Activity。

6.2.示例

// step1: 构建跳转页面的 PendingIntent
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// step2: 生成 RemoteViews 关联 PendingIntent
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
// step3: 关联 widget 和 RemoteViews
appWidgetManager.updateAppWidget(appWidgetId, views);

7.MIUI Widget 布局规范

7.1.描述

系统通过固定的ID找到相应控件并添加圆角,保证所有MIUI Widget圆角统一。因此开发者需要在小部件的根布局上声明ID为”@android:id/background”。由于MIUI Widget 切换页面的动画使用到了背景色,因此根布局

必须有背景色且不能为全透明

7.2.示例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ... 
    // 颜色值为示例
    android:background="#fff"
    android:id="@android:id/background">
    ...
</LinearLayout>

8.MIUI Widget 布局兼容适配

8.1.描述

为了让用户有更好的体验,MIUI Widget 需要保证以下场景正常显示:

  • 负一屏、默认布局下的桌面正常显示
  • 桌面图标行列数变化后正常显示 需保证桌面4×6、5×6网格体系都能正常显示(设置方式:设置—桌面—桌面布局)
  • 桌面搜索框变化时正常显示 需保证有搜索框、无搜索框都能正常显示(设置方式:设置—桌面—桌面搜索框)
  • 桌面虚拟键变化时正常显示 需保证有虚拟键、无虚拟键都能正常显示(设置方式:设置—桌面—系统导航方式)
  • 1k、2k屏幕手机都能正常显示

为了使卡片在各种场景下都有较好的展现,需要做以下适配:

  • MIUI Widget 根布局宽高必须使用match_parent, 内容区需要在根布局里居中
  • 内容区控件尽量不要使用绝对尺寸和绝对位置,可以使用RelativeLayout以及LinearLayout的layout_weight等控制控件的位置和尺寸

8.2.示例

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/background"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    <!--widget_root_padding 与设计图保持一致,如示例图为40px-->
    android:padding="@dimen/widget_root_padding"
    android:gravity="center">

     <!--内容区-->
    <LinearLayout
        android:id="@+id/widget_content"
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent">
    </LinearLayout>   
</FrameLayout>

9.应用清除数据适配

9.1.描述

用户在使用过程中可能存在清除应用数据的行为,用户清除数据时MIUI系统会给 MIUI Widget 发送刷新广播,此时应用处在无数据或未授权状态,开发者需要在该时机将小部件恢复成默认视图或授权视图。

9.2.示例

<receiver android:name="com.miui.ExampleWidgetProvider" >
    ...
    // MIUI Widget 标识
    <meta-data
    android:name="miuiWidget"
    android:value="true" />
    <intent-filter>   
        //MIUI展现刷新
        <action android:name="miui.appwidget.action.APPWIDGET_UPDATE" />    
    </intent-filter>
</receiver>

public class ExampleWidgetProvider extends AppWidgetProvider {
    ...
    @Override
    public void onReceive(Context context, Intent intent) {
        if ("miui.appwidget.action.APPWIDGET_UPDATE".equals(intent.getAction())) {
            int[] appWidgets = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            // 根据应用自身逻辑更新视图 
            ...
        } else {
            super.onReceive(context, intent);
        }
    }
}

10.小部件版本号

10.1.描述

MIUI小部件增加版本号,方便小部件版本迭代管理。小部件版本号针对应用内所有小部件,而非单个。当应用内任一小部件发生功能变动或新增小部件时需升级小部件版本号。若小部件较前一应用版本未发生变动,则无需更改小部件版本号。

注意:

  • 名称为”miuiWidgetVersion” 的meta-data标签,应放置在application下而不是某个小部件provider下。
  • 小部件发生变动或新增时一定要修改小部件版本,否则存在应用所有小部件被下线的风险。
  • 小部件版本号为整数,从1开始,同应用版本号的升级一致,都只能升高。

10.2.示例

AndroidManifest.xml 文件参考如下配置:

<application
...
<meta-data
    android:name="miuiWidgetVersion"
    android:value="1" />
...
</application>

11.MIUI Widget 大屏适配

11.1.描述

为了使MIUI Widget可以在大屏设备(折叠屏,平板)上提供更好的用户体验,需要对MIUI Widget在大屏幕设备上进行适配。

11.2.编辑页适配

若开发者在MIUI Widget中设置了编辑页,大屏设备上适配后的展示效果如下图所示:

适配方法:

  • 对于小部件视图树上,所有可以通过点击调起编辑页面的视图控件,需要在代码中进行以下调用:
public class ExampleWidgetProvider extends AppWidgetProvider {
    ...
    @Override
    public void onUpdate(Context context, AppWidgetManager 
        appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {          
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_ui);
            // 编辑页适配
            val widgetOptions = AppWidgetManager.getInstance(context).getAppWidgetOptions(appWidgetId)
            //由host提供,业务直接读取,以判断当前设备是否支持大屏预览模式
            if (widgetOptions != null && widgetOptions.getBoolean("miuiLargeScreenDevice", false)) {
                //如果当前设备支持大屏预览模式,则先生成一个Bundle实例,再分别执行步骤1,2
                val largeScreenOptions = Bundle().apply {
                    //1. 传入widgetId
                    putInt("miuiWidgetId", widgetId)                                      
                }
                //2. 通过setBundle方法,调用控件的supportLargeScreenEditPreviewMode方法,并传入以上生成的Bundle
                //R.id.item_container_1 为一个示例,所有点击事件为调起编辑页的控件,都必须进行类似的调用
                remoteViews.setBundle(R.id.item_container_1, "supportLargeScreenEditPreviewMode", largeScreenOptions)
            }    
            appWidgetManager.updateAppWidget(widgetId, views);
        }
        ....
    }
}
  • 如果业务小部件有长按编辑入口,则在生成编辑页面链接时,需要新增miuiWidgetId参数:
//示例:
public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager 
        appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {          
            Bundle options =   
            appWidgetManager.getAppWidgetOptions(appWidgetId);
            String path = 
            "widgetdemo://com.miui.widgetdemo/widget/WidgetEditActivity";
            Uri.Build uriPath = Uri.parse(path).buildUpon();
            //本次新增参数,host依据此参数来判断,业务编辑页是否支持大屏预览模式
            uriPath.appendQueryParameter("miuiWidgetId", widgetId.toString());
            options.putString("miuiEditUri", uriPath);
            appWidgetManager.updateAppWidgetOptions(appWidgetId, options);
            RemoteViews views =   
            new RemoteViews(context.getPackageName(), 
            R.layout.widget_ui);
            appWidgetManager.updateAppWidget(widgetId, views);
        }
        ....
    }
}
  • 业务侧在编辑页面Activity的onCreate方法中,需要从Intent中解析对应参数,判断是否以大屏预览模式展示,并对大屏预览模式下的编辑页面进行改造:
public class WidgetEditActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        val intent = getIntent()
        //是否以大屏预览模式展示
        if (intent.getBooleanExtra("isLargeScreenMode", false)) {
            //该字段返回一个矩形区域,代表小部件在屏幕坐标系上的具体位置
            val rect = intent.getParcelableExtra("miuiWidgetScreenBound")
            if (rect != null) {
                //业务需要根据rect提供的小部件位置信息,来计算业务内容视图的具体展示位置:
                //1、业务内容视图应当始终展示在小部件的左侧,或者右侧,距离小部件固定间距
                //2、业务内容视图应当在小部件的左侧和右侧中,选择空间较大的一侧展示
            }
        }
    }
}

需要注意的事项:

  • 不要在生成PendingIntent时,put自定义的Parcelable对象或者Serializable对象,这样会导致Host处理intent时因找不到类而抛异常。请转换成基础数据类型进行传递。
  • 请不要在生成PendingIntent时,添加FLAG_IMMUTABLE,否则业务将无法解析到Host添加的参数,android s以上可以换成FLAG_MUTABLE。
  • 编辑页面打开时,不允许横竖屏旋转(需要支持横竖屏,只是不允许动态旋转)。
  • 业务只需要负责编辑页面内容区域(如上图示中的白色背景覆盖区域)的展示,背景高斯以及小部件的预览图部分,均由Host实现。

11.3.布局适配

在大屏设备上,负一屏和桌面对业务小部件布局进行了全局缩放,因此一般情况下,不需要业务再对小部件进行额外适配。

如果业务希望自行对小部件进行更完美,精细的适配,可以选择将全局缩放能力关闭,只需要在小部件对应的provider里,加上如下所示的配置即可

<receiver
    android:name=".service.normal.widget.NormalExampleWidgetProvider">
    <meta-data
        android:name="miuiAutoScale"
        android:value="false"/>
</receiver>

提示: 对于使用了GridView或者ListView的小部件,该方法不能使用,如遇到问题,请联系相关技术支持。

12.注意事项

12.1.圆角

MIUI Widget 虽然会在MIUI13上统一裁切圆角,但为了能够兼容旧版系统和非MIUI 手机,开发者仍需为MIUI Widget添加圆角 ,圆角大小与设计规范保持一致。

12.2.刷新兼容

曝光刷新仅存在支持MIUI Widget的系统中(可通过“MIUI 小部件系统能力说明–是否支持MIUI Widget”判断),在不支持MIUI Widget 的系统以及非MIUI手机上,MIUI Widget刷新机制与原生Widget保持一致,各项目需要根据原生刷新机制以及业务需求进行适配。

12.3.Activity 进程

系统会对 Widget 进程进行优化,系统资源紧张时,Widget 进程容易被回收。为了保证 Activity 的正常显示,Activity所在进程不能是Widget 进程

12.4.小部件名称

MIUI Widget必须配置小部件名称。开发者可以根据每个小部件的功能,为小部件撰写一个简洁的名字(2~8个汉字),帮助用户理解小部件的用途。在小部件中心里,小部件名称会展示为“应用名称·小部件名称”,为了展示体验更佳,小部件名称与应用名称不得相同。小部件名称对应代码AndroidManifest.xml中receiver的label。设置方法如下:

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider"
    android:label="@string/app_widget_example"
    android:process=":widgetProvider">
    ...
</receiver>    

12.5.深色模式

MIUI Widget 只能在xml中静态适配深色模式(通过配置drawable-night、values-night等资源文件适配),不支持在RemoteViews通过代码动态设置深色模式。

12.6.禁止随意修改 MIUI Widget的receiver类名

Android系统会根据receiver类名标识 MIUI Widget。一旦改名,用户在旧版添加的小部件升级后就会消失。因此 MIUI Widget 一旦审核通过,禁止随意修改receiver类名。

12.7.小部件通过审核后,禁止随意移除 MIUI Widget 标识

通过审核的小部件移除MIUI Widget 标识,会导致系统无法识别,造成一些严重的错误,因此小部件一旦审核通过,禁止随意移除MIUI Widget 标识。

MIUI小部件技术规范与系统能力说明

版本更新说明

版本更新特性更新时间
v1.0.7技术规范小部件版本号MIUI Widget 大屏适配MIUI 小部件系统能力Push透传刷新服务2022-8-19
v1.0.6技术规范页面跳转规范MIUI Widget 布局规范注意事项MIUI 小部件系统能力调起 MIUI Widget 详情页2021-9-13
v1.0.5技术规范页面跳转规范注意事项-小部件名称label应用清除数据适配MIUI 小部件系统能力跳转MIUI Widget 详情页-新增可传自定义参数判断是否支持MIUI Widget 详情页MIUI Widget 与 Activity 切换动画2021-8-26
v1.0.4技术规范MIUI Widget 布局兼容适配注意事项2021-7-5
v1.0.3技术规范MIUI Widget 独立进程尺寸适配MIUI 小部件系统能力判断是否支持MIUI Widget2021-6-23
v1.0.2MIUI 小部件系统能力修改跳转 Widget 商店里的详情页修改设置卡片状态2021-5-26
v1.0.1技术规范修改刷新机制修改 MIUI Widget 标识新增 MIUI Widget 数据恢复适配新增页面跳转规范新增布局规范MIUI 小部件系统能力修改跳转 Widget 商店里的详情页修改设置卡片状态修改跳转编辑页新增判断是否支持 MIUI Widget2021-5-6
v1.0.0初始版本2021-4-21 

简介

MIUI小部件基于原生 Android Widget,开发一个 MIUI 小部件和开发一个原生 Android Widget 基本一致,部分区别如下:

  • 为了保证整机系统用户体验,MIUI 小部件对开发者做了一些规范要求;
  • 为了拓展小部件能力,丰富可玩性,MIUI 小部件额外提供了一些调用能力;

一、小部件技术规范和要求

开发者基于MIUI小部件体系开发 Widget 时,需要满足下面的技术要求:

1.MIUI Widget 使用独立进程

1.1.描述

为了方便管理 Widget,减少 Widget 更新对后台内存的占用,提升整机的用户体验。MIUI 小部件规定 Widget 需要使用独立后台进程(后简称:Widget进程)来进行内容更新。

1.2.Widget 进程限制说明

– Widget 进程名字为” :widgetProvider ” 

– Widget 进程不能拉起其他任意进程(包括 App 主进程),同时系统也会进行相应限制

– Widget 进程内存占用不能超过 35M,执行命令( adb shell dumpsys meminfo 进程名 ) 可获取进程所占内存

– Widget 进程严禁使用 native方法(fork)拉起进程

– Widget 进程只能运行 Widget内容准备和刷新相关的逻辑

1.3.Widget 进程配置说明

<receiver android:name="com.miui.ExampleWidgetProvider"
    android:process=":widgetProvider">
    ....
</receiver>

<service android:name="com.miui.ExampleWidgetService"
    android:process=":widgetProvider" >
    ....
</service>
<provider android:name="com.miui.demo.ExampleWidgetProvider"
    android:process=":widgetProvider">
    ...
</provider>

备注:Activity不需要放在 Widget 进程中

1.4.播放器小部件适配独立进程

播放器小部件存在后台播放音频以及拉起其他进程的情况,这种特殊小部件可以运行在非 Widget 进程,但必须关闭曝光刷新,必须使用前台Service,更新UI时使用主动刷新(详细可看“MIUI 小部件系统能力说明-App主动刷新小部件”)。

1.5.其他

Widget 进程系统分配的 adj 值较高,在系统资源不足时,容易被系统回收。

2.MIUI Widget 曝光刷新适配

2.1.描述

MIUI Widget 将会去掉系统原有的定时刷新。用户滑动到有 Widget 的页面,系统会判定需要触发一次刷新并通知应用。默认 MIUI Widget 曝光不刷新,有曝光刷新需求的 MIUI Widget 需在 AndroidManifest.xml 中申请,并设定曝光刷新间隔miuiWidgetRefreshMinInterval(在间隔时间内曝光多次只会触发一次),曝光刷新间隔最短为10秒。

2.2.适配方式

<receiver android:name="com.miui.ExampleWidgetProvider" >
    ...
    // 定义是否需要曝光刷新
    <meta-data                
    android:name="miuiWidgetRefresh"                
    android:value="exposure" />
    // 定义曝光刷新的时间间隔
    <meta-data
    android:name="miuiWidgetRefreshMinInterval" 
    // 时间单位为毫秒                    
    android:value="20000" />
    // MIUI Widget 标识
    <meta-data
    android:name="miuiWidget"
    android:value="true" />

    <intent-filter>   
         //系统定时刷新             
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> 
        //MIUI展现刷新
        <action android:name="miui.appwidget.action.APPWIDGET_UPDATE" />    </intent-filter>
</receiver>

public class ExampleWidgetProvider extends AppWidgetProvider {
    ...
    @Override
    public void onReceive(Context context, Intent intent) {
        if ("miui.appwidget.action.APPWIDGET_UPDATE".equals(intent.getAction())) {
            int[] appWidgets = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            onUpdate(context, AppWidgetManager.getInstance(context), appWidgets); //或者自定义刷新逻辑
        } else {
            super.onReceive(context, intent);
        }
    }
}

3.MIUI Widget 尺寸设置

3.1.描述

MIUI Widget支持 2*2、4*2、4*4三种尺寸,开发者可以根据组件信息和所需展示的功能选择合适的尺寸进行设计,并且在 resource 配置文件中指定具体的尺寸。

3.2.适配方式

每个小部件必须定义 minWidth 和 minHeight,表示默认情况下应占用的最小空间量。当用户向AppWidgetHost(如桌面,负一屏等)添加小部件时,小部件占用的宽度和高度通常会超过您指定的最小值。AppWidgetHost 为用户提供了一个可用空间网格,供用户放置小部件和图标,网格可能因设备而异。

添加小部件后,它将在水平和垂直方向进行拉伸,占用满足其 minWidth 和 minHeight 约束条件所需的最小单元格数。

虽然单元格的宽度和高度以及应用到小部件的自动外边距量可能会因设备而异,但您设置尺寸时可以参考下表中的建议大小。

规格建议minWidth(单位dp)建议minHeight(单位dp)
2*2110110
4*2300110
4*4300250

3.3.示例

根据 Widget 尺寸大小在上表中选择对应的dp值,填到对应配置文件里的 minWidth/minHeight 属性。下面以2×2的 Widget 为例,说明尺寸适配过程。

// step1:在AndroidManifest.xml中声明widegt配置文件
<receiver android:name=".ExampleAppWidgetProvider" >
   ....
    <meta-data android:name="android.appwidget.provider"
    android:resource="@xml/example_appwidget_info" />
</receiver>

// step2: example_appwidget_info中设置尺寸属性
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="@dimen/widget_min_width"
    android:minHeight="@dimen/widget_min_height"
    ....    
</appwidget-provider>

// step3: values/dimens中定义尺寸值
<dimen name="widget_min_width">110dp</dimen>
<dimen name="widget_min_height">110dp</dimen>

4.MIUI Widget 标识

4.1.描述

在 AndroidManifest.xml文件中,MIUI Widget 的receiver组件需要添加 MIUI 标识,只有添加 MIUI 标识的Widget才享有MIUI Widget 能力。

4.2.示例

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider"
    android:process=":widgetProvider">
   <meta-data
    android:name="miuiWidget"
    android:value="true" />
    ....
</receiver>

小米开放平台MIUI小部件规范二

小米开放平台MIUI小部件规范二

3.6.开放能力

为了给用户提供更好的体验,MIUI小部件体系会开放并持续更新部分系统能力,便于开发者使用。

  • 跳转应用页面:小部件可以跳转自身应用或其他应用页面
  • 多个跳转入口:小部件内支持展示多个跳转入口,点击后跳转至应用内不同的页面
  • 是否已存在小部件:支持应用判断用户是否已添加该应用具体某个小部件
  • 调用小部件中心详情页:应用可以直接调用小部件中心中该应用的详情页,方便应用引导用户更快地添加小部件
  • 具体流程:在应用内引导添加时,应用调起小部件中心应用详情页,用户点击“添加到负一屏”或“添加到桌面”后,对应小部件会被添加到负一屏或桌面上,同时弹窗落下,toast提示用户添加成功,回到应用端内
  • 调用方式:见技术文档;
  • 注意事项:此时不支持用户拖动小部件添加
  • 优先级设置:开发者可以给小部件的不同状态设置相应的优先级(高/中/低),每个优先级对应不同的事件参数,MIUI会智能地根据小部件优先级进行排序,优先级越高,越可能在小部件屏上方出现
  • 点击不跳转:点击小部件直接刷新,无需跳转。如内存清理小部件

3.7.禁止的能力

  • 小部件内上下滑动或左右滑动
  • 文字输入

4.小部件设计规范

4.1.基本规范

4.1.1.手机小部件基本规范

a.尺寸

普通手机上,小部件主要分为 2×2、4×2、4×4 三个尺寸。i.设计稿尺寸

小部件在不同设备屏幕以及桌面布局下的大小不同。2k:1080=1.333x,各元素可参考此倍数关系。详细尺寸如下表:

附:MIUI小部件设计模版(设计师同学可从这里取用哦)

示例:以下为1080×2340分辨率对应widget尺寸像素值

ii.自适应布局的实际尺寸

因小部件采用的是自适应布局,在不同设备屏幕及不同样式的桌面上,小部件的尺寸会相应变化。以下是需考虑的小部件适配兼容场景(以1080*2340分辨率为基础): 

  • 负一屏和桌面小部件的样式
  • 桌面图标行列数变化后的样式:桌面支持4*6和5*6布局,需保证4*6布局下完美显示,5*6布局下无显示缺陷问题 (设置方式:设置—桌面—桌面布局规则)
  • 桌面无搜索框时的样式:需保证有搜索框下完美显示,无搜索框下无显示缺陷问题(设置方式:设置—桌面—桌面搜索框)
  • 桌面虚拟键存在和不存在时的样式:需保证无虚拟键下完美显示,有虚拟键下无显示缺陷问题(设置方式:设置—桌面—系统导航方式)
  • 1080p和2k屏幕手机下样式显示:需保证1080p与2k分辨率的屏幕上无显示问题。

基于以上场景,我们列出了各尺寸的小部件在不同设备屏幕上实际显示的最小尺寸和最大尺寸,供设计师参考。(无需单独输出设计方案,设计和验收时注意不同布局下显示是否正常即可)

iii.圆角

  • 开发者提供的手机上小部件预览图必须为38px的平滑圆角
  • 开发者交付的小部件资源里,1080p屏幕需提供38px的圆角,2k屏需提供50px圆角
  • MIUI13系统侧会对真实的小部件进行统一裁切,所以1080p屏幕上圆角实际显示为46px,2k屏上实际显示为62px
b.内容显示区

普通手机上主要内容需预留大于等于40px的安全区(红色区域内不可显示元素),2K屏幕下需预留大于等于54px的安全区。

c.文字

i.字体

为了视觉上的美观及统一,推荐使用MiSans 字体。

ii.字阶

字号大小决定了信息的层级和主次关系,合理有序的字号设计能让界面信息清晰易读、层次分明。

iii.字重

本规范主要针对以数据展示为核心信息的小部件,2×2,4×2通用。

若小部件以文字信息为主,字重规范为:主要信息使用MiSans Medium,次要信息使用MiSans regular,且透明度为40%

*计算器正常使用 MiSans

(1)以数字/数据为核心要素的小部件,数字字体统一使用Mitype-SemiBold(或Mitype Mono- SemiBold

*低于(包含)100px字号的数字,统一使用 Mitype-Bold

(2)小部件中标题文字,字重使用MiSans Medium,且透明度为40%的黑(Dark 下为40%的白)

*如含有多层级文字,字重选择为:一级标题Medium,二级标题Regular

(3)日期表达式的规范

统一使用格式:年–月–日 、月–日。 如:2021–8–16、 8–16。

(4)示例

iv.颜色

界面可以通过字色增强界面视觉层级,定义主、次、辅三个层级。

v.行高

为避免文字出现多种间距,统一字高使阅读感受及开发标准一致性。 h=f*1.2。

d.描边和投影(由MIUI统一处理)

i.描边(内描边

light:10%#000000 1px

dark:10%#ffffff 1px

ii.投影

投影由MIUI统一处理,无需三方适配

样式示例

4.1.2.折叠屏小部件基本规范

注:已适配普通手机的小部件,能够直接在折叠屏上经桌面缩放后显示,无需开发者另作适配。如果能够通过长按小部件,从“编辑”入口进入编辑页,可选择按照交互规范重新适配编辑页。

a.尺寸

与普通手机一样,折叠屏上小部件主要分为2×2、4×2、4×4 三个尺寸。

i.设计稿尺寸

以下为1916×2160分辨率对应像素值

ii.圆角

  • 开发者提供的折叠屏上小部件预览图为42px的平滑圆角
  • 开发者交付的小部件资源里,折叠屏1916px*2160px屏幕需提供42px的圆角
  • MIUI13系统侧会对真实的小部件进行统一裁切,所以1916px*2160px屏幕上圆角实际显示为42px,外屏与1080p屏的规范保持一致,实际显示为46px
b.内容显示区

折叠屏上主要内容需预留大于等于36px的安全区(红色区域内不可显示元素)

c.文字(字体/字重/颜色/行高 与手机规范一致)

i.字阶

字号大小决定了信息的层级和主次关系,合理有序的字号设计能让界面信息清晰易读、层次分明。

d.投影和描边(与手机规范一致)

4.1.3.平板小部件基本规范

注:已适配普通手机的小部件,能够直接在平板上经桌面缩放后显示,无需开发者另作适配。如果能够通过长按小部件,从“编辑”入口进入编辑页,可选择按照交互规范重新适配编辑页。并需要在开发者平台上传pad4x2(超大) 尺寸小部件信息时,提供相应的预览图。

a.尺寸

与手机、折叠屏不同,平板上小部件主要分为1×1、2×1、2×2、4×2(超大) 三个尺寸,其中1×1、2×1、2×2与手机上的2×2、4×2、4×2存在对应关系。

i.设计稿尺寸

ii.圆角

  • 开发者提供的平板小部件预览图必须为36px的平滑圆角
  • 开发者交付的小部件资源里,平板小部件需提供30px圆角
  • MIUI13系统侧会对真实的小部件进行统一裁切,平板小部件圆角实际显示为36px

【注意】如果仅新增适配pad4x2超大尺寸小部件,只有这个小部件预览图的圆角需要为36px,其他手机上已有的小部件,可沿用之前的预览图。

b.内容显示区

平板上主要内容需预留大于等于22px的安全区(红色区域内不可显示元素)

c.文字(字体/字重/颜色 与手机规范一致)

i.字阶

字号大小决定了信息的层级和主次关系,合理有序的字号设计能让界面信息清晰易读、层次分明。

ii.行高

为避免文字出现多种间距,统一字高使阅读感受及开发标准一致性。 h=f*1.2

d.投影和描边(与手机规范一致)

4.2.无内容场景规范(手机/折叠屏/平板一致)

说明:导致widget无内容的场景有:无网络、无数据、未授权、未添加内容、未登录账号等,各业务可以根据实际需求选择设计,无需全部适配。

以下是搭建【widget无内容场景】元素使用规范,对于各场景使用效果不做要求

基本规范

a.配色参考

注:不同背景下色块样式参考,颜色不做规范

b.占位色块圆角

将widget内容占位色块分为 图片占位、文字占位使用圆形或正方形来代替图标

c.文字

用于说明文字;授权说明、网络说明、数据说明 等

d.信息显示
e.加载态

已安装的小部件在加载数据时显示占位符内容。可以通过将 UI 的静态部分与代表其内容的半透明形状相结合来创建占位符外观。

注:各业务需出加载数据时显示占位符内容,用作加载时使用。

样式示例

5.上传须知

完成适配并通过审核后,开发者可以在小部件开发者平台上传小部件,具体操作流程可参考《小部件提交审核与上传操作指南 》。

6.联系我们

如果确认要适配MIUI小部件,且没有和我们联系过,请按照以下模板发送邮件至“miui-widget@xiaomi.com”:

开发者微信:

开发者联系电话:

应用包名:

我方已阅读MIUI小部件产品设计规范与技术规范,准备按照审核要求适配MIUI小部件,希望进一步沟通审核流程和相关规范。

温馨提示:我们会在看到邮件后的10个工作日内与您联系,感谢您适配小部件。

小米开放平台MIUI小部件规范一

小米开放平台MIUI小部件规范

1.小部件简介

小部件是Android提供的能力。开发者可选取应用中重要的内容直接在桌面或小部件屏中呈现给用户,以提升用户在桌面获取信息与直达服务的效率。同时用户可通过小部件实现个性化桌面。

2.MIUI小部件体系

MIUI小部件体系基于Android小部件,对设计与技术实现进行了规范,并持续迭代开发者所需要的能力,从而联合开发者一起为用户提供更好的使用体验。

2.1.桌面网格

通过对桌面的重新设计,MIUI支持小部件在手机桌面中优雅的展示,用户可以非常方便的将小部件添加至桌面,并可快速拖动至小部件屏。

同时还支持“无字模式”:隐藏桌面上的应用与小部件名称,使桌面的简约美感达到极致。(操作路径:桌面双指捏合—左下角设置图标—无字模式)

2.2.小部件屏

原MIUI智能助理升级为小部件屏,承载了用户统一查看并使用小部件的需求。用户可以根据自己的使用习惯和喜好,对小部件进行自定义排序。

3.3.小部件中心

当用户需要添加小部件时,可以很方便的从桌面或小部件屏中,找到小部件中心入口。小部件中心支持用户直接拖拽小部件至桌面或小部件屏。

除此之外,用户还可以在这里发现更多好看或好用的小部件,即使用户未安装对应的应用。若用户未安装应用,在小部件中心中将展示开发者上传的小部件名称与截图,以此向用户传达该小部件的价值,吸引用户添加。用户添加后,将先下载该应用,安装完成后添加对应小部件。

同时,小部件中心也支持安卓小部件的入口,用户可以通过“支持小部件应用-全部-安卓小部件”入口添加。

3.小部件产品规范

3.1.小部件的名称与介绍

开发者可以根据每个小部件的功能,为小部件撰写一个简洁的名称(2~10个汉字)。在小部件中心里,小部件名称会展示为“应用名称·小部件名称”,为了展示体验更佳,小部件名称与应用名称不得相同。

为了更好的帮助用户理解小部件的用途,开发者还需要为每个小部件撰写介绍。小部件介绍的中文长度不得超过22个中文字符,其他语言长度不超过44个中文字符(1个中文字符 = 2个特殊字符 / 2个非中文字符)。

3.2.小部件的尺寸

MIUI小部件支持2*2、4*2、4*4三种尺寸,开发者可以根据小部件信息和所需展示的功能选择合适的尺寸进行设计。

3.3.如何设计一个小部件

小部件中展示的内容由开发者定义,在开发者设计小部件时,我们建议遵循以下原则:

  • 仅展示重点信息:小部件可便捷展示简洁的信息,建议开发者选择一个与您的应用主要用途相关的功能或内容。如运动应用的小部件展示今日步数、音乐应用的小部件展示最近播放的歌曲等;
  • 与个人内容相关:当小部件与用户个人信息有关时,更容易被用户添加并使用。如日历应用在展示月视图的同时展现我的日程、电商应用的小部件展示用户最新物流信息、电影购票应用展示用户即将开场的电影票信息。
  • 根据尺寸填充不同内容:小部件中展示的信息量应该根据尺寸进行变化,如2*2的天气小部件仅展示当前温度,4*2的天气小部件可展示最近几小时的温度走势,4*4的天气小部件可展示未来多天的天气预报。
  • 避免仅跳转应用首页:小部件应该为用户提供信息外露或功能直达的价值,如果点击一个小部件仅仅是打开应用首页,大部分用户将不会添加到其桌面上。
  • 具有品牌辨识度:为了让用户能知道小部件所展示的内容来自哪个应用,建议在设计小部件时加入品牌元素,但不建议直接放APP的图标在小部件中。
  • (建议适配项)支持深色模式:小部件需要支持深色模式,在用户进入深色模式后,可以通过修改前景与背景颜色,使小部件与深色模式下的系统更加统一。如小部件未适配深色模式,也要保证在系统切换为深色模式后,小部件的显示和功能正常。
  • (建议适配项)支持无障碍:MIUI一直致力于让全球每一个人都能享受科技带来的美好生活,因此我们建议开发者设计的小部件,也支持无障碍功能,了解更多可前往小米无障碍官网(http://accessibility.miui.com/)。如小部件未适配无障碍,也要保证在系统开启无障碍talkback功能或是在“设置-无障碍-视觉”中将字体调大后,小部件的显示和功能正常,未出现截断或遮挡等异常现象。
  • (建议适配项)支持多语言:MIUI已经拥有国内外3.1亿用户,覆盖80种语言,支持221个国家与地区。在MIUI国内版中,建议开发者设计的小部件,跟随主应用适配简体中文、繁体中文、英文、维语、藏语中的全部或部分语言,并保证在系统切换为多语言后,小部件的显示和功能正常,未出现截断或遮挡等异常现象。

3.4.支持用户编辑小部件内容

MIUI小部件支持在桌面长按时,提供「编辑小部件」功能,用户点击后跳转到开发者提供的指定页面,对该小部件进行编辑。

a.普通手机设备编辑页展示效果

b.折叠屏编辑页交互规范

由于折叠屏内屏较大,负一屏/桌面的横屏/竖屏模式中,长按小部件点击“编辑”进入编辑页,都采用半屏为预览区,半屏为编辑页的样式。以图中负一屏竖屏为例:

i.编辑类型一:快捷入口合集

没有特定交互框架,没有规范,以原有手机界面为准。

ii.编辑类型二:内容选择,即时生效

所做的操作均会产生即时的影响,没有撤销方式。用以快速操作。

c.平板编辑页交互规范

i.手机上的2×2/4×2/4×4尺寸,在平板上的编辑页与在折叠屏上一样,复用折叠屏上内屏的编辑页设计资源即可。

ii.平板专属的4×2超大尺寸(详见3.a.平板小部件基本规范-尺寸),采取MIUIX设计规范里居中浮窗形式。

3.5.刷新机制

MIUI会在小部件展现时,调用对应小部件进程进行刷新。开发者也可在主应用存活时对小部件进行刷新。同时,MIUI会根据小部件的类别及当前系统状态,平衡体验与性能,智能调整小部件的刷新频率。如您的小部件需要使用日期或时间,请直接调用原生控件 AnalogClock。

小米手机 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 通知栏快捷开关 (Quick Settings Tile) 适配说明

1. 什么是通知栏快捷开关 (Quick Settings Tile)

快捷开关 (Quick Settings Tile) 是 Android 7.0 引入的新功能,支持开发者自定义通知栏快捷开关。任何开发者都可以注册快捷开关,然后会出现在开关编辑面板中,如下图:

 为了放下更多的快捷开关,MIUI 10 开关面板支持左右横滑,也不限制快捷开关的数量。用户也可以自由排序这些开关,比如放在最后或放在最前面,如下图:

点击开关的行为,由开方者自行定义,可以直接开启某功能,或者直接跳转到某一页面,如下图:

2. 如何适配通知栏快捷开关 (Quick Settings Tile)

可以参考官方文档中以下两章的内容:

  1. Tile: https://developer.android.com/reference/android/service/quicksettings/Tile
  2. TileService: https://developer.android.com/reference/android/service/quicksettings/TileService

如果需要完整的讲解,可以参考这篇谷歌 Framework 工程师的博客:Quick Settings Tiles on Android 7.0(可能需要翻墙),现摘录重点如下:

2.1. 在 manifest 中声明 TileService ,示例如下

<service
  android:name=".AwesomeTileService"
 
  //快捷开关的图标
  android:icon="@drawable/ic_tile_default"
 
  //快捷开关的名称
  android:label="@string/tile_name"
  android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
  <intent-filter>
    <action
      android:name="android.service.quicksettings.action.QS_TILE"/>
  </intent-filter>
</service>

2.2. 准备规范的快捷开关图标

如上所述,android:icon 是显示在通知栏的图标,有以下要求:

  • 要求是 vector drawable 矢量格式(注意是矢量格式)。
  • 图形必须是纯白色,透明背景,图形和功能有明显联系。
  • 大小为 24 x 24 dp (注意单位不是 px)。

快捷开关示例(矢量 + 纯白 + 透明背景):

2.3. 注意以下交互细节

  • 点击:开发者需要确认点击行为是什么,同时确认开关是否有二态变化,如果有,需要准备新的 vector drawable
  • 长按:长按快捷开关,默认会跳转到应用信息页。但开发者可以指定跳转的落地页,通过给 ACTION_QS_TILE_PREFERENCES 增加一个 <intent-filter> 来实现。
  • 自动更新:系统支持开发者更新开关的 UI,一个应用场景是“天气刷新”。开发者视业务需求酌情使用吧。

3. FAQ

3.1. 第三方快捷开关,默认会出现在第一屏吗

不会。第三方快捷开关,默认都会是未添加的状态,只有用户主动添加,才会出现在快捷开关面板中。

3.2. 这个功能对 Android 版本有依赖吗

有,必须是 Android 7.0 及以上的设备。

3.3. 这个功能 MIUI 9 支持吗

不支持,是 MIUI 10 新增的功能。

3.4. 这个功能区分国内版和海外版吗

不区分,只要是 MIUI 10 就支持。

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 最新开发版即可,支持的机型和下载链接如下(找到对应机型的最新开发版):

小米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 最新开发版即可,支持的机型和下载链接如下(找到对应机型的最新开发版):

小米手机MIUI 10 媒体通知适配说明

小米手机MIUI 10 媒体通知适配说明

1.媒体通知是什么

媒体通知(或称 Media template / Media style)是 Android 标准通知样式的一种,自 API 21(Android 5.0) 引入,方便用户直接控制媒体内容,常见于音乐类、广播类应用。

MIUI 10 完全兼容该通知样式,并调整了视觉风格,以符合系统风格,如下图(第一条通知)。

2. 为什么要用系统的媒体通知?

音乐类应用已经都有自己的「自定义通知」可以控制音乐,那为什么还要用系统的媒体通知?

调用系统的媒体通知,系统就能做针对性的优化,如:

  • 正在播放的媒体通知默认置顶 + 默认展开为大视图。
  • 正在播放的媒体通知默认会出现在锁屏上(置顶+大视图)。

这些都能让内容更充分地曝光,也有利于提升用户体验。

注:不同于原生 Android O 允许所有通知出现在锁屏上,MIUI 限制了出现在锁屏的通知类型,以减少用户打扰。如果需要出现在锁屏上,需要前往设置 – 通知和状态栏 – 通知管理,找到对应应用,开启‘’锁屏通知‘’。

3. 如何适配系统媒体通知

3.1 媒体通知的两种视图

媒体通知有两种视图,分别称为:

  • 标准视图 / compact view:标准的通知高度,最多显示3个按钮(actions)。
  • 大视图 / expanded view:面积更大,最多显示5个按钮。

3.2 适配媒体通知

MIUI 10的媒体通知,基于 Android O,以下是几个重要的接口:

媒体通知代码示例如下:

Notification = new Notification.Builder()
    .setContentTitle("Havana(feat.Young Thug")
    .setContentText("Camila Cabello - Havana")
    .setSmallIcon(mySmallIcon)
    .setLargeIcon(album)
    .addAction(previousAction)
    .addAction(playAction)
    .addAction(nextAction)
    .addAction(playlistAction)
    .addAction(favoriteAction)
    .setOngoing(true)
    .setStyle(newNotification.MediaStyle()
        .setShowActionsInCompactView(1,2,3)
        .setMediaSession(mySession)))
    .build();

详细接口说明见官方文档:https://developer.android.com/reference/android/app/Notification.MediaStyle

不过,以下接口在 MIUI 10 无法生效:

以上接口都可能会产生不协调的颜色,故暂时屏蔽。

4. FAQ

4.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

4.2. 只有 MIUI 10 支持媒体通知吗

其实 MIUI 9 也支持媒体通知,但因为没有做好优化。所以,可以大体认为「只有 MIUI 10 支持媒体通知」,含国内版和国际版,且样式和逻辑统一。

4.3. 只要是 MIUI 10 就支持媒体通知吗?

是的。因为 Android 原生从 API 21 (android 5.0)就支持媒体通知了,详见 https://developer.android.com/reference/android/app/Notification.MediaStyle。而 MIUI 10 最低是兼容到 Android 6.0,所以只要是 MIUI 10就支持媒体通知。

4.4. MIUI 10 会禁用自定义通知吗

不会。但系统无法识别自定义通知的具体内容,只会当成普通通知处理。

4.5. MIUI 10 媒体通知的样式以后会变化吗

有可能。主要取决于:

  • Android 增加新的接口:我们后续会尽力兼容新的接口,并以此调整样式。
  • 业务需求和用户反馈。

4.6. MIUI 以前自己做的锁屏音乐界面还有用吗

在 MIUI 10 上没有用了。由于那个页面的实现方式有较多问题,因此我们在 MIUI 10 上弃用了,改由在锁屏上显示媒体通知,来实现锁屏音乐控制的需求。

4.7. 第三方应用自己做的锁屏页面(如音乐类或运动类的覆盖式)还能用吗?

可以继续使用,MIUI 10 没有限制这个能力(不过前提是开启了「锁屏显示」的权限)。但我们不建议开发者做这么一个覆盖式的页面,因为用户体验很差,还是希望能够用媒体通知替代。

4.8. 如何测试

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

小米手机MIUI 9 通知过滤适配说明

小米手机MIUI 9 通知过滤适配说明

自 MIUI 9 开始,我们新增了通知过滤的功能,接下来为大家说明该功能,以解答大家的疑问。

1. 这是什么功能

通知过滤是指通过分析用户对通知的操作,来个性化地确定通知的重要与不重要,然后将不重要的通知收纳进一个统一的页面。具体交互样式如下(MIUI 10),点击不重要通知可直接查看所有被折叠的通知。

2. 如何判断重要与否

评分模型会通过多个维度判断通知重要与否,包括但不限于:

  • 通知的文本:即标题(title) 和描述(description)
  • 该应用的通知在该设备上的历史点击率
  • 用户的属性(如年龄、性别、地域等)

评分模型会综合各个维度对特定用户的影响,个性化地给出该通知的针对该用户的“评分”, 如果“评分” > “过滤阈值”,则认为是重要通知,反之则是不重要通知。

在这些维度中,目前影响最大的维度是历史点击率,这意味着:

  • 对开发者来说,优化通知的点击率(针对设备,而不是大盘),是提高重要性的最好方法,如减少推送频率,提高内容质量等。注意,是点击率,而不是点击量。
  • 历史点击率针对的是设备,而不是大盘用户。这就要求业务使用个性化的推送策略,避免全量推送,以减小被列为不重要通知的概率。

3. 为什么在同一台手机上,同一个应用的通知,有些是重要的,有些是不重要的

如前所述,除了历史点击率,通知的评分也受通知的标题和描述影响,也就使同一应用的不同通知评分产生差别。

4. 一条通知被收进二级页,是不是就不会被点击了

不是的。从目前的数据情况来看,即便是在二级页,也有很多曝光、点击行为。

5. 这个“过滤阈值”对应的历史点击率具体是多少

如前所述,历史点击率只是评分的一个影响因子,过滤阈值可以理解为是一个特殊的评分。由于评分还受到通知文本和用户属性影响,因此无法与历史点击率直接挂勾,我们也就没法给到一个准确的值。

6. 新激活的手机和新装应用如何处理

  • 新激活的手机

新激活的手机上,初始状态各应用的历史点击率都为 0,为了避免这些通知都被直接过滤,过滤阈值在新激活后的前 14 天,会从 0 开始慢慢增长(逐小时更新)。因此新激活用户的大部分通知都不会被过滤。

  • 新装应用或新发通知

如果这是一个新安装的应用,或者之前从来没有发过通知,那么这个应用的历史点击率也为 0。为了避免这个应用的通知直接被过滤,我们加入“首 x 条保护”的规则 :即如果某个应用在当前设备的过去30天都没有产生过通知,那么,该应用在该设备的前 x 条通知,默认都是重要通知,不会被过滤。只要这些通知有1条被点击,后续通知就有很大概率成为重要通知,因此请开发者提高前几条通知的质量。 x 是一个服务端配置的值,目前默认为3。

7. 一旦成为不重要通知,是不是就没法成为重要通知了

不是的。

第一,历史点击率是一个动态变化的值,意味着:

  • 如果用户经常点击你的通知,不管它当时被判断为重要还是不重要,其历史点击率都会不断上涨,超过阈值之后,不重要通知也会变成重要。
  • 如果用户经常不点击你的通知,不管它当时被判断为重要还是不重要,其历史点击率都会不断下降,低于阈值之后,重要通知也会变成不重要。

因此开发者应持续提供优质的通知内容,以保持较高的历史点击率。

第二,历史点击率的计算范围是过去30天。举个极端例子,如果一个应用超过30天停止发送通知,那么这个应用对于系统来说,就等价于一个新安装的应用,将重新开始计算历史点击率,同时,“首 x 条保护”的规则也会生效。

8. 这个功能的影响范围

8.1. 用户范围

  • 目前仅影响国内版用户,国际版暂无此功能(仍为按时间排序)
  • 目前已随 MIUI 9 稳定版发布(仅覆盖国内用户)

8.2. 应用范围

  • 所有在通知栏发通知的应用,包括MIUI 应用和第三方应用。

9. 你们只看历史点击率吗?

不是。如前所述,模型会综合考虑非常多的维度,但目前的训练结果表明,历史点击率是影响权重较大的因子。

10. 是不是历史点击率高,这个应用的所有通知都会是重要的

  • 短期:这个说法近似正确。如前所述,目前历史点击率对“重要”与否的影响比较大, 所以短期来看,这个说法近似正确。但因为也有其他维度的影响,个别通知仍然可能出现差异。
  • 长期:这个说法不正确。未来半年会挖掘更多的维度,可以预见,历史点击率的影响权重会不断降低,那么这个说法就不成立了。

11. 用户可以手动纠正吗

  • 不感兴趣

如果用户觉得某条通知不重要,可以手动从右往左滑通知,点击“不感兴趣”,那之后这个应用的所有通知都会变成不重要了。大家应该注意到,以前的右滑屏蔽不见了(长按通知进入设置页的操作仍保留)。我们不鼓励用户直接屏蔽应用,因此弱化了这个操作,用“不感兴趣”来代替。

  • 设为重要

同样,如果用户觉得某条通知重要,可以从右往左滑通知,点击“设为重要”,那之后这个应用的所有通知都会变成重要。

12. 用户可以关闭这个功能吗

可以。前往设置 > 通知和状态栏 ,关闭“通知过滤”这个开关,就会取消该功能,同时恢复为原有的按时间排序的规则。

13.如何联系我们

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

MIUI 9 & 10“状态栏黑色字符”实现方法变更通知

MIUI 9 & 10“状态栏黑色字符”实现方法变更通知

我们在开发版 7.7.13(2017年的开发版) 及以后的版本,MIUI 状态栏更新了一处实现逻辑。直接影响就是,部分应用无法显示“状态栏黑色字符”,如下图中的即刻、UC、好奇心日报等应用。

1. 问题出现的原因

  • 在 Android 6.0 以前,Android 没有方法可以实现“状态栏黑色字符”效果,因此 MIUI 自己做了一个接口。
  • 在 Android 6.0 及以上版本,Android 提供了标准的方法实现“状态栏黑色字符”效果,但这个方法和 MIUI 的方法产生了冲突,以致于当开发者使用 Android 标准方法时,没有出现预期的效果,这给很多开发者都造成了困扰,尤其是海外开发者。

2. 如何解决

基于以上背景,我们决定兼容 Android 的方法,舍弃 MIUI 的自己的实现方法。从今天的 7.7.13 开发版生效,之后随 MIUI 9 外发。非常抱歉给各位开发者带来麻烦,但长远来看,兼容 Android 的标准,对 MIUI 和开发者都更为有利。

3. 开发者需要做的

如果开发者需要设置“状态栏黑色字符”的效果, 需要做以下几件事:

3.1. 在新的 MIUI 版本(即基于 Android 6.0 ,开发版 7.7.13 及以后版本)

  • 使用 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR ,来设置“状态栏黑色字符”效果
  • 同时要设置 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
  • 并且不设置 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS

  参考实例:

Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);

3.2. 在旧的MIUI版本

还有大量的用户使用旧的 MIUI 版本,因此仍然需要使用 MIUI 原有的方法,即

public void setStatusBarDarkMode(boolean darkmode, Activity activity) {
        Class<? extends Window> clazz = activity.getWindow().getClass();
        try {
        int darkModeFlag = 0;
        Class<?> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
        Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
        darkModeFlag = field.getInt(layoutParams);
        Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
        extraFlagField.invoke(activity.getWindow(), darkmode ? darkModeFlag : 0, darkModeFlag);
        } catch (Exception e) {
            e.printStackTrace();
        }
}

对于广大开发者而言,为了保证在新旧版本的 MIUI 都能实现「状态栏黑色字符」的效果,需要开发者同时写上以上两种实现方法。给各位开发者带来麻烦,再次表达歉意,望谅解。文末附上了常见问题,如果在适配过程中遇到问题,可以邮件给我们的项目组 miuix-systemui@xiaomi.com,会有同事解答相关疑问。。

4. FAQ

4.1. 如何调试

如前所述,相关改动将从 7.7.13 开发版(2017年的版本)及以后版本生效,因此开发者升级到  MIUI 10 最新开发版即可,支持的机型和下载链接如下(找到对应机型的最新开发版):

4.2. 如何实现状态栏白色字符效果

int flag = window.getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
window.getDecorView().setSystemUiVisibility(flag);​

小米商店小米上架“我的收藏”接入说明

小米商店小米上架“我的收藏”接入说明

1.“我的收藏” 简介

“我的收藏”是信息助手(负一屏)上一张默认开启的卡片。

能同步将各应用中的收藏内容添加到“我的收藏”,并通过“我的收藏” 卡片和全局搜索来查找使用。

2.为什么要做“我的收藏”

日常使用的大多数应用都有收藏功能,而手机上的收藏功能目前存在如下3个问题:

  • 查找难:收藏的内容忘记在哪个应用,且应用的收藏入口不同,查找费力;
  • 忘记查看:收藏的内容本来准备先码后看,因入口过深而遗忘;
  • 收藏方式不统一:市面上现有的全局收藏方式较为复杂,且不统一,有一定学习成本; 

3.如何使用“我的收藏”

3.1如何添加收藏

当用户在应用中有收藏行为时,应用主动发送广播,将当前收藏内容发送到“我的收藏”,“我的收藏”收藏成功后,会弹出通知提醒。 (与原应用的收藏方式相同,无需用户学习新的收藏方式) 

3.2如何查看收藏内容

目前有2种方式查找使用收藏内容:我的收藏卡片和全局搜索。

3.2.1信息助手(负一屏)我的收藏卡片

作为负一屏默认开启的常驻卡片,展现最新收藏的3篇文章。

3.2.2 全局搜索

通过全局搜索对标题、来源进行搜索查找收藏的内容。

3.3收藏的打开方式

通过合作接口添加到“我的收藏”中的所有内容,打开方式均通过原应用打开。

4. 合作方接入的好处是

· 提升产品DAU和带来一定量的新增

通过接口收藏的内容,都会通过原应用打开。若应用被卸载,会提示用户安装后使用。

· 提升品牌曝光和内容展现

收藏的内容会展现在负一屏收藏卡片,同时通过全局搜索可以查找到收藏内容。

5. 接入技术文档

接入方式,因收藏动作无需回调结果,所以可以使用原生最简单的广播接口来实现。 

    private interface IntentConfig {
        public static final String String ACTION = "com.miui.personalassistant.action.FAVORITE";
        public static final String String PACKAGE = "com.miui.personalassistant";   //发送广播指定的包名
        public static final String String PERMISSION = "com.miui.personalassistant.permission.FAVORITE";    //发送广播指定的权限
        public static final String String BUNDLES = "bundles";
        public static final String String ACTION_FAV = "action_fav";
    }
 
    private interface BundleConfig {
        public static final String MATCH_COMPONENT = "matchComponent";
        public static final String MATCH_ACTION = "matchAction";
        public static final String TARGET_URL = "targetUrl";
        public static final String TARGET_DATA = "targetData";
        public static final String TARGET_TITLE = "targetTitle";
        public static final String TARGET_IMAGE = "targetImage";
        public static final String TARGET_EXTRA = "targetExtra";
    }
 
 
 
/**
 *  批量创建bundle方式
 *
 */
    /* 添加收藏动作如下 */
    // 指定跳转应用,方案有1、2两种,选其一
    // 1、设置当前收藏的 ComponentName(以便收藏后显示来源和原App跳转), 形式如(packageName/className全称):com.android.browser/com.android.browser.BrowserActivity
    // 2、设置intent action,另需要ComponentName设置为packageName,intent.setAction(action)  intent.setPackage(packageName)跳转使用
    Bundle bundle = new Bundle();
    bundle.putString(BundleConfig.MATCH_COMPONENT, componentName);  //收藏跳转对应APP使用:setComponent(component);若设置通过intent action跳转应用,则component设置为packageName
    bundle.putString(BundleConfig.MATCH_ACTION, actionName);  //收藏跳转对应APP使用:Intent.setAction(action)
  
    bundle.putString(BundleConfig.TARGET_URL, targetUrl);  // 参数类型http/https,指详情页的具体链接地址,用于对应应用卸载后,网页显示详情,webview.loadUrl(targetUrl)
    bundle.putString(BundleConfig.TARGET_DATA, targetData);  //对应详情页面的intent data,从收藏跳转到对应APP的详情页使用:Intent.setData(Uri targetData)
    bundle.putString(BundleConfig.TARGET_TITLE, targetTitle); // 必选,收藏条目显示标题
    bundle.putString(BundleConfig.TARGET_IMAGE, targetImage);   //可选,收藏图片url,推荐大小180x180px(宽高比为1:1)
    bundle.putString(BundleConfig.TARGET_EXTRA, targetExtra);  // 可选,用于有额外需求的参数传递
 
 
    /* 取消收藏动作如下 */
    // 只需要取消对应的链接
    Bundle bundle = new Bundle();
    bundle.putString(BundleConfig.TARGET_URL, targetUrl);
     
    // 向集合中添加所有要收藏数据的bundle
    ArrayList<Bundle> bundleList = new ArrayList<Bundle>();
    for (int i = 0; i < length ; i++) {
        bundleList.add(bundle);
    }
 
    // 发送收藏广播
    Intent intent = new Intent(IntentConfig.ACTION); // 指定广播Action
    intent.putParcelableArrayListExtra(IntentConfig.BUNDLES, bundleList);
    intent.putExtra(IntentConfig.ACTION_FAV, true);  //true添加收藏动作,false取消收藏动作
    intent.setPackage(IntentConfig.PACKAGE); // 限定当前收藏广播接收者的包名和权限
    context.sendBroadcast(intent,  IntentConfig.PERMISSION);
 
    //考虑到数据安全,外发应用需要判断下是否是小米手机,如果是,则发送广播,否则取消发送
    使用android.os.Build.MANUFACTURER,其值为Xiaomi即为小米设备

6.其他说明

6.1“我的收藏”已经支持哪些版本

MIUI 9及以上的版本都已经支持“我的收藏”功能。

6.2“我的收藏”是否还有进一步的合作方式

我们愿意与合作方一起探索更深入的合作,比如收藏商品降价提醒、追剧等可能的方向。

小米商店 MIUI 10 桌面图标上传规范

小米商店 MIUI 10 桌面图标上传规范

1. 背景

自 MIUI 10 开始,小米手机桌面默认图标将升级为全新的样式,图标更精致,圆角更小。

2. 规范

小米手机桌面是直接读取应用包内图标的,但为了保证桌面图标的视觉统一,系统会对图标作一定的截剪缩放,针对「方形」和「非方形」图标,我们有不同的处理规则,详情请见附录一和附录二。希望开发者按以下要求自查或优化现有的包内图标:

  1. 方形图标请保证是直角,且周围无透明像素。由于 MIUI 10 默认桌面图标的圆角很小,请开发者避免上传大圆角的图标。
  2. 提供更大尺寸的图标,防止图标模糊,具体建议尺寸如下:
  • xhdpi:≥ 94px
  • x​xhdpi:≥ 130px

桌面图标是用户对应用的第一印象,希望各位能够保证应用在小米手机上呈现最好的视觉效果。感谢各位开发者的支持与配合,谢谢!

3. 附录

附录一:方形图标的缩放/裁剪规则

附录二:非方形图标的缩放规则

小米手机APP桌面应用角标适配说明

小米手机桌面应用角标适配说明

1. 默认逻辑

当应用向通知栏发送了一条通知 (除了进度条样式和常驻通知外),应用图标的右上角就会显示「1」。角标的数字代表应用的通知数,即应用发送了「x」条通知,角标就会显示为「x」。

2. 开发者如何设置桌面角标

2.1 MIUI6-MIUI11桌面应用角标适配方法

可通过反射调用设置桌面角标,参考代码如下:

try {
    Field field = notification.getClass().getDeclaredField(“extraNotification”);
    Object extraNotification = field.get(notification);
    Method method = extraNotification.getClass().getDeclaredMethod(“setMessageCount”, int.class);
    method.invoke(extraNotification, mCount);
} catch (Exception e) {
    e.printStackTrace();
}

2.2 MIUI12及以后桌面应用角标适配方法

由于Google屏蔽了hideAPI的反射调用,因此MIUI12及以后可以使用notification.number,可参照Android开发者文档https://developer.android.google.cn/reference/android/app/Notification#number,参考代码如下:

Notification notification = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle(textTitle)
            .setContentText(textContent)
            .setNumber(int number)
            .build();

3. 如何判断MIUI版本

可参考文档https://dev.mi.com/console/doc/detail?pId=1312,其中6.1节有具体方法说明。

小米手机全面屏手势提示线(虚拟键)适配说明

小米手机全面屏手势提示线(虚拟键)适配说明

1.背景

MIUI12将加入”手势提示线”,在设置-更多设置-全面屏中,可以通过开关打开/关闭该功能。手势提示线的实现原理参考了原生的方式,和虚拟按键在同一个window当中,可以理解成是虚拟按键的一种新的形态。

  • 适配的原则是:”手势提示线”(也就是虚拟键)的背景颜色和页面整体的背景颜色保持一致;
  • 主要有两种适配方式:”使用沉浸式虚拟键”和“给虚拟键设置合适的颜色”。

  图例为开启“手势提示线”效果                              图例为关闭“手势提示线”效果

2.使用沉浸式虚拟键

2.1如何设置

沉浸式虚拟键,即app的content view延伸到虚拟键区域,虚拟键的颜色透明。类似的,还有沉浸式状态栏,即app的content view延伸到状态栏区域,状态栏的颜色透明。

有两种设置的方式,参考代码如下。

方式1:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.test_layout);
 
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);       //设置沉浸式状态栏,在MIUI系统中,状态栏背景透明。原生系统中,状态栏背景半透明。
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);   //设置沉浸式虚拟键,在MIUI系统中,虚拟键背景透明。原生系统中,虚拟键背景半透明。
}

方式2:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.test_layout);
 
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    getWindow().setStatusBarColor(Color.TRANSPARENT);
    getWindow().setNavigationBarColor(Color.TRANSPARENT);
    getWindow().getDecorView().setSystemUiVisibility(
        View.SYSTEM_UI_FLAG_LAYOUT_STABLE|
        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    );
}

2.2可能存在的问题

设置了沉浸式虚拟键和沉浸式状态栏之后,状态栏和虚拟键会遮挡住app的内容,效果如下:

2.3解决方案

如果不希望app的内容被遮挡,可以给view设置  android:fitsSystemWindows=”true”,设置之后,系统会给该view自动加上paddingTop和paddingBottom。参考代码和效果如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/apps_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#0000FF"
    android:fitsSystemWindows="true">

2.4适用的场景举例

当页面具有复杂的背景或纹理时:

3.给虚拟键设置合适的颜色

3.1如何设置

使用setNavigationBarColor来设置虚拟键的颜色。注意不要单独把颜色设置成透明,如果要设置成透明的话,参考上面的沉浸式虚拟键,配合其他flag使用。

参考下面的代码,按照这种方式设置之后,app的content view将不会延伸到虚拟键区域。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.test_layout);
 
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    getWindow().setStatusBarColor(Color.BLUE);     //设置状态栏的背景颜色
    getWindow().setNavigationBarColor(Color.BLUE);  //设置虚拟键的背景颜色
}

3.2可能存在的问题

同一个应用往往有多个子页面,如果页面中背景颜色发生了变化,为了让虚拟键的颜色和页面的颜色保持一致,需要重新设置虚拟键的颜色,而不是统一给应用设置一个色值。

目前大部分业务已经适配了深色模式,因此在深色模式下还需要单独对虚拟键背景做深色适配。

3.3适用的场景举例

使用底部tab的颜色或者页面背景的颜色

小米商店上架APP权限用途用户告知适配说明

1.系统权限描述

MIUI11中,应政府对手机软件权限获取的合规要求,MIUI将支持三方应用通过向系统接口传输参数的方式,显示获取运行时权限的目的及用途,具体方案如下:

应用申请运行时权限时,可通过接口传输权限用途及描述,满足政府对于软件获取权限的合规要求,同时通过有效行为描述增加用户授权的可能性。

2.接口开放标准

2.1.填写权限使用申请

  • 仅支持通过小米应用商店上架的应用申请使用接口,申请时请按以下内容填写权限申请资料:
权限名称是否申请权限用途及说明(请提供简体中文及英语,中文字符不超过18个汉字)
Manifest.permission.READ_CONTACTS  
Manifest.permission.WRITE_CONTACTS  
Manifest.permission.GET_ACCOUNTS  
Manifest.permission.WRITE_CALENDAR  
Manifest.permission.SEND_SMS  
Manifest.permission.RECEIVE_SMS  
Manifest.permission.READ_SMS  
Manifest.permission.RECEIVE_MMS  
Manifest.permission.READ_EXTERNAL_STORAGE  
Manifest.permission.WRITE_EXTERNAL_STORAGE  
Manifest.permission.ACCESS_FINE_LOCATIONManifest.permission.ACCESS_COARSE_LOCATION  
Manifest.permission.READ_CALL_LOG  
Manifest.permission.WRITE_CALL_LOG  
Manifest.permission.PROCESS_OUTGOING_CALLS  
Manifest.permission.READ_PHONE_STATE  
Manifest.permission.READ_PHONE_NUMBERS  
Manifest.permission.CALL_PHONE  
Manifest.permission.ADD_VOICEMAIL  
Manifest.permission.USE_SIP  
Manifest.permission.ANSWER_PHONE_CALLS  
Manifest.permission.RECORD_AUDIO  
Manifest.permission.ACTIVITY_RECOGNITION  
Manifest.permission.CAMERA  
Manifest.permission.BODY_SENSORS  
  • 重要提示:以下权限,在MIUI 11 3.12 的开发版后将无法获取:
Manifest.permission.READ_PHONE_STATE

Manifest.permission.READ_PHONE_NUMBERS

Manifest.permission.CALL_PHONE Manifest.permission.ANSWER_PHONE_CALLS

2.2.发送权限申请至审核邮箱

填写权限使用申请后,请按如下格式将内容发送至miui-security-open@xiaomi.com

邮件主题:申请使用系统权限描述

【申请应用】填写应用名称

【应用包名】com.xxxx.xxxx

【公司主体】填写公司主体名称

【相关负责人】填写相关负责人

【联系方式】填写相关负责人联系方式

【权限申请目录】粘贴权限使用申请内容

2.3.等待审核

发送邮件后,请等待反馈,反馈时间在1-3个工作日内,若审核通过,将在反馈中提供调用方法并开设白名单。

2.4.技术接入

确定开放白名单后,请按以下方式接入:

申请示例:

private String[] permissions = new String[]{
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO,
        Manifest.permission.ACCESS_FINE_LOCATION
};



private String[] permissionDescs = new String[] {
        "存储用户精修美图",
        "拍摄抖音短视频",
        "游戏中进行语音交流",
        "获取地理位置来提供导航功能"
};


private List<String> mPermissionList = new ArrayList<>();
private static final int PERMISSION_REQUEST = 1;

//开始申请
for (int i = 0; i < permissions.length; i++) {
    if (ContextCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
 mPermissionList.add(permissions[i]);
 mPermissionList.add(permissionDescs[i]);
    }
}
if (mPermissionList.isEmpty()) {
    //业务操作
} else {
    String[] permissions = mPermissionList.toArray(new String[mPermissionList.size()]);
    ActivityCompat.requestPermissions(MainActivity.this, permissions, PERMISSION_REQUEST);
}


//申请回调
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 if (requestCode == PERMISSION_REQUEST) {
        //业务操作
    }
}

 以上是最新推荐的申请权限的方式,只需要在申请的权限数组中加上对应的权限描述即可。

2.5.注意事项

权限及传入的权限描述要一一对应,权限描述不能传入null,若不需要描述,可传入空字符串:” “。

面向隐私保护的权限使用行为管理说明

1.背景

自MIUI于2010年8月16日上线以来,用户的隐私及安全都是我们最关注的要点。

小米于2018年起草《移动智能终端补充设备标识规范》,应对IMEI等设备标识被篡改及滥用的信息安全问题,确保用户数据隐私不被泄露。

在用户的隐私方面,MIUI希望可以通过系统权限隐私保护逻辑,在用户的隐私及数据安全方面有进一步的突破与保护措施。

2.权限改动

2.1.禁止获取不可重置的设备标识

  • 安卓P版本中将不再支持通过READ_PHONE_STATE权限获取不可重置的设备标识。具体表现与Android Q一致,获取到空值不抛出异常。
  • 生效时间:开发版-MIUI11 3.12(小米6)。
  • 针对READ_PHONE_STATE权限在Android P各MIUI版本的表现为:
版本/设备标识IMEI/MEIDIMSIICCIDSerialOAID
MIUI 11开发版(3.12之前)可以获取可以获取可以获取可以获取可以获取
MIUI 11稳定版可以获取可以获取可以获取可以获取可以获取
MIUI 11开发版(3.12之后)不可获取不可获取不可获取不可获取可以获取
  • 适配方法:可以通过”1″.equals(SystemProperties.get(“ro.miui.restrict_imei_p”))为true判断Android P上已经开启了设备标识的限制策略。
  • 建议开发者使用OAID替代IMEI等设备标识作为用户标识。OAID获取方法:http://msalliance.icoc.bz/col.jsp?id=120

2.2.CALL_PHONE 及 ANSWER_PHONE_CALLS

  • 除应用被设置为默认拨号应用外,CALL_PHONE权限及ANSWER_PHONE_CALLS将默认禁止获取。
  • 生效时间:开发版-MIUI11 3.12。
  • 适配方法:若业务需要以上两项权限,可从产品逻辑角度引导用户将应用设置为默认拨号应用。

2.3.禁用后台拍照行为

  • CAMERA权限仅支持应用在前台时调用,应用处于后台时将禁止使用该权限进行拍照或录像等行为。
  • 生效时间:开发版-MIUI11 3.12。
  • 适配方法:请开发者检查业务是否有相关行为并修改相关业务逻辑,避免出现异常情况;若出现不可避免的后台使用相机行为,请通过调起前台服务实现。

2.4.定位、录音权限用途描述

当应用申请定位、录音权限时,系统会根据应用是否对权限进行用户说明来判断授权弹窗中出现的选项:

  • 若应用未对权限用途进行说明,则显示“拒绝”“允许本次”及“仅在应用使用中允许”选项;
  • 若应用已进行权限用途说明,则在以上选项的基础上,增加显示“始终允许”选项。

该显示逻辑不影响基础权限选项,若用户进入应用授权管理,仍可对权限设置始终允许。

  • 生效时间:开发版-MIUI11 3.12。
  • 适配方法:请开发者进行权限用途说明,接入权限用途说明请查看:”MIUI权限用途接入说明“。

小米手机深色模式适配说明

小米手机深色模式适配说明

1.深色模式背景与介绍

1.1.安卓原生深色模式介绍

Android 10 (API 级别 29) 及更高版本中提供深色主题背景。深色主题背景具有诸多优势:

  • 可大幅减少耗电量(具体取决于设备的屏幕技术)。
  • 为弱视以及对强光敏感的用户提高可视性。
  • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

深色主题背景同时适用于 Android 系统界面和在设备上运行的应用。在 Android 10 (API 级别 29) 及更高版本中,您可以通过以下三种方法启用深色主题背景:

  • 使用系统设置(Settings -> Display -> Theme)启用深色主题背景。
  • 使用“快捷设置”图块,从通知托盘中切换主题背景(启用后)。
  • 在 Pixel 设备上,选择“省电模式”将同时启用深色主题背景。其他原始设备制造商 (OEM) 不一定支持这种行为。

可参考:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#top_of_page

1.2.MIUI深色模式介绍

深色模式是一种将屏幕主色调转为深色的模式。如下图MIUI10界面为例,上图是正常浅色背景,下图为MIUI深色模式。

用户在MIUI中有两个方式可以打开深色模式:1.设置-显示-深色模式 2.下拉控制中心 中可以开启,开启后将全局变黑如上图显示:

对于适配深色模式的应用来说,我们会优先启用应用的深色模式。未接入的应用,则会通过算法进行反色。适配方式将在第三部分详细描述。如果用户不希望某个应用被反色,用户可以手动关闭该应用的反色功能,见下方:

目前已有众多知名头部三方应用适配了深色模式,例如微信、QQ、爱奇艺、优酷、知乎、小红书、钉钉等。

2.适配深色模式的好处

我们强烈建议您为您的应用适配深色模式,主要有如下原因:

  • 更酷、更时髦的流行趋势

作为一种全新的潮流,黑色界面受到众多用户、尤其是年轻用户的欢迎。目前,各大安卓系统、iOS系统都已经支持深色模式,众多主流头部应用也已适配或正在适配深色模式。                

  • 让用户更专注于内容

深色背景下,文字、图片、视频都能更清晰地呈现,尤其是暗光环境下。对于浏览器、资讯和视频类app,深色可以让用户沉浸其中,为应用贡献更多的使用时长:对于使用时段是晚上的应用,这一特性更加明显。                                               

  •  降低应用的耗电量                                               

省电是用户最关注的性能之一。低电情况下,用户更青睐使用深色模式的应用。根据小米实验室测试数据:

OLED屏幕100%亮度下,深色模式耗电相比浅色模式,最高降低83%;

OLED屏幕50%亮度下,深色模式耗电相比浅色模式,最高降低50%。

  • 推广优待适配深色模式时可以联系我们,小米可以为应用提供应用商店专题和微博宣传等方式,为应用增加额外的宣传。

3.适配方式

适配方式有两种:

  • 适配深色模式资源:开发者根据自身应用设计深色模式,按照适配规范进行适配。
  • 适配全局反色:利用安卓Q上提供的forcedark能力可以直接反色。

在适配前,开发者需要考虑是否在自有应用内增加深色模式开关。我们建议的方式:

  • 务必提供跟随系统深色模式的选项。可以默认跟随系统,或在监测到系统切换为深色/浅色时提示用户。 
  • 可以为用户提供手动切换的开关。

目前已有三方应用如下(左 小红书 右 QQ),仅供参考:

方式一 适配深色模式资源                                           

适配深色模式资源需要设计师先对所有页面设计深色页面,再由开发完成深色模式资源开发。

优点:在所有安卓版本、所有手机厂商用户均可以使用,且体验较好。

3.1.谷歌适配要求

开发者可基于谷歌深色模式适配标准进行适配:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#top_of_page

已经适配过的页面要将ForceDarkAllowed()​参数值设置为false,若整个主题均已适配深色模式,则需要将这个主题的ForceDarkAllowed()参数值设置为false。

3.2.小米深色模式配色标准设计

其余适配建议详见设计文档:附件1-小米深色模式配色标准设计文档。

3.3.接入方式

3.3.1.接入介绍

  • 根据谷歌深色模式接入标准,如要支持深色主题背景,您必须将应用的主题背景(通常可在res/values/styles.xml中找到)设置为继承 ​DayNight​ 主题背景:
<style name="AppTheme" parent="Theme.AppCompat.DayNight">

您还可以使用 MaterialComponent 的深色主题背景

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">

这会将应用的主要主题背景与系统控制的夜间模式标记相关联,并将应用的默认主题背景设置为深色主题背景(如果已启用)。详细内容见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#force_dark

从开发的角度说,就是打开夜间模式后,系统会优先从xxx-night资源中寻找资源并替换。
Android系统从2.2开始就已经⽀支持了了DarkMode,其对应接口是名为“uimode”的系统服务,DarkMode所使⽤用到的接口方法是:setNightMode、getNightMode分别对应设置夜间模式和获取设置状态。

// 获取uimode系统服务
UiModeManager uiModeManager = (UiModeManager)
getSystemService(Context.UI_MODE_SERVICE);                                             
// 获取设置状态
int currentMode = uiModeManager.getNightMode();                                               
// 设置夜间状态
uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_AUTO); // ⾃动
uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_YES); // 启⽤
uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO); // 停⽤
                                             
在日志中的体现:
------------------------------------------------------                                                        
DUMP OF SERVICE uimode:
Current UI Mode Service state:                                                        
mDockState=0 mLastBroadcastState=0                                                        
mNightMode=2 mNightModeLocked=false
mCarModeEnabled=false mComputedNightMode=false
mCarModeEnableFlags=0 mEnableCarDockLaunch=true                                                        
mCurUiMode=0x2d mUiModeLocked=false mSetUiMode=0x2d
mHoldingConfiguration=false mSystemReady=true
mTwilightService.getLastTwilightState()=null                                                        
--------- 0.001s was the duration of dumpsys uimode,ending at: 2019-01-18 00:00:01--------------

从uimode服务dump出来的信息可以判断:

mNightMode的取值:0 → AUTO; 1 → NO; 2 → YES另外,在Android O开始,Android⽀支持adb命令启动和停⽤用DarkMode,具体命令是:adb shell cmd uimode night <auto | yes | no>

O以前的机型,只能通过代码调用uimode接口启动和停用DarkMode。需要注意的是,miui虽然从level 16开始支持DarkMode,但是并不意味着miui之前的版本不能切换DarkMode,用户完全可以通过命令或者代码调用系统接口,启动和关闭DarkMode。而Setting中的开关是阻⽌不了的,除⾮MIUI对这个权限进行收紧。   

MIUI SDK从level 16开始,支持DarkMode,所以的支持DarkMode,是指MiuiSDK增加了新的主题,DayNight,来自适应的适配Light和Dark主题。其实在16之前的SDK上,Light和Dark主题都是一直存在的,只是没有一个自动适配的机制存在,而且Dark主题存在一些Bug,并不是它应该是的样⼦。适配过程中,存在一些APP,之前就是在Dark主题下进行的开发,那么在sdk level 16以后,发现Dark模式存一些问题,比如弹窗变成了⿊⾊,均是因为Dark主题修改造成的。附录中会给出SDK针对Dark和Light进行的改动,包括所有受影响的资源和主题样式修改。

3.3.2.接入方式详细说明                                              

重点说明⼀下接⼊和使⽤⽅式。这⾥除了Theme.Light.DarkActionBar这个主题以外,所有Light(Dark)主题均有对应的DayNight主题。

  • 对于非解耦应⽤,可以直接使⽤Miui SDK的public的资源和属性。

代码接⼊方式:可以直接将使⽤MiuiSdk的Light或者Dark主题的地方,替换成DayNight主题,⽐如:将

<style name="MiuiDemo.Theme.Test" parent="miui:Theme.Light">
或者<style name="MiuiDemo.Theme.Test" parent="miui:Theme.Dark"> 
替换成<style name="MiuiDemo.Theme.Test" parent="miui:Theme.DayNight">

Java代码中:将使⽤Light主题的所有地⽅miui.R.style.Theme_Light替换成miui.R.style.Theme_DayNight

  • (如果本身就是⾃升级应用,就不⽤看这⼀步了)对于解耦自升级应⽤,需要在gradle中进行如下操作:   
1)修改build.gradle中的dependencies,使⽤用miui插件,如下:(以gradle 3.3.0为例)
dependencies {                                               
classpath 'com.miui.tools.build:gradle:3.3.0'
}
2)gradle/wrapper/gradle-wrapper.properties⽂文件中的gradle修改到5.0,即:
distributionUrl=http://sdk.pt.miui.com/miuisdk/software/gradle/gradle-5.0-all.zip
3)此步,3.2.0可以忽略略,在3.3.0下必须将:                                                
apply plugin: 'com.android.application'
改成
apply plugin: 'com.miui.application'                                       
4)如果gradlew的⽇日志中有如下输出:
add property to gradle.properties of this project:                                                
android.aapt2FromMavenOverride=/home/{username}/.miuisdk/build-tools/aapt/28/aapt2
则修改gradle.properties,最后加一行:
android.aapt2FromMavenOverride=//home/{username}/.miuisdk/build-tools/aapt/28/aapt2
5)增加依赖:
compileOnly 'com.miui:core:alpha-SNAPSHOT'

于是,miui sdk中的Theme.DayNight就接入完成,在APP中可以直接使⽤Theme.DayNight.<…>主题。针对之前的miui:Theme.Light主题或者miui:Theme.Dark主题,均有对应的Dark或者Light主题。所以,如果需要适配自动切换夜间和日间模式,只要替换成对应的DayNight主题即可,DayNight主题负责⾃动切换两种(Dark和Light)主题。

  • (非单发应⽤请忽略)对于单发应用:单发应用的接⼊⽅式,可以参考:MIUI SDK support包的使用指南将依赖由:compileOnly ‘com.miui:core:alpha-SNAPSHOT’改成 implementation ‘com.miui.support:core-compat:alpha-SNAPSHOT’
  • 对于单发或者⾃升级应⽤,在代码接入上完全可以参考1中的代码接入方式。即,替换Light或者Dark为DayNight。但是,对于⾃自升级来说(单发应⽤不会有这个问题,因为单发应用已经将DayNight主题打包进APK),由于不确定⾃升级的目标ROM是否⽀持DayNight主题,所以如果在没有DayNight主题的ROM上⾃自升级,就会崩溃。这⾥需要看下⾯的注意事项1,可以找到解决方案。详见开发文档:附件2-深色模式开发文档

方式二 适配全局反色

全局反色利用安卓Q上提供的forcedark能力可以直接反色,对于开发者来说较为友好。

  • 优点:开发工作量小,适配较快。
  • 缺点:仅安卓Q手机可以使用该功能,复杂页面用forcedark适配难度较大;非原生的Webview无法反色,图片无法反色。

可以在安卓Q手机上,打开以下开关即可体验安卓Q反色能力,查看本应用反色后的情况。开关位置如下:

3.4.谷歌Force Dark适配要求

Android 10 提供 Force Dark 功能。一如其名,此功能可让开发者快速实现深色主题背景,而无需明确设置 ​DayNight​ 主题背景。如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。有些开发者会混合使用 Force Dark 和本机实现,以缩短实现深色主题背景所需的时间。应用必须选择启用 Force Dark,方法是在其主题背景中设置 ​android:forceDarkAllowed=”true”​。此属性会在所有系统及 AndroidX 提供的浅色主题背景(例如 ​Theme.Material.Light​)上设置。使用 Force Dark 时,您应确保全面测试应用,并根据需要排除视图。如果您的应用使用深色主题背景(例如​Theme.Material​),则系统不会应用 Force Dark。同样,如果应用的主题背景继承自 DayNight​主题背景,则系统不会应用 Force Dark,因为会自动切换主题背景。

3.5.MIUI深⾊模式适配的目标和方法

在MIUI12上推出全局的深色模式,所有系统应用和主流三方应用适配。在MIUI12上用户开启深色模式后将默认开启全局反色。要达到的效果:系统切换到深⾊模式以后,应⽤也随之切换到深色模式,这是我们这次MIUI适配深色模式要达到的目标,也是我们对每个系统应⽤的要求。

适配的⽅法有三种:                                          

  • Android原生的深色主题
  • 全局反⾊
  • 第三方框架或者应用⾃自己实现深色模式  

目前只有Android Q深色模式支持全局反色:Android Q上提供了两种实现深色模式的⽅式:深色主题和全局反⾊。

深⾊主题即Theme.AppCompat.DayNight或者Theme.MaterialComponents.DayNight。深⾊主题是Android官⽅提供的实现深色模式的一种方法,应用也可以其它自己的方法实现深色模式。深⾊主题起作用后,将会加载-night⽬目录下的资源,如果没有找到,那么加载默认资源。对于标准控件,如果应用不提供深色模式的资源,系统也会提供一套默认的深色资源(不过比较难看,基本不可用)全局反色即ForceDark,系统会根据一套算法,计算出每个View属于前台还是后台。对于前台View,会使其变亮,对于后台View,会使其变暗,⽽不需要开发者提供两套资源。具体的原理可以移步Android Q Force Dark调研。如果一个activity使⽤了深色模式,那么全局反色对其将不生效。    

3.6.接入方式介绍

3.6.1.建议的整体适配步骤                                              

  • 现状评估

打开系统的深⾊模式,看看⾃⼰应用中那些⻚面有没有问题,一般可以分为三类:

A.适配了深色模式没有问题

B.没有适配深色模式的

C.适配了深⾊模式但是有问题的

  • 全局反色过滤

对于所有的A,请务必禁用全局反色,否则全局反色可能会和已有的深⾊模式效果冲突。对于所有的B,可以设置 <item name=”android:forceDarkAllowed”>true</item> ,然后可以打开全局反色,观察有没有问题。没有问题的,那就适配完成了,问题⽐较⼤的归为D。

  • 全局反色问题分析                                       

对于所有的D,分析问题的原因,是反色本身的问题还是⾃己应⽤导致的,⽐如布局存在有问题,有图片、webVIew等。其他的看下能不能通过调整布局等最后还是通过全局反⾊简单解决,剩下统一归为C。

  • 重新适配

对于C,看看是不是原来的适配方法有问题,有没有必要采用新的适配⽅法、框架,可不可以采用系统的深色主题等重新适配。                                              

  • 验证

打开系统深色模式,重新测试所有页⾯。

比较理想的做法是:建议采用深色主题和全局反色相结合的方式。整个应用用的主题使用DayNight,设置<item name=”android:forceDarkAllowed”>false</item>。对于已经⽀持深色主题或者用其他方法实现了深⾊模式的⻚面,没必要再支持全局反⾊,务必禁⽤。对于需要使用反⾊的⻚面,使⽤Light主题,设<itemname=”android:forceDarkAllowed”>true</item>。其他⻚面单独适配。                                             

3.6.2.深色主题的适配

深色主题在Android Q之前就是已经有了的,只不过其在Android Q上得到了了加强。一个activity如果想使用深色主题,让其主题继承于DayNight主题或者让整个应用的主题继承于DayNight主题即可。不过,上⾯的做法只适合跟随系统变化的需求,如果有高级的需求,⽐如应用想要主动切换到深⾊色主题或者不跟随系统变化,而是让⽤户选择,那么需要这样做:

1.监听UImode的变化。manifest中 activity 设置属性 android:configChanges=”uiMode”,并重写onConfigurationChanged。注意,声明了configChanges系统将不不会重建对应的activity,⽽而是回调onConfigurationChanged,需要activity⾃己处理变化的情况

2.让Activity继承于AppCompatActivity,这样子可以通过AppCompatDelegate.setDefaultNightMode(mode) 设置当前activity的mode,通过AppCompatDelegate.getDefaultNightMode()获取当前设置的mode。系统可以选择的mode有四种,具体解释可以看下面第1篇文章。

3.用户选择夜间模式的时候,保存用户选择的模式到SharedPreferences,调用recreate()重启activity,注意recreate()是重新创建activity,会回调所有生命周期方法。

4.activity启动的时候,取出之前保存的模式,通过AppCompatDelegate.setDefaultNightMode(mode) 进行当前activity的mode。                                            

5.系统UImode发⽣变化的时候,即onConfigurationChanged时,获取系统当前的UImode,根据应用⾃身的逻辑判断需不需要改变当前activity的mode,如果需要改变重复第3、4步。

切换到深⾊模式时,资源应用的顺序:                                  

  • 应用提供的-night资源
  • 应⽤设置的默认资源(通常是亮色下的资源,也可以是应用主动在深⾊模式下设置的主题)
  • 系统的默认深⾊资源
  • 系统默认的亮色资源

所以,凡是应用⾃己提供资源了的,那就提供两套,要么就全部⽤用系统的默认资源。如果一个应⽤没有使用DayNight主题,那么只会变化应用提供的-night深⾊资源,其他不会变化,系统也不会应⽤标准控件的默认深色资源。

具体实现,可以参考:  

https://segmentfault.com/a/1190000011472198
https://blog.csdn.net/xiaoxiaocaizi123/article/details/90370275

有两种方式,⼤同小异,相信大家能看得懂。

3.6.3.全局反色的适配      

⾸先来看下全局反色生效前提:

系统端:1.开启深色模式

应用端:1.activity对应的主题是Light的 2.对应的控件允许全局反色,默认是forceDarkAllowedDefault,这个值由系统属性debug.hwui.force_dark决定,debug.hwui.force_dark⼜是由全局反⾊的开关控制,默认是关闭的。         

以上几点缺⼀不可,需要详细说明几点是:

1.实现了深色主题的activity是不会受全局反色的影响,前面说了,深⾊主题是DayNight,不属于Light

2.如果一个控件没有申明forceDarkAllowed,那么forceDark是否⽣效取决于全局反色是否开启,如果全局反色开启,那么forceDark生效,否则不生效;

如果一个控件声明了forceDarkAllowed = false,那么无论如何都不会生效;

如果一个控件声明了forceDarkAllowed = true,那么只要开启深色模式,全局反色就会⽣效。             

3.forceDarkAllowed可以在主题xml中申明,也可以在代码中动态设置,动态设置可以具体到某⼀个view,后者会覆盖前者。遵循覆盖原则,即子View的声明会覆盖父View的声明,activity的会覆盖这个应⽤的申明。

适配全局反色的步骤:                                                

1.compileSdkVersion 设为29, 否则会编译失败,targetSdkVersion 貌似没有要求。

2.对于需要使用forceDark的activity,务必让其实现Light主题,Dark主题、DayNight主题都不会⽣效。

3.在activity的主题中申明<item name=”android:forceDarkAllowed”>true</item> 或者 在onCreate中,在setContentView之前,调⽤getWindow().getDecorView().setForceDarkAllowed(allowed)。

最后提供demo,需要的可以参考一下,源码:https://github.com/CQULittleMing/AndroidDemo/tree/master/demo

操作路径:  View – View 下面最后的两个。     

4.FAQ

4.1.全局反色的局限

  • 目前全局反色只是实验性功能,存在很多bug,不可过于依赖,更多可以参考Force Dark目前存在的问题。
  • 不会反色图⽚。
  • WebView/Flutter⽀持不完善所以如果一个⻚面存在Flutter书写、WebView或者亮色的图片,建议还是使⽤深色模式进行适配。

4.2.如何开启和禁用全局反色

设置是否⽀持全局反色有静态和动态两种方法:                                              

静态方式就是对应用或者activity的主题中声明<item name=”android:forceDarkAllowed”>true</item> 或者 <item name=”android:forceDarkAllowed”>false</item>;

动态⽅法是调用View.setForceDarkAllowed(true)或者View.setForceDarkAllowed(false),对DecorView设置可以对整个窗口起作⽤。

5.联系我们

有任何问题可以随时联系我们:

商务合作:liushuo3@xiaomi.com

产品问题:zhangyanan10@xiaomi.com

研发问题:darkmode@xiaomi.com