小米开发平台小米计步器接口适配说明

小米开发平台小米计步器接口适配说明

1.小米计步器简介 

收集手机中传感器的数据,通过机器学习算法判断步数。 

1.1 算法说明 

  • 计步器Sensor 5分钟上报一次数据,后台Service将计步数据记录插入本地数据库,若没有数据上报, 则不插入; 
  • app第一次请求数据会立即响应, 后台Service会立即返回给app最新的记录数据;
  • app在1分钟内多次请求数据, 则只有第一次得到的数据是最新的, 后面的请求结果和第一次相同; 
  • 一条记录只有一种计步模式.。例如, 用户在10分钟内有600步数据, 400步走路, 200步跑步, 则这10分钟会分拆成两条记录, 400步走路和200步跑步;
  • 只传给应用层3种计步模式: 0: 不支持(在不支持计步的手机上不会得到数据), 2: 走路, 3: 跑步。 

2.计步器接入 

2.1 判断本机是否支持计步 

使用miui.util.FeatureParser提供的接口去判断是否支持stepsProvider功能

//示例code
//在项目中新建一个工具类,FeatureParser,通过反射机制来获取miui.util.FeatureParser
public class FeatureParser {
    public static boolean getBoolean(String name, boolean defaultValue) {
        try {
            Class featureParserClass = Class.forName("miui.util.FeatureParser");
            Method method = featureParserClass.getMethod("getBoolean", String.class, boolean.class);
            return (Boolean) method.invoke(null, name, defaultValue);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }
}

//在功能开始之前判断是否支持stepsProvider功能
boolean isSupport= FeatureParser.getBoolean("support_steps_provider",false); 

2.2 App采用ContentProvider的query请求获取计步数据 

接口格式: 

Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
参数必选类型范围说明
uritrueandroid.net.UriUri.parse(“content://” + “com.miui.providers.steps” + “/item”);取固定值
projectionfalseString[]可选列: “_id”, “_begin_time”, “_end_time”, “_mode”,  “_steps”获取指定列, 若为null则获取所有列
selectionfalseString从可选列中选取自定义条件获取满足条件的记录行, 若为null则获取所有行
selectionArgsfalseString[]一般为可选列的值Selection中带?的格式化参数
sortOrderfalseString从可选列中选取列升序(asc)或降序(desc), 可多个并列返回结果的记录行排序方式, 若为null则按id大小排序

2.3 返回结果: 满足查询条件的记录行

记录行的数据结构如下 

public class Step {
    private int mId; // 记录在sqlite的id
    private long mBeginTime; // 计步开始时间
    private long mEndTime; // 计步结束时间
    private int mMode; // 计步模式: 0:不支持模式, 1:静止, 2:走路, 3:跑步, 11:骑车, 12:交通工具 
    private int mSteps; // 总步数
}

3.代码示例

3.1 AndroidManifest.xml中声明权限 

 <uses-permission android:name=”miui.permission.READ_STEPS” />(不声明权限无法读取计步数据)。 

3.2 Query用到的数据结构 

public class Steps {
/* Data Field */
public static final String ID = "_id";
public static final String BEGIN_TIME = "_begin_time";
public static final String END_TIME = "_end_time";
// 0: NOT SUPPORT 1:REST 2:WALKING 3:RUNNING
public static final String MODE = "_mode";
public static final String STEPS = "_steps";
/* Default sort order */
public static final String DEFAULT_SORT_ORDER = "_id asc";
/* Authority */
public static final String AUTHORITY = "com.miui.providers.steps";
/* Content URI */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
}
public static String[] projection = new String[] {
Steps.ID,
Steps.BEGIN_TIME,
Steps.END_TIME,
Steps.MODE,
Steps.STEPS
};

3.3 返回结果的数据结构 

public class Step {
private int id;
private long mBeginTime;
private long mEndTime;
private int mMode;
private int mSteps;
}

3.4 查询方法示例 

public LinkedList<Step> getAllSteps(String selection, String[] args) {
LinkedList<Step> steps = new LinkedList<Step>();
Cursor cursor = resolver.query(Steps.CONTENT_URI, projection, selection, args,
Steps.DEFAULT_SORT_ORDER);
if (cursor.moveToFirst()) {
do {
Step s = new Step(cursor.getInt(0), cursor.getLong(1), cursor.getLong(2),
cursor.getInt(3),
cursor.getInt(4));
steps.add(s);
} while (cursor.moveToNext());
}
return steps;
}

小米开发平台小米妙播适配说明

小米开发平台小米妙播适配说明

一、小米妙播背景与介绍

1.1 安卓原生Media Session介绍

Android 框架定义了两个类(媒体会话和媒体控制器),它们为构建媒体播放器应用提供了一个完善的结构。

媒体会话和媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。

更多信息详见:https://developer.android.com/guide/topics/media-apps/media-apps-overview

1.2 MIUI小米妙播介绍

小米妙播支持基于Media Session的音频播控、基于Wi-Fi的跨设备音乐接力。

用户可以在小米手机和平板的系统控制中心、通知栏/锁屏媒体中心使用该功能。

控制中心内的小米妙播播控,基于安卓原生Media Session能力实现,音频应用适配Media Session即可,众多知名三方应用均已适配。

二、适配小米妙播的好处

2.1 系统全局常驻播控

小米妙播作为控制中心常驻的播控、互联入口,用户可以在手机全局操作音频,是Android厂商中全面媲美苹果 AirPlay2 的系统级功能。

2.2 覆盖更多场景的播放

各音频app的用户活跃场景不再局限于手机,用户用音箱等设备听歌时,也可使用各音频app,通过小米妙播互联播放。

三、适配方式

基本适配方式请参考谷歌官方文档:

https://developer.android.com/guide/topics/media-apps/working-with-a-media-session

https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice

适配要点:

及时更新Meta信息

// 构建MediaSession
MediaSessionCompat mMediaSession;
//构建MediaMetadata,并传入媒体meta信息(歌曲名、专辑名、歌手名、歌曲时长等)
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, getTrackName())
        .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, getAlbumName())
        .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, getArtistName())
        .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration());
MediaMetadataCompat metadata = builder.build();
mMediaSession.setMetadata(metadata);
// 构建并传入歌曲封面
Bitmap result;
// 获取封面BitMap...
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, result);
mMediaSession.setMetadata(builder.build());

及时更新播放状态信息

// 构建PlaybackState,传入播放状态、播放进度等信息。
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(getSourcePlaybackState())
        .setBufferedPosition(getBufferedPosition())
        .setState(PlaybackStateCompat.STATE_PLAYING, position(), getSpeedRatio());
mMediaSession.setPlaybackState(builder.build());

处理播放回调 

// 构建MediaSessionCompat.Callback回调,处理播放、暂停、上一首、下一首、调整播放进度等操作
private class MediaPlaybackSessionCallback extends MediaSessionCompat.Callback {
    @Override
    public void onPlay() {
        play();
    }
    @Override
    public void onPause() {
        pause();
    }
    @Override
    public void onSkipToNext() {
        next();
    }
    @Override
    public void onSkipToPrevious() {
        prev();
    }
    @Override
    public void onFastForward() {
        forward();
    }
    @Override
    public void onRewind() {
        backward();
    }
    @Override
    public void onStop() {
        stop();
    }
    @Override
    public void onSeekTo(long pos) {
        seek(pos);
    }
}
mMediaSession.setCallback(new MediaPlaybackSessionCallback());

小米开发平台MiHaptic适配说明

小米开发平台MiHaptic适配说明

1.MiHaptic简介

MiHaptic是一套振动波形的生成框架,可以通过参数或者HE格式为你的线性马达机器定制出丰富的振动效果。

1.1.能力

MiHaptic支持接入不同的触感波形生成算法以驱动手机上的线性马达,目前支持小米自研算法和RichTap。

1.2.场景及案例

  • QQ音乐节奏实验室

QQ音乐节奏实验室4D振感,开启音乐随振,让听歌更有节奏感。

  • 和平精英高品质振动

和平精英高品质振动使用MiHaptic方案对不同枪械、载具、脚步声、玻璃破碎等场景提供拟真的触感反馈。

2.使用入门

2.1.开发环境

  • 安卓Studio 4.0 以上

2.2.开发准备

2.2.1.集成MiHaptic SDK

  • miui.os

DynamicEffect_0807_2

提供组合PrimitiveEffect的方式。

  • android.os

DynamicEffect_0128

提供HE文件振动方式。

2.2.2.增加振动权限

2.2.3.使用 MiHaptic SDK文档 中的接口。

DynamicEffect_ZH.docx

2.3.使用MiHaptic

  • 组合PrimitiveEffect的方式

DynamicEffect可组合Transient和Continuous类型的效果,如下图所示

DynamicEffect 数据结构实际上是对一组参数的描述,一个DynamicEffect由若干个PrimitiveEffect和若干Parameter所组成。

PrimitiveEffect 分为两种,一种是描述瞬时振动的Transient,例如哒哒哒的清脆效果;另一种为Continuous,这种振动效果的特点是持续时间长,例如嗡嗡嗡的效果。

对于每种PrimitiveEffect,有两个重要的参数,intensity和sharpness,分别描述它们的强度与锐度。

Parameter与PrimitiveEffect的两个重要属性对应,用来控制PrimitiveEffect的强度或者锐度。

Parameter 可以添加到整个DynamicEffect里,那么它可以控制所有时间范围内的PrimitiveEffect。

Parameter 也可以添加在Continuous效果中,用来作渐变。

例如,下图所示的是一个由两个Continuous与一个Transient所组合成的效果:

DynamicEffect effect = DynamicEffect.startCompose(); 
 
DynamicEffect.PrimitiveEffect continuous1 = DynamicEffect.createContinuous(0.5, 1.0, 3.0); 
DynamicEffect.Parameter param1 = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{0.5, 1, 2} , new float[] {0.6 , 0.3, 0.2); 
continuous1.addParameter(1, param1); 
 
DynamicEffect.PrimitiveEffect transient = DynamicEffect.createTransient(0.8, 1.0); 
 
DynamicEffect.PrimitiveEffect continuous2 = DynamicEffect.createContinuous(0.3, 1.0, 6.0); 
DynamicEffect.Parameter param2 = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{1.0, 2.0} , new float[] {0.3, 1.0); 
continuous1.addParameter(2, param2); 
 
effect.addPrimitive(0, continuous1); 
effect.addPrimitive(3.5, transient); 
effect.addPrimitive(4, continuous2); 
 
DynamicEffect.Parameter global = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{0, 8.0} , new float[] {0.2, 1.0); 
 
effect.addParameter(2.5); 

HapticPlayer player = new HapticPlayer(effect);
player.start();

上图描述了一个DynamicEffect以及intensity属性的渐变示意图。DynamicEffect中包含了两个Continuous和一个Transient。

生成波形的强度在0时刻的强度为0.5,它是由第一个Continuous创建时所指定的。 在第0.5S时强度为0.6,该变化是由Continuous被自己的Parameter所控制引起。第2.5S时,Continuous的强度渐变到了0.04,该变化由于Global参数对其产生了影响,对于强度而言,global参数与PrimtiveEffect参数的强度作乘积,因此变为0.2*0.2=0.04 。而后该Continuous的Intensity无自身Parameter影响,但由于globalParameter存在,从2.5到第3S改变到0.05。

在第3.5S时开始播放Transient,强度为初始值0.8与当前时刻的global参数0.3相乘为0.24。(global起始时间为2.5S,因此在3.5S时参数的值渐变为0.3)。

第二个Continusou起始时间为第4S,从第4s-第6S强度递增,因为当前依旧属于global参数的作用时间,第4S强度为=0.3*0.35 = 0.105,后面的计算方式同理。

  • 使用HE文件
{
 "Metadata": {
 "Version": 1, // 版本号,整形
 "Created": "2020-07-08", // 创建时间,String类型
 "Description": "game haptic" // 震动效果描述,String类型
 },
 "Pattern": 
 [
 {
 "Event": {
 "Type": "continuous", // 事件类型: continuous->持续震动。transient->简短震动
 "RelativeTime": 0, // 相对开始时间, 整形, 单位ms
 "Duration": 300, // 持续震动类型参数:持续时间。整形, 单位ms
 "Parameters": {
 "Intensity": 80, // 震动强度, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Frequency": 50, // 震动频率, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Curve": [ // 持续震动类型参数:曲线。实现上保证平滑过渡效果
 {"Time": 0, "Intensity": 0, "Frequency": 25}, // 起始点,必须。time为RelativeTime,Intensity必须取值为0。
 {"Time": 100, "Intensity": 0.7, "Frequency": -30}, 
 {"Time": 200, "Intensity": 0.75, "Frequency": 90},
 {"Time": 300, "Intensity": 0, "Frequency": 50} // 结束点,必须。time为Duration,Intensity必须取值为0。
 ]
 }
 }
 },
 {
 "Event": {
 "Type": "transient", // 事件类型: continuous->持续震动, transient->简短震动
 "RelativeTime": 400, // 相对开始时间, 整形, 单位ms
 "Parameters": {
 "Intensity": 80, // 震动强度, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Frequency": 40 // 震动频率, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 }
 }
 }
 ]
}

HE文件描述如上面的JSON格式所示,可以将描述文件放在项目中,使用DynamicEffect.create(string)接口创建效果。

InputStream is = getResources().openRawResource(R.raw.demo_he);
try {
    int size = is.available();
    byte[] buffer = new byte[size];
    is.read(buffer);
    is.close();
    str = new String(buffer);
} catch (Exception e){}
effect = DynamicEffect.create(str);
player = new HapticPlayer(effect);
player.start();

注:使用HE文件创建出的DynamicEffect不可再用于添加PrimitiveEffect,HE文件当前仅支持16个以内的event。

小米开放平台三方App相机无法对焦适配方案

小米开放平台三方App相机无法对焦适配方案

1.问题现象

打开App扫一扫 -> 画面一直无法聚焦

2.问题分析

对焦失败的原因:第一次AFtrigger被reconfigure冲掉了,导致AF模块状态异常,一直无法对焦。

2.1.App的对焦方式

扫一扫采用auto方式对焦,即先设置AFMode=auto,然后周期性不断下发aftrigger(loop focus)来主动触发每一次对焦。原理如下:

                                                       图1 auto对焦原理图

2.2.configure_stream和reconfigure

  • 什么是configure_stream

configure_stream是Cameraservice的行为。在open camera初始化阶段,Cameraservice会根据app的对分辨率、画幅、stream等需求向底层发送configure_stream的指令。通俗的讲,比如某个camera需要预览和yuv数据,那么在初始化阶段就需要将这些需求通知底层camera,Camera会根据这些需求搭建对应的硬件和软件资源,当底层准备完毕后,就可以开始接收app的request需求,并返回数据。

  • 什么是reconfigure

当Cameraservice检测到app的需求发生变化时,便会触发reconfigure,将新的需求向底层发送configure_stream的指令。例如:一开始app只需要预览,那么Cameraservice就会向底层configure预览流,底层就会搭建好预览流的输出通道,过了几帧后,app又需要yuv了,于是就会执行reconfigure,此时会将上次configure的资源全部清除,并重新建立预览+yuv的通道。

2.3.微博的流程

以微博扫一扫为例分析不对焦的原因,不同app行为可能有所不同。

                                                   图2 微博扫一扫对焦流程图

一般对于扫码的场景,app会请求预览流(用于显示)和yuv流(用于处理、识别二维码),但由于yuv并不需要马上使用,因此,预览和yuv的请求可能会存在时间差。

  • 为什么旧的机型这些app没有问题,在新平台上会有问题

根据以上分析可知,不对焦有两个条件,一是触发了reconfigure,二是reconfigure冲掉了第一次AFtrigger。因此对于很多app,reconfigure的问题一直存在,但reconfigure的时机具有随机性,如果第二次configure发生在第一次AFtrigger之前,就不会冲掉trigger,也就不会引起问题。

3.修复方案

通过与微博研发人员合作验证以下两种方案均可修复该问题:

  • 方案1:将第一次触发AF的时机移到get yuv之后;
  • 方案2:同时请求预览和yuv,避免时间差导致的reconfigure。

建议方案2,可以根源上解决问题。如下是微博的修复方案(不同app可能情况不一样,仅供参考):在startpreview()前执行setPreviewCallback(YUV)。

说明:在开启预览时app会调用startpreview()接口,如果在此之前调用setPreview Callback(),提前设置yuv的callback,这样便可以在预览时同时请求yuv流。否则,startpreview()只会请求预览流,当执行到setPreviewCallback(YUV),会触发reconfigure请求yuv。

小米开发平台MIUI无极音量适配说明

小米开发平台MIUI无极音量适配说明

1.MIUI无极音量综述

MIUI为了提升用户的音频体验,将媒体音音量范围从[0,15]修改为[0,150]。用户在滑动音量条时,每一次轻微的滑动都能改变音量,细微的音量调节让用户能找到更加合适的音频响度。

2.App适配建议

2.1.若App无调节音量功能或不拦截音量上下按键,此时无需单独适配

2.2.若App通过onTouchEvent等接口拦截volume up或down事件

  • 拦截事件后,做其他的事件处理,例如按键拍照等,此时App无需单独适配;
  • 拦截事件后,通过AudioManager的adjustSuggestedStreamVolume接口调节音量,此时MIUI 系统会内部计算每次按键应该调节多少index,此时App无需单独适配;
  • 拦截事件后,通过AudioManager的setStreamVolume接口,此时接口要传递具体的index值,此时App需适配。

App应内部自己定义步长step,进而算出index。

若支持无极音量step = 10,若不支持step = 1;原因是为了兼容不支持无极音量的机型,统一定义:按键15次将音量调满。

统一计算公式  step = MaxVolume /15;

MaxVolume = AudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)。

总结:简单来说,应用App如果有自己的音量调节逻辑,应该内部定义步长step。通过以上方式可兼容任意机型。       

3.未适配的应用具体表现

  • 具体表现为:按键150次,才能调满音量,调节缓慢。

4.联系我们

MIUI 期待您的适配,若有任何问题,请及时与我们沟通:liuxiaoyu7@xiaomi.com。

小米开放平台相机各场景下3A操作适配说明

小米开放平台相机各场景下3A操作适配说明

1.Preview

在预览时AF可以由App控制触发对焦,也可以底层自动对焦,推荐使用底层自动对焦。

1.1.App手动对焦设置

  • CONTROL_AF_MODE_AUTO并不是AF自动模式,根据Google定义,是由App进行手动触发。App控制触发对焦逻辑是设置AF为Auto,然后App主动触发对焦。
  • App使用CONTROL_AF_TRIGGER触发对焦。CONTROL_AF_TRIGGER使用建议,CANCEL – START – IDLE;App何时触发对焦由App决定,比方App检测到加速度计和陀螺仪变化达到一定的阈值或者app为扫码软件,检测扫码一直无法解析扫码结果等(因为出现这种场景时物距发生了变化或者焦点模糊),具体场景具体调整。对于扫码等APP还可以设置AF测量区域,CONTROL_AF_REGIONS,尽量使用中心区域作为对焦点。

1.2.AF自动对焦设置

可以先检测AF有效模式,如果支持CONTINUOUS_PICTURE,设置即可,具体如下:

mCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES),CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE))。

CONTROL_AF_MODE设置为CONTROL_AF_MODE_CONTINUOUS_PICTURE。

2.Video

  • 可以先检测AF有效模式,如果支持CONTINUOUS_VIDEO,设置即可,具体如下:
mCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES),CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO))。

CONTROL_AF_MODE设置为CONTROL_AF_MODE_CONTINUOUS_VIDEO。

  • 注释

CONTROL_AF_MODE_CONTINUOUS_PICTURE和CONTROL_AF_MODE_CONTINUOUS_VIDEO的区别是前者在AF时会快速收敛,后者在AF时慢速收敛,因为录制视频时为了防止图像抖动,收敛过程需要慢一点,共同点是二者都为自动对焦。

3.Touch

Touch是为了在预览时对特定的区域进行3A权重提高,以便于得到预期的图像。当touch预览窗口某区域时,通用的操作流程是设置CONTROL_AF_REGIONS,CONTROL_AF_TRIGGER的流程还是CANCEL – START – IDLE。

4.Flash

Flash的实用场景通常在Touch,拍照和录像情况下使用,flash有两种操作模式,手动模式和自动模式,具体操作如下:

4.1.手动模式,即Torch模式

当需要打开flash时,设置CONTROL_AF_MODE为OFF/AUTO模式,同时设置FLASH_MODE为torch即可。

4.2.自动模式

自动模式是flash交给底层AE算法控制,具体如下两种常用模式:

ON_AUTO_FLASH:设置AF_Mode为此模式表示flash由AE根据当前亮度进行打开,关闭。

ON_ALWAYS_FLASH:设置AF_Mode为此模式表示flash每次操作都会打开。

5.拍照

拍照操作流程通常为触发3A,等待3A收敛,获取到3A收敛结果,进行拍照请求。

5.1.触发3A操作

CONTROL_AE_MODE根据是否需要闪光灯设置为ON_AUTO_FLASH,ON_ALWAYS_FLASH,ON。

CONTROL_AE_PRECAPTURE_TRIGGER设置建议 CANCEL-START-IDLE。

CONTROL_AF_MODE设置为AUTO,CONTINUOUS_VIDEO都可以。

CONTROL_AF_TRIGGER设置建议CANCEL-START-IDLE。

5.2.等待3A状态收敛

根据CaptureResult的metadata获取3A状态,如果3A都收敛了即可请求拍照

result.get(CaptureResult.CONTROL_AF_STATE);   //AF_State

result.get(CaptureResult.CONTROL_AE_STATE);   //AE_State

result.get(CaptureResult.CONTROL_AWB_STATE); //AEB_State 

5.3进行拍照请求

CONTROL_AE_MODE根据是否需要闪光灯设置为ON_AUTO_FLASH,ON_ALWAYS_FLASH,ON。

CONTROL_AF_MODE设置为AUTO,CONTINUOUS_VIDEO都可以。

最终下发拍照请求:

mCaptureSession.capture(request, mCaptureCallback, mBackgroundHandler); 

6.参考

https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics
https://developer.android.com/reference/android/hardware/camera2/CaptureRequest
https://github.com/googlearchive/android-Camera2Raw

小米开放平台双WiFi适配说明

小米开放平台双WiFi适配说明

1.双WIFI简介

双WiFi顾名思义提供了手机同时连接两个WiFi同时上网的能力,一般手机只能同时连接AP的一个WiFi频段获取 WiFi信号,而支持双WiFi技术的手机可以同时连接AP的两个频段(2.4Ghz或5GHz)获取WiFi信号(可以连接一个AP的两个频段,也可以连接两个AP的不同频段)。

1.1双WIFI的优点

  • 提升网络稳定性:如果一路链路因为无线参数波动出现网络波动,可以通过另一链路及时将数据包发送出去;
  • 提升网络速度:双WiFi提供了两条链路同时加载数据的能力,提升网络速度。

1.2目前小米支持双WiFi的已发布机型

小米10、小米10 Pro、Redmi K30 5G、Redmi K30 Pro。

1.3如何进入双WIFI UI操作界面

双WiFi UI操作界面可从手机如下操作路径:“设置->WLAN->WLAN助理->双WLAN加速”进入。该界面称为副WiFi界面,副WiFi界面的打开和连接与主WiFi除了频段差异外并无区别(当主WiFi连接5Ghz时,副WiFi只能连接2.4Ghz,反之主WiFi连接2.4Ghz的时候,副WiFi只能连接5Ghz)。如下图所示:

2.双WIFI接入

2.1双WIFI接入需求

  • 判断当前机型是否支持双WiFi;
  • 判断当前双WiFi是否已连接;
  • 若未连接,作双WiFi的连接引导;
  • 接入双WiFi 网络;
  • 副WiFi相关的api。

2.2判断当前机型是否支持双WiFi

private boolean supportDualWifi(Context context) {
    String cloudvalue = Settings.System.getString(context.getContentResolver(), "cloud_slave_wifi_support");
    if ("off".equals(cloudvalue)) {
        return false;
    }
    boolean support;
    try {
        support = context.getResources().getBoolean(
                context.getResources().getIdentifier("config_slave_wifi_support", "bool", "android.miui"));
    } catch (Exception exception) {
        return false;
    }
    return support;
}

2.3判断当前双WIFI是否已连接

WiFi的api都属于系统api,有严格的权限校验,第三方应用无法直接调用。miui提供了workround判断方法,当副WiFi连接上了之后,如下参数会设置为对应的副WiFi的ssid,断开时则会清空,可依此判断当前副WiFi是否已连接。

 private boolean isSlaveWifiConnected(Context context) {
        String slaveWifiSsid =  getDualWifiSsid(context);
        if (slaveWifiSsid == null || slaveWifiSsid.isEmpty()){
            return false;
        } else {
            return true;
        }
    }


    private String getDualWifiSsid(Context context){
        return Settings.System.getString(context.getContentResolver(), "slave_wifi_ssid");
    }

2.4 若副WiFi未连接,则引导用户至副WiFi界面进行连接

判断当前副WiFi是否已连接,若未连接跳转到系统副WiFi界面引导用户连接,返回到当前界面则再次校验,校验成功后进行副WiFi网络操作。

 if (!isSlaveWifiConnected(getApplicationContext())){
            //TODO Provide Dual Wifi Ui guidance and reCheck
            Intent intent = new Intent("android.settings.DUAL_WIFI.WIFI_SETTINGS");
            startActivity(intent);
            return;
        }

2.5接入双WiFi网络part 1/4

反射获取副WiFi的transport

private int getSlaveTransportType() {
        int slaveTransportType = -1;
        try {
            Class networkCapabilitiesClass = Class.forName("android.net.NetworkCapabilities");
            Field field = networkCapabilitiesClass.getField("TRANSPORT_SLAVE_WIFI");
            slaveTransportType = (int) field.get(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return slaveTransportType;
    }

2.6接入双WiFi网络part 2/4

初始化副WiFi对应的NetworkRequest

 int slaveTransportType = getSlaveTransportType();
        if (slaveTransportType < 0) {
            return;
        }
        NetworkRequest nr = getNetworkRequestForType(slaveTransportType);


    private NetworkRequest getNetworkRequestForType(int type) {
        return new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addTransportType(type).build();
    }

2.7接入双WiFi网络part 3/4

初始化副WiFi所需的NetworkCallback

SlaveCallback slaveNetworkCallback = new SlaveCallback();
    private class SlaveCallback extends ConnectivityManager.NetworkCallback {
        @Override
        public void onAvailable(Network network) {
            Log.d(TAG, "Network available: " + network.toString());
            Socket skS;
            try {
                InetAddress[] inetAddress = network.getAllByName("www.mi.com");
                skS = network.getSocketFactory().createSocket("www.mi.com", 80);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            Log.d(TAG, "skS = " + skS.toString());
        }
    }

2.8接入双WiFi网络part 4/4

请求副WiFi网络

ConnectivityManager cm = (ConnectivityManager) getApplicationContext().
        getSystemService(Context.CONNECTIVITY_SERVICE);
cm.requestNetwork(nr, slaveNetworkCallback);

PS: manifest 添加 permission
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

2.9 api补充

副WiFi提供的api汇总,详见“小米双wifi接口列表v1.0”

3.补充说明

3.1系统支持

当打开“双WLAN加速”并且辅助WLAN连接时,系统可以自动分配连接到不同的WiFi链路。 应用开发者可以跟小米确认开发的APP是否已经被支持。 当前已支持 :

  • 直播类:斗鱼,虎牙、企鹅电竞、抖音、快手、微视;           
  • 购物类:淘宝、天猫、京东、毒、唯品会、苏宁易购、阿里巴巴、有品、小米商城、拼多多、河马生鲜、 PP视频、咸鱼 ;        
  • 视频类:优酷、腾讯视频、哔哩哔哩、爱奇艺、影视大全、西瓜视频、芒果Tv,人人视频、搜狐视频、 电视家;       
  • 浏览器:UC浏览器、QQ浏览器、百度浏览器、搜狗、猎豹,搜狗搜索;        
  • 音乐类:网易云音乐、酷我、央视影音、咪咕音乐、酷狗、qq音乐 ;      
  • 休闲app、大准点评、知乎、最右、虎扑;      
  • 新闻类:今日头条、腾讯新闻、一点资讯、想看、新浪新闻;  
  • 其他:迅雷、58、汽车之家、懂车帝、乐视视频、链家、瓜子二手车、安居客。 

3.2应用适配 

推荐开发者参照本接入指南中的“双WiFi接入”章节和附带的demo对双WiFi进行应用的适配。双WiFi连接的情况下,系统可以在两个网卡之间对socket进行分配,对于多socket的情况,是有加速效果的。   但是这种有一个弊端,系统不知道APP的socket想被放到哪个网卡,是随机控制的,所以就可能出现跨运营商访问的情况。而应用自己做适配,应用可以自己控制数据传输到自己想传输的网卡上,这样就可以避免跨运营商访问的情况,更好地达到提升网络稳定性和数据加载速度的目的。

4.附件

小米手机设备全局拖拽功能技术适配说明

小米手机设备全局拖拽功能技术适配说明

1.简介

安卓拖拽分享功能提供了一种跨窗口传递数据的功能,文本、图像或任何可以用uri表示的数据都可以通过拖拽从一个窗口传递到另一个窗口。

可参考谷歌官方文档:Drag and drop | Android Developers

app适配拖拽功能主要分为拖出适配拖入适配,本文将分别简介其适配方法。

2.拖出适配

app对任意view调用startDragAndDrop方法即可实现拖出。本章分别对拖出文字、拖出图片、拖出任意文件进行演示。

2.1.拖出文字

使用一个TextView来拖出文字:

// 拖出文字示例
findViewById<TextView>(R.id.drag_text_view).setOnLongClickListener { view -> // 设置长按回调
    val textView = view as TextView
    val clipData = ClipData.newPlainText("label", textView.text) // 构建存放文本的ClipData
    // 调用view.startDragAndDrop方法开始拖拽
    textView.startDragAndDrop(clipData, // 传入clipData
        View.DragShadowBuilder(textView), // 使用textView的draw方法绘制拖拽的图像
        null, // 传入一个本地数据对象
        View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    true
}

我们只需要使用ClipData.newPlainText构建一个保存文本的clipData,再调用view.startDragAndDrop方法,将这个clipData作为参数传入,即可实现文字的拖出。

安卓的EditText本身就实现了文字的拖出和拖入,不需要额外适配。

2.2.拖出图片

使用一个ImageView来拖出图片:

// 拖出图片示例
findViewById<ImageView>(R.id.drag_image_view).setOnLongClickListener { imageView -> // 设置长按回调
    val imageUri = getFileUri(R.mipmap.drag_image, "drag_image.png") // 通过fileProvider生成图像文件uri
    val clipData = ClipData("label", arrayOf("image/png"), ClipData.Item(imageUri)) // 使用imageUri构建ClipData
    // 调用view.startDragAndDrop方法开始拖拽
    imageView.startDragAndDrop(clipData, // 传入clipData
        View.DragShadowBuilder(imageView), // 使用imageView的draw方法绘制拖拽的图像
        null, // 传入一个本地数据对象
        View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    true // 返回true表示长按事件被处理了
}

其中getFileUri方法将安卓资源中的一张图片作为文件保存到本地,再使用FileProvider得到图片文件的uri,如果对具体实现有兴趣可以阅读源码。最终我们是将uri放入ClipData中,再调用view.startDragAndDrop方法,将这个clipData作为参数传入,即可实现图片的拖出。

2.3.拖出任意文件

任意文件和图片一样,使用一个uri来表示,因此拖出任意文件可以使用和拖出图片类似的方法实现。

使用一个Button来选择任意文件,然后使用一个TextView来显示文件uri并实现文件的拖出:

// 拖出任意文件示例
findViewById<Button>(R.id.choose_file_button).setOnClickListener { chooseFile() } // 选择文件按钮
mDragFileView = findViewById(R.id.drag_file_view)
mDragFileView.setOnLongClickListener { // 设置长按回调
    if (mFileUri != null) { // 选择的文件保存在mFileUri,如果其不为null表示已经选择了一个文件
        val clipData = ClipData.newRawUri("label", mFileUri) // 使用mFileUri构建ClipData
        // 调用view.startDragAndDrop方法开始拖拽
        mDragFileView.startDragAndDrop(
            clipData, // 传入clipData
            View.DragShadowBuilder(mDragFileView), // 使用imageView的draw方法绘制拖拽的图像
            null, // 传入一个本地数据对象
            View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    }
    true // 返回true表示长按事件被处理了
}

3.拖入适配

app对任意view注册OnDragListener监听器即可实现拖入处理。本章分别对拖入文字、拖入图片进行演示。

3.1.拖入文字

使用一个TextView来拖入文字:

// 拖入文字示例
findViewById<TextView>(R.id.drop_text_view).setOnDragListener { view, event -> // 设置拖拽监听器
    val textView = view as TextView
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DRAG_STARTED -> {
            val hasText = event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) // 查找是否有文字类型的数据
            if (!hasText) { // 没有文字类型的数据
                return@setOnDragListener false // 返回false代表这次拖拽不再继续接收拖拽事件
            }
        }
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            textView.text = event.clipData.getItemAt(0).text // 将文字设到textView上显示
        }
    }
    true // 返回true代表拖拽事件被处理了
}

我们只需要通过view.setOnDragListener方法注册一个监听器,并在监听器里面处理拖拽事件即可实现拖入。其中在ACTION_DRAG_STARTED事件中对数据类型进行判断,如果不是我们想要的数据类型就返回false即可不再接收本次拖拽事件;最后在ACTION_DROP事件中获取ClipData数据并进行相应的处理。

3.2.拖入图片

使用一个ImageView来拖入文字:

// 拖入图片示例
findViewById<ImageView>(R.id.drop_image_view).setOnDragListener { view, event -> // 设置拖拽监听器
    val imageView = view as ImageView
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DRAG_STARTED -> {
            val mimeTypes = event.clipDescription.filterMimeTypes("image/*") // 查找是否有图像类型的数据
            if (mimeTypes == null) { // 没有图像类型的数据
                return@setOnDragListener false // 返回false代表这次拖拽不再继续接收拖拽事件
            }
        }
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            requestDragAndDropPermissions(event) // 申请读取uri的权限
            imageView.setImageURI(event.clipData.getItemAt(0).uri) // 将图像uri设到imageView上显示
        }
    }
    true // 返回true代表拖拽事件被处理了
}

与拖入文字不同的地方在于,图片是一个用uri表示的文件,要访问这个uri之前必须调用Activity.requestDragAndDropPermissions方法申请权限。

3.3.拖入任意文件

类似于拖入图片,想要拖入任意文件只需要对拖过来的任意uri进行处理即可,示例代码如下:

// 拖入任意文件示例
view.setOnDragListener { view, event -> // 设置拖拽监听器
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            requestDragAndDropPermissions(event) // 申请读取uri的权限
            // 处理uri
        }
    }
    true // 返回true代表拖拽事件被处理了
}

3.4.判断拖入的数据类型

拖入方通常有2种方式判断拖入数据类型。

方法1:可以根据event.clipDescription中的MIMETYPE来判断数据类型

MIMETYPE本身是一个字符串,谷歌对其在ClipDescription.java中有一些预定义:

public class ClipDescription implements Parcelable {
    /**
     * The MIME type for a clip holding plain text.
     */
    public static final String MIMETYPE_TEXT_PLAIN = "text/plain";

    /**
     * The MIME type for a clip holding HTML text.
     */
    public static final String MIMETYPE_TEXT_HTML = "text/html";

    /**
     * The MIME type for a clip holding one or more URIs.  This should be
     * used for URIs that are meaningful to a user (such as an http: URI).
     * It should <em>not</em> be used for a content: URI that references some
     * other piece of data; in that case the MIME type should be the type
     * of the referenced data.
     */
    public static final String MIMETYPE_TEXT_URILIST = "text/uri-list";

    /**
     * The MIME type for a clip holding an Intent.
     */
    public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent";

    /**
     * The MIME type for an activity. The ClipData must include intents with required extras
     * {@link #EXTRA_PENDING_INTENT} and {@link Intent#EXTRA_USER}, and an optional
     * {@link #EXTRA_ACTIVITY_OPTIONS}.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_ACTIVITY = "application/vnd.android.activity";

    /**
     * The MIME type for a shortcut. The ClipData must include intents with required extras
     * {@link Intent#EXTRA_SHORTCUT_ID}, {@link Intent#EXTRA_PACKAGE_NAME} and
     * {@link Intent#EXTRA_USER}, and an optional {@link #EXTRA_ACTIVITY_OPTIONS}.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_SHORTCUT = "application/vnd.android.shortcut";

    /**
     * The MIME type for a task. The ClipData must include an intent with a required extra
     * {@link Intent#EXTRA_TASK_ID} of the task to launch.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_TASK = "application/vnd.android.task";

    /**
     * The MIME type for data whose type is otherwise unknown.
     * <p>
     * Per RFC 2046, the "application" media type is to be used for discrete
     * data which do not fit in any of the other categories, and the
     * "octet-stream" subtype is used to indicate that a body contains arbitrary
     * binary data.
     */
    public static final String MIMETYPE_UNKNOWN = "application/octet-stream";
    ......
}

这里面只定义了部分数据,还有其它数据需要app自己定义,目前没有一个准确的规范,通常来说如果是格式为jpg的图像数据则MIMETYPE为”image/jpg”。所以MIMETYPE可以用来初步判定数据类型,但是不完全准确,毕竟MIMETYPE是由拖出方设定的值。

方法2:可以对传入的uri来判断数据类型

我们可以使用ContentResolver的getType方法获取uri的MIMETYPE,然后可以通过MimeTypeMap的getExtensionFromMimeType方法获取文件后缀名。

val mimeType = contentResolver.getType(uri) // 获取MIMETYPE
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) // 获取文件后缀名

通过文件后缀名我们就可以准确判断数据类型。

小米手机设备全局自由窗口适配说明

小米手机设备全局自由窗口适配说明

1.自由窗口简介

  • 小窗
  • 迷你小窗

2.为什么要做“自由窗口”

  • 在多任务处理场景下,小窗意在解决临时使用某个应用的场景,例如:在使用游戏应用时,不便离开,但想发消息给朋友,此时就可以借助小窗打开第二应用;
  • 迷你小窗意在解决应用临时挂机的场景,例如:等待网约车、等待游戏更新、观看直播等。

3.如何使用“自由窗口”

4.自由窗口技术适配指南

  • MIUI的小窗是基于Android的多窗口Freeform方案实现的。
  • 小窗目前主要问题是应用兼容性导致的一系列问题,内容显示不全、Touch事件不响应等等,其实根本原因是应用没有很好的支持、适配多窗口、多分辨率,如下是小窗适配的一些参考性适配指南。

4.1 google多窗口开发适配指南

多窗口适配支持文档:

https://developer.android.com/guide/topics/ui/multi-window#dnd

支持不同屏幕分辨率的开发技巧文档:

https://developer.android.google.cn/training/multiscreen/screensizes

4.2 应用声明是否支持自由窗口

如果您的应用以 API 级别 24 或更高级别为目标平台,那么您可以配置该应用的 activity 是否支持以及如何支持多窗口显示,自由窗口也是google多窗口的一种,所以goolge多窗口适配方案同样适用,可以在清单的<activity><application>元素中设置此属性,以启用或停用多窗口显示:

android:resizeableActivity=["true" | "false"]

如果将此属性设置为 true,则activity 能以分屏和自由窗口模式启动。如果将此属性设置为 false,则 activity 不支持多窗口模式。如果此值为 false,并且用户尝试在多窗口模式下启动 activity,则 activity 会全屏显示。

如果您的应用以 API 级别 24 为目标平台,但您未指定此属性的值,则其值默认设为 true。

4.3 判断应用进入退出自由窗口模式

public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
}

每当 Activity 进入或退出多窗口模式时,系统都会调用此方法。当 Activity 进入多窗口模式时,系统会向该方法isInMultiWindowMode传递 true 值,退出多窗口模式时则传递 false 值。

可以通过newConfig中的mWindowingMode窗口模式来判断是否进入退出自由窗口,其中自由窗口的mWindowingMode为freeform,全屏的mWindowingMode为fullscreen,其他窗口模式可打印newConfig来查看,其中mWindowingMode是非public的,可以通过newConfig.toString().contains(“mWindowingMode=freeform”)间接获取。

4.4 获取自由窗口的Task大小

public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
}
public void onConfigurationChanged(Configuration newConfig) {
}

可以通过newConfig 对象中的mBounds以及sw获取自由窗口的窗口大小,应用可以通过窗口的大小进行UI布局适配。

4.5 小窗上操作杆区域遮挡应用内容适配方案

  • 应用全屏沉浸模式

app的根布局加上 android:fitsSystemWindows=”true”

应用全屏非沉浸模式

不需要处理

4.6 焦点窗口适配

迷你小窗和小窗都有可能成为不是焦点窗口(Focus Window),小窗、迷你窗和底部的全屏应用是可以随意进行焦点切换的,操作哪个窗口哪个窗口就是焦点窗口,所以就要保证应用在没有焦点的前提下也是可以正常运行的,特别是视频,游戏类app,比如说某款游戏在失去焦点时会停止绘制,造成游戏界面黑屏等问题。焦点窗口进行切换时系统会回调Activity.onWindowFocusChanged(boolean hasFocus)方法。

4.7 Stop Playback

问题描述:

在切换到Multi Window状态下焦点发生变化/Switch/Resize时视频停止播放。

修改建议:

播放视频的Activity不要在其onPause()中暂停视频,建议在onStop()中处理暂停,在onStart()中播放。

从Android P开始,应用可以增加支持Multi-resume属性,支持以后,在多窗口中的应用都会处于Resume状态,而非pause状态。

4.8 小窗上下杆子的背景色

应用可以通过设置状态栏,导航航栏的颜色调整上下杆子的背景色,但是不是绝对的,

横屏游戏等特殊场景上下杆子的背景色系统侧做了处理,是固定不变的。

4.9 适配Activity大小切换时不重启, 正确处理Configuration变化

强烈建议应用适配Activity窗口大小切换时不重启,遵循google规范,在android:configChanges 属性增加 screenSize|screenLayout|orientation|smallestScreenSize,并在Activity的onConfigurationChanged回调中更新宽高刷新各个子布局。

4.10 Display APIs

  • 获取屏幕大小

R版本之前:Display.getRealSize()、Display.getRealMetrics()。

R版本之后:WindowManager.getMaximumWindowMetrics() 。

  • 获取当前窗口大小

R版本之前: Display.getSize() 。

R版本之后:WindowManager.getCurrentWindowMetrics() 。

4.11 注意事项

所有的Configuration对象要用接口入参newConfig对象做相关的处理逻辑,不要通过context.getResources().getConfiguration()等方式去获取,可能存在Configuration内容还没有被系统更新的情况。

小米分屏功能适配说明

小米分屏功能适配说明

1. 前言

从 7.0 开始,Android 加入了新的特性 Multi-Window,以支持同时显示多个应用,根据不同场景及设备,细分为以下三种:

  • Split-Screen Mode,即分屏,用于手机和平板
  • Picture-in-Picture Mode,即画中画,用于电视端(Android N)及移动端(Android O)
  • Freeform Mode,自由模式,用于在更大尺寸的设备上自由缩放应用窗口

可以看到,分屏是 Multi-Window Mode 的其中一种,用于手机等移动设备。MIUI 从 MIUI 9 开始支持分屏,基于 Android 7.0 优化,因此开发者不用针对 MIUI 做重复的适配工作。

2. MIUI 分屏的表现

  • 默认将窗口一分二,支持调整窗口大小。
  • 支持横屏
  • 支持主应用始终在前台
  • 对于折叠屏设备,在屏幕展开状态下,分屏模式为左右分屏,而且宽度支持多档位调节

3. 分屏对开发者的好处是

如前所述,Android 7.0 的分屏允许某个应用始终可见,MIUI 会在这个基础上做更多的交互优化,以符合用户的预期。在这个背景下,分屏对于开发者来说有以下好处:

  • 可能提升使用时长:由于应用始终可见,主流程不会被打断,有助于提升使用时长。
  • 可能提升日均使用次数:使用分屏后,消费长内容(如长视频)的成本变低了,用户不用想着留一整段时间,也有助于提升使用次数。

随着 Android 7.0 的设备越来越多,支持分屏带来的好处将会被不断放大。加之 Android 有标准的分屏接口,大大减少了开发者的适配工作量。

4. 如何支持分屏

支持分屏的方式非常简单,只需要声明一个属性。从 7.0 开始,Android 新增了一个 Activity 属性: resizeableActivity ,以声明该 Activity 是否支持多窗口显示。

android:resizeableActivity=["true" | "false"]

如果这个属性设为 true ,Activity 就可以在分屏模式下显示;设为 false ,Activity 则不会在分屏模式下显示,而是会占满整个屏幕。因此开发者可以根据具体场景,仅让部分 Activity 支持分屏。

若开发者没有为 Activity 声明该属性,Android 会根据应用的 targetSDKVersion 及 Activity 的 screenOrientation 属性来综合判断是否可以在分屏显示。关于判断的详细逻辑,可以参考官方文档 Configuring Your App for Multi-Window Mode 或者这篇更详细的博客 Android N7.0多窗口适配开发指导 。

5. 如何进一步优化分屏模式

配置 resizeableActivity 的属性,是适配分屏的最简单方式,但如果想要提供更好的使用体验,需要开发者做一定优化工作。下面是我们了解到的一些案例(测试机型:Nexus 6 Andriod 7.1),开发者可以根据自己的业务需求,做不同程度的优化。

5.1. 减少不可滑动的页面/控件

在分屏过程中,屏幕高度只有原来的一半,如果有太多的控件不响应滑动事件,那么用户将无法上下滚动应用页面,甚至无法进行下一步操作。这类页面,最常见于Splash screen、登录注册页、音乐播放页、大图区域、弹窗等。

由于用户可以自由调整分屏的窗口比例,因此开发者只要减少了不可滑动的控件,分屏的可用性就会大大提高,是性价比非常高的优化方案。

5.2. 尽可能使用相对位置,以兼容多种窗口尺寸

分屏时,屏幕的高度和宽度会发生变化,因此在书写控件布局时,尽量使用相对位置,以避免窗口大小改变时,控件无法显示或显示不全。这也是一种性价比很高的优化方案,可以保证用户在分屏时能正常使用应用。

5.3. 注意多窗口下 Activity 的生命周期

视频、直播等类别的应用需要特别关注这一点。Android 7.0 在分屏时会同时运行两个应用,其中用户最后操作的那个应用会处于 Resumed 状态,另一个则会处于 Paused 状态。

这会带来一些问题,以视频应用为例,如果开发者在 onPaused 中处理视频的 「暂停/播放」,那在分屏时,就会因为用户操作另一个应用,导致视频停止播放。因此我们建议开发者在 onStart/onStop 中处理视频的「暂停/播放」,或者特殊处理分屏时的 Paused 状态。详见官方文档 Multi-Window Lifecycle

5.4. 处理 Configuration Changes

由于分屏过程中,允许用户调整窗口的大小,这就会导致 Configuration 的改变。Android 的默认处理方式是 relaunch 整个 Activity,从而出现页面闪一下的问题。如果想避免闪一下的问题,建议开发者自己处理 Configuration 的变化。

5.5. 给内容更多空间

分屏后,屏幕空间变得非常小了,为了给内容让出更多空间,应尽量减少常驻控件。一种解决办法是在浏览内容时,隐藏底部tab等常驻控件,用户回滚时再出现,以展示更多的内容。

5.6. 为分屏定制新的布局(动态布局)

动态布局指根据当前的窗口大小,重新调整页面的布局。这是一项锦上添花的优化项,开发者可以酌情考虑是否添加此优化。

6.FAQ

6.1. MIUI 分屏支持哪些设备

搭载 Android 7.0 或以上的 MIUI 手机及平板设备均支持分屏。MIUI 也将尝试将分屏移植至 Android 6.0。因此将有数千万的新老设备支持分屏。

6.2. 如何调试

MIUI 的分屏方案完全兼容 Android 7.0,因此可以在任意运行 Android 7.x 的设备上调试,无需为 MIUI 作特别处理。同时,以上提到的案例均能在任意 Android 7.x 设备复现。调试过程中,建议开启以下设置项:「开发者选项 > 强制将活动设为可调整大小」,然后重启手机,之后系统就会强制应用进入分屏模式,以方便开发者观察应用在分屏时的表现。

6.3. 分屏是个伪需求吗,用户为什么需要分屏

分屏不仅不是伪需求,而且会是一个大众需求。我们认为分屏最主流的使用场景是:一边看视频,一边做其它事情。用手机看视频,已经成为用户的主流场景,各大视频应用的日活人数、使用次数、使用时长都可以佐证这个观点。但使用手机看视频有很多痛点,比如会被 IM 消息打断、切换至后台视频会暂停等。这些痛点都可以通过分屏较好地解决,这也是分屏能成为大众需求的潜力。

但不可否认的是,分屏也会带来很多可用性问题,尽管系统已经做了很多优化工作,仍然需要各大开发者做进一步的优化,提高分屏模式的可用性。因此非常希望各位开发者能够支持该功能,为广大用户提供更好的使用体验。再次感谢所有开发者的支持与付出!

6.4. 参考资料

小米应用商店MIUI小部件提交审核与上传操作指南

小米应用商店MIUI小部件提交审核与上传操作指南

“MIUI小部件”非安卓原生小部件,若仅适配安卓原生小部件则无需通过MIUI审核,可自行按照安卓规范适配。安装适配安卓原生小部件的APP版本后,可按照下图所示步骤找到原生小部件入口进行添加:

适配“MIUI小部件”请先阅读MIUI产品设计规范MIUI小部件技术规范与系统能力说明,按照规范进行产品设计和开发。通过审核的小部件可在小部件中心上线展示,提交审核与上传操作步骤见本文档正文部分。

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

开发者微信:

开发者联系电话:

应用包名:

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

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

1.MIUI小部件创建流程

本文档介绍了小部件创建与提交的全流程,您可在了解文档内容后进行小部件的创建与提交。

同时,因为只有符合审核规范的小部件才会被【小部件中心】收录,我们建议您在上传您的小部件之前,先阅读我们的MIUI小部件审核规范

1.1.小部件上传入口

因MIUI小部件是包含在APP中的,所以需要先确保您包含小部件的APP已经在应用商店进行上传/更新,并通过应用商店的审核。

您可直接进入小部件开放平台。https://widget.xiaomi.com/

当您的APP在应用商店开发者平台上传/更新成功后,可以在提交审核成功页面进入小部件开放平台,上传/更新您该应用的小部件。

注:如果您是首次上传有小部件版本的应用,请先邮件联系miui-widget@xiaomi.com,我们进行相应的操作后,才可在应用商店开发者平台看到小部件上传入口。

在应用商店开发者平台,您APP的管理页面内,可以进入小部件开放平台,上传/更新您该应用的小部件。

注:如果您是首次上传有小部件版本的应用,请先邮件联系miui-widget@xiaomi.com,我们进行相应的操作后,才可在应用商店开发者平台看到小部件上传入口。

当您登录开发者账号进入小部件开放平台首页时,您需要阅读并同意《小部件开放平台协议》。

1.2.上传小部件信息

登录开发者账号(企业开发者账号注册流程),进入小部件开放平台首页,点击“我的应用”或者鼠标向下滑动至“我的应用”,我们将会展示您账号下所有应用列表,选择需要上传小部件所属的应用,点击“管理小部件”,即可开始该应用小部件的首次创建。

当您点击“管理小部件”时,我们将从小米应用商店拉取您最新版本的APP,并自动解析该版本APP,解析后将在“上传页面”展示该版本APP中的所有小部件,每一张小部件的名称、小部件ID、小部件尺寸和小部件的状态都会按照您适配的情况进行显示。

如您的小部件展示遇到问题,可以发送邮件至miui-widget@xiaomi.com 联系我们,我们的开发者关系团队会尽快为您解决。

“上传页面”说明:

“小部件详情”说明:

完善资料页,“本地化语言信息”说明

完善资料页,“发布设备”说明:完善资料页,“小部件介绍”、“预览图”说明:

填写完成后,您可以鼠标点击“查看商店展示预览”,进行展示效果查看。

因填写信息过多,您可在填写过程中选择保存,保存成功后,小部件的状态变为“已填写”,保存后的信息可再次进行编辑。

1.3.排序

当所有小部件的信息填写完毕后,您可以在“小部件详情页”通过上下拖拽小部件,或者点击小部件前面的数字序号进行排序。该排序为小部件在小部件中心应用详情页的展示顺序,排序靠前的小部件可以获得更多曝光哟!

1.4.提交审核

当所有小部件的信息填写完毕后,可点击提交审核。提交审核后应用状态更新为等待审核,我们会在1-3个工作日内为您审核。如果超过3个工作日还未有审核结果反馈给您,可以发送邮件至miui-widget@xiaomi.com 联系我们,我们的开发者关系团队会尽快为您解决。

2.MIUI小部件更新流程

2.1.小部件版本更新

已发布的小部件,如需更新版本,则需要优先在应用商店更新您的APP,应用商店操作流程可参考:应用创建与更新,当您的APP更新后,可进入小部件开放平台,与上述小部件创建流程一致。

如您新版APP中有新添加的小部件,该小部件会有“新增”标签提示。

2.2.小部件仅资料更新

已发布的小部件,如需更新资料(小部件介绍文案/小部件预览图),可在应用管理界面,点击“管理小部件”按钮进入小部件详情页,针对不同状态的小部件操作如下:

  • 已上架的小部件:可点击“更新资料”按钮,进行资料更新,与创建小部件的流程一致
  • 审核中的小部件:仅支持操作“查看详情”
  • 未提交审核的小部件/未发布成功的小部件:可点击“重新编辑”按钮,进行资料更新,与创建小部件的流程一致。

3.MIUI小部件管理流程

3.1.排序

当您的小部件已经上线,您进入小部件管理页面后,仍可在“小部件详情页”通过上下拖拽小部件,或者点击小部件前面的数据序号进行排序。

⚠️注意:更改排序无需提交审核,平台每五分钟定时向小部件中心推送最新排序

3.2.驳回原因展示

3.2.1.审核未通过

如您提交的小部件审核未通过,您可在小部件详情页,鼠标滑至该小部件状态,即可查看审核反馈,同时还可以下载附件,直接查看我们提供的图片反馈。

3.2.2.小部件被下架

如您在线的小部件因违规等原因被下架,您可在小部件详情页,鼠标滑至该小部件状态,即可查看下架原因,您可以待问题解决后重新提交审核。

如您对我们的下架有异议,可以发送邮件至miui-widget@xiaomi.com 进行申诉,我们的开发者关系团队会尽快与您联系。

3.2.3.APP被下架

如您的APP在应用商店被下架,您该版本的小部件将同样在小部件中心被下架。

该情况,我们将在您进入小部件开放平台后进行弹窗提示,您可以选择到应用商店处理该版本APP下架问题,当您的APP在应用商店重新提交审核后,您可以在小部件开放平台重新上传小部件。

如您点击“返回”按钮,您可进入小部件详情页面管理历史版本的小部件,被下架的小部件仅支持查看,无法进行其他操作。

4.MIUI小部件下架流程

4.1.适用范围

当您的小部件在上架状态,可申请小部件下架。当您的小部件申请下架后,或在未发布状态下,可以重新编辑资料并进行提交,但不支持删除小部件。

4.2.注意事项

小部件下架:小部件下架后还是存在后台的数据库中,只是未上线,但下架的小部件将无法在小部件中心中进行展示。

4.3.操作流程

  • 登录小部件开放平台,在“我的应用”列表模块,选择要需要管理的应用,并点击“管理小部件”,进入小部件详情页,将展示最新版本的小部件。
  • 当您需要下架的小部件在多个版本均有上线,点击“申请下架”时,可先确认需要下架的版本,确认下架后,小部件状态和操作由[已发布——申请下架]变为[待填写——编辑]。

小米展示图规则须知

小米展示图规则须知

上传的预览图将在小部件商店您应用的详情页面进行展示,需要您保证您的预览图符合符合如下规范:

1. 需上传对应小部件的展示图,用于小部件商店中展示;

2. 预览图尺寸为:

2*2:  440*440 px

4*2:  948*440 px

4*4: 948*1000 px

3. 像素值需适配不同的设备和屏幕(1080p屏和2k屏),并兼容桌面的不同布局;

4. 预览图圆角 :小部件商店的预览图圆角为38px;

5. 预览图内如需使用到手机外观图片,必须使用小米手机外观,禁止使用 iPhone 或其他品牌手机外观素材;

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