Android 13 应用适配指南

1.获取Android 13

1.1 小米手机升级Android13

如果您已错过小米社区发布的关于MIUI基于Android 13的升级公开招募,可前往开发者内测申请权限,或请您联系自己公司商务或运营同学,联系我们,我们会为您开通有关权限并指导升级。

目前支持升级Android 13的MIUI开发版的机型有(有序更新中):

小米:小米12、小米12 Pro(高通平台)

Redmi: K50G(电竞版)、K40S、Note 11T Pro/Pro+

1.2 小米云测平台已上线

今年我们提供了足量的云测设备来供开发者适配,小米云测平台

提示:如果发现云测平台没有权限,可以通过小米云测平台权限申请

1.3 Google原生机升级Android 13

开发者持有Pixel系列的机器可以直接ota升级,或者下载镜像升级,具体见链接:https://developer.android.com/about/versions/13/download

1.3 Android 模拟器

在 Android Studio 中,您可以按如下方式安装 Android 13 SDK:

  • 依次点击 Tools > SDK Manager。
  • 在“SDK Platforms”标签页中,选择 Android 13。
  • 在 SDK Tools 标签页中,选择 Android SDK Build-Tools 33。

点击OK安装 SDK。

如需访问 Android 13 API 并测试您的应用与 Android 13 的兼容性,请打开模块级build.gradle或build.gradle.kts文件,并使用 Android 13 所对应的值对它们进行更新:如何设置这些值的格式取决于您所使用的 Android Gradle 插件 (AGP) 版本。

注意:如果您尚未准备好完全支持 Android 13,您仍然可以使用可调试的应用、Android 13 设备和兼容性框架来执行应用兼容性测试,而无需更改应用以使其与预览版 SDK 兼容或以此为目标平台。

小米开发平台 关于无GPS硬件机型的位置服务适配说明

小米开发平台 关于无GPS硬件机型的位置服务适配说明

1.背景

由于部分WiFi版本的小米平板(例如:小米平板5 WiFi版等)无GPS硬件,即使用户开启位置服务,应用也无法获取到GPS Provider,部分依赖于GPS位置服务的应用可能会提示“未开启位置服务或出现其他问题”,用户体验较差。

2.适配说明

针对无GPS硬件的机型,应用可以通过检测Location Proveders中是否包含”gps”来判断是否有GPS硬件。

另外,可以通过检测Network Provider状态来判断用户是否打开位置服务。

参考Demo如下:

LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
// 无GPS设备通过判断Network Provider来判断位置服务是否打开
// 其他设备仍然通过GPS Provider来判断位置服务是否打开
if (!isGPSExist(lm) && isNetworkLocationAble(lm) || isGpsAble(lm)) {
    // 用户已开启位置服务
    // 应用自身定位逻辑
} else {
    // 提醒用户未开启位置服务,引导用户至位置服务设置页面
    openLocationService();
}

// 应用可以通过以下方式判断是否有GPS硬件
public static boolean isGPSExist(LocationManager lm) {
    return lm.getAllProviders().contains("gps");
}

private boolean isGpsAble(LocationManager lm){
    return lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
}

private boolean isNetworkLocationAble(LocationManager lm){
     return lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}

private void openLocationService()(){
    Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    startActivity(intent);
}

3.优化建议

如果应用对GPS有强依赖,建议应用优化新增相关友好提示。

小米开发平台openGL ES glFinish使用规范

小米开发平台openGL ES glFinish使用规范

1.背景

在高通最新的平台上,为了进一步提升性能,启用了 CompressTexturesOnUpload 功能,如果应用不遵循 openGL glFinish 规范,会​进一步增加应用渲染数据出现异常的概率。

2.规范说明

应用在修改更新纹理后,openGL 规范要求应用程序执行完 glFinish 后, 其他地方才能去操作使用此纹理(注意高通平台目前只需要 glFlush, 但对于 openGL 规范,需要执行 glFinish,详见 OpenGL ES 3 specification section D “Propagating changes to objects”)。

3.案例分析

  • 美图秀秀相机预览人像扭曲
  • 美颜相机预览画面偏暗
  • 问题原因

应用的 thread 6 和 thread 7 共享 GL Context,然后 thread 6 对 thread 7 上传的纹理(texture13)进行采样,但是在 thread7 中,纹理更新时没有 进行 glFinish 或 glFlush,导致数据渲染出现问题。所以 thread7 应该在纹理更新后调用 glFinish 或 glFlush,然后 thread 6 再使用 texture13 来渲染。

小米开发平台 双开应用修改头像失败等场景适配说明

小米开发平台 双开应用修改头像失败等场景适配说明

背景

在Android 11及以上版本,当应用双开后,双开应用中启动系统剪裁页面,实现修改头像等场景时,会出现剪裁保存图片失败问题。此场景需要应用适配双开。

应用双开功能

手机设置-应用设置-应用双开

适配说明

在应用启动剪裁页面时,intent中放入的输出路径的uri需要带上userId,具体如下:

/**
* 反射获取getCallingUserId
*/
private int getCallingUserId(){
        Class UserHandleClass = UserHandle.class;
        int userId = 0;
        try {
            userId = (int)UserHandleClass.getMethod("getCallingUserId").invoke(UserHandleClass);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e){
            e.printStackTrace();
        }
            return userId;
    }
/**
* @param uri: 需要剪裁uri,如图库等返回的uri
* 拉起系统剪裁页面
*/
private void cropImage(Uri uri) {
        if(uri == null) {
           return;
        }
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.putExtra("scale", true);
        intent.putExtra("aspectX", XXX);
        intent.putExtra("aspectY", XXX);
        intent.putExtra("outputX", XXX);
        intent.putExtra("outputY", XXX);
        intent.putExtra("outputFormat", XXX);
        intent.putExtra("return-data", XXX);
        File tmpFile = new File(getExternalFilesDir(null), System.currentTimeMillis()+".jpg");
        try {
            tmpFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Uri mCutUri = FileProvider.getUriForFile(MainActivity.this,
                "com.xxx.fileprovider", tmpFile);
        //生成带userId的uri
        Uri.Builder builder = mCutUri.buildUpon();
        Uri newUri = builder.encodedAuthority("" + getCallingUserId() + "@" + mCutUri.getEncodedAuthority()).build();
 
        intent.putExtra(MediaStore.EXTRA_OUTPUT, newUri);
        //调用grantUriPermission给newUri授权
        grantUriPermission("packaName", newUri,  Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.setDataAndType(uri, "image/*");
        startActivityForResult(intent, REQUEST_CROP);
  
    }

小米开发平台应用删除前台服务通知导致的闪退问题说明

小米开发平台应用删除前台服务通知导致的闪退问题说明

  • 近期发现较多应用出现闪退现象,具体trace如下:
AndroidRuntime: FATAL EXCEPTION: main
AndroidRuntime: Process: com.xxx.app, PID: 25339
AndroidRuntime: java.lang.SecurityException: Not allowed to delete channel XXX with a foreground service
AndroidRuntime:         at android.os.Parcel.createExceptionOrNull(Parcel.java:2376)
AndroidRuntime:         at android.os.Parcel.createException(Parcel.java:2360)
AndroidRuntime:         at android.os.Parcel.readException(Parcel.java:2343)
AndroidRuntime:         at android.os.Parcel.readException(Parcel.java:2285)
AndroidRuntime:         at android.app.INotificationManager$Stub$Proxy.deleteNotificationChannel(INotificationManager.java:4040)
AndroidRuntime:         at android.app.NotificationManager.deleteNotificationChannel(NotificationManager.java:909)
AndroidRuntime:         at androidx.core.app.NotificationManagerCompat.deleteNotificationChannel(SourceFile:2)
AndroidRuntime:         at com.gyf.cactus.ext.c$a.run(SourceFile:1)
AndroidRuntime:         at android.os.Handler.handleCallback(Handler.java:938)
AndroidRuntime:         at android.os.Handler.dispatchMessage(Handler.java:99)
AndroidRuntime:         at android.os.Looper.loop(Looper.java:236)
AndroidRuntime:         at android.app.ActivityThread.main(ActivityThread.java:8142)
AndroidRuntime:         at java.lang.reflect.Method.invoke(Native Method)
AndroidRuntime:         at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
AndroidRuntime:         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
AndroidRuntime: Caused by: android.os.RemoteException: Remote stack trace:
AndroidRuntime:         at com.android.server.notification.NotificationManagerService$10.enforceDeletingChannelHasNoFgService(NotificationManagerService.java:3427)
AndroidRuntime:         at com.android.server.notification.NotificationManagerService$10.deleteNotificationChannel(NotificationManagerService.java:3440)
AndroidRuntime:         at android.app.INotificationManager$Stub.onTransact(INotificationManager.java:1737)
AndroidRuntime:         at android.os.Binder.execTransactInternal(Binder.java:1160)
AndroidRuntime:         at android.os.Binder.execTransact(Binder.java:1129)
  • 原因是受Google的一笔安全校验Patch影响:https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java;l=3413
private void enforceDeletingChannelHasNoFgService(String pkg, int userId,
                String channelId) {
            if (mAmi.hasForegroundServiceNotification(pkg, userId, channelId)) {
                Slog.w(TAG, "Package u" + userId + "/" + pkg
                        + " may not delete notification channel '"
                        + channelId + "' with fg service");
                throw new SecurityException("Not allowed to delete channel " + channelId
                        + " with a foreground service");
            }
        }

也就是Google限制了存在前台服务时,不允许应用删除与前台服务绑定的channel。

以上,请开发者检查处理,感谢支持!

小米开发平台 应用频繁调用getSubscriberId/getDeviceId优化说明

应用频繁调用getSubscriberId/getDeviceId优化说明

getSubscriberId/getDeviceId返回null情况说明

Android 10隐私权变更应用无法通过以下方法获取设备标识符:

Build

getSerial()

TelephonyManager 

getImei()
getDeviceId()
getMeid()
getSimSerialNumber()
getSubscriberId()

具体参见:https://developer.android.com/about/versions/10/privacy/changes?hl=zh-cn

由于Android 10隐私权限变更导致应用无法获取设备标识符,应用无法获取设备标识符后一直频繁调用相关接口尝试获取,从而导致系统相关进程binder占满,引发一系列问题:如手机重启,应用黑屏卡顿等问题。因此建议App优化,不要频繁调用以上接口。

小米开发平台应用调用Camera预览效果适配说明

小米开发平台应用调用Camera预览效果适配说明

1.应用中拍摄或扫一扫功能预览页面出现拉伸现象

由于不同手机屏幕尺寸比例不尽相同,或许应用在调用Camera预览实现方法不标准或第三方SDK维护不及时,提供的预览窗口宽高比例和Camera输出图像的宽高比例不一致,以至于应用在使用拍摄或扫一扫功能时预览页面就可能出现拉伸现象。

在此建议应用开发者使用的预览窗口宽高比例和Camera输出图像的宽高比例尽量保持一致,这样预览显示的画面即可以正常显示。为了解决应用频繁适配工作,希望开发者参考标准的Camera预览使用示例实现功能。

2.Camera预览接口使用示例

2.1.获取Camera支持的预览图像输出分辨率

//Camera API1
Camera mCamera = Camera.open(cameraId);
List<Size> supportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes()
 
//Camera API2
Size[] supportedPreviewSizes = CameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(klass)

2.2.根据预览窗口Surface的宽高来选取最佳Camera图像输出尺寸

  • Camera API1:
//获取预览窗口的宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // We purposely disregard child measurements because act as a
    // wrapper to a SurfaceView that centers the camera preview instead
    // of stretching it.
    final int width = resolveSize(getSuggestedMinimumWidth(),
            widthMeasureSpec);
    final int height = resolveSize(getSuggestedMinimumHeight(),
            heightMeasureSpec);
    setMeasuredDimension(width, height);
 
    if (mSupportedPreviewSizes != null) {
        mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width,
                height);
    }
 
    if (mCamera != null) {
      Camera.Parameters parameters = mCamera.getParameters();
      parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
 
      mCamera.setParameters(parameters);
    }
}
//得到最佳Camera图像输出尺寸
public Camera.Size getOptimalSize(@NonNull List<Camera.Size> sizes, int w, int h) {
    final double ASPECT_TOLERANCE = 0.1;
    double targetRatio = (double) w / h;
    Camera.Size optimalSize = null;
    double minDiff = Double.MAX_VALUE;
 
    int targetHeight = h;
 
    for (Camera.Size size : sizes) {
      double ratio = (double) size.width / size.height;
      if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
      if (Math.abs(size.height - targetHeight) < minDiff) {
        optimalSize = size;
        minDiff = Math.abs(size.height - targetHeight);
      }
    }
 
    if (optimalSize == null) {
      minDiff = Double.MAX_VALUE;
      for (Camera.Size size : sizes) {
        if (Math.abs(size.height - targetHeight) < minDiff) {
          optimalSize = size;
          minDiff = Math.abs(size.height - targetHeight);
        }
      }
    }
 
    return optimalSize;
  }

详细实现见:https://cs.android.com/android/platform/superproject/+/master:development/samples/HoneycombGallery/src/com/example/android/hcgallery/CameraFragment.java

  • Camera API2:
//获取预览窗口的宽高
private final TextureView.SurfaceTextureListener mSurfaceTextureListener
        = new TextureView.SurfaceTextureListener() {
 
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
        openCamera(width, height);
    }
 
    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
        configureTransform(width, height);
    }
 
    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
        return true;
    }
 
    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture texture) {
    }
 
};
//得到最佳Camera图像输出尺寸
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
        int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
 
    // Collect the supported resolutions that are at least as big as the preview Surface
    List<Size> bigEnough = new ArrayList<>();
    // Collect the supported resolutions that are smaller than the preview Surface
    List<Size> notBigEnough = new ArrayList<>();
    int w = aspectRatio.getWidth();
    int h = aspectRatio.getHeight();
    for (Size option : choices) {
        if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
                option.getHeight() == option.getWidth() * h / w) {
            if (option.getWidth() >= textureViewWidth &&
                option.getHeight() >= textureViewHeight) {
                bigEnough.add(option);
            } else {
                notBigEnough.add(option);
            }
        }
    }
 
    // Pick the smallest of those big enough. If there is no one big enough, pick the
    // largest of those not big enough.
    if (bigEnough.size() > 0) {
        return Collections.min(bigEnough, new CompareSizesByArea());
    } else if (notBigEnough.size() > 0) {
        return Collections.max(notBigEnough, new CompareSizesByArea());
    } else {
        Log.e(TAG, "Couldn't find any suitable preview size");
        return choices[0];
    }
}

详细实现见:https://cs.android.com/android/platform/superproject/+/master:development/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java

2.3.将获取到的最佳图像输出尺寸设置到Camera

//Camera API1
mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight())
 
//Camera API2
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight())

小米开发平台Audio接口使用规范

小米开发平台Audio接口使用规范

1. 关于setSpeakerphoneOn方面的使用

问题描述:

在MIUI的项目开发过程中,经常收到用户反馈通话声音会自动外放,经分析排查发现均是第三方应用在通话期间调用了setSpeakerphoneOn(true)方法引起的,影响用户正常通话。

建议:

音频类应用可以利用AudioFocus机制或者监听电话状态来进行控制处理,在手机通话期间不要调用setSpeakerphoneOn方法,从而避免对正常通话的影响。

2. 关于setMode的使用

问题描述:

当前很多APP,多见于社交或需要队友语音的游戏。使用这个接口来设置手机通话模式,改变手机声音的输出设备。当电话模式处于“AudioManager.MODE_IN_COMMUNICATION”或“AudioManager.MODE_IN_CALL”时,声音会默认从听筒发声。且用户默认音量调节类型会变为通话音量类型。

建议:

开发过程中,setMode(AudioManager.MODE_IN_COMMUNICATION)和setMode(AudioManager.MODE_NORMAL)务必成对使用。需要考虑应用异常的情况下,也能够将电话模式重置为normal状态。

3. 关于getStreamVolume的使用

使用建议:

通过该方法得到的音量值受多方因素影响。需要充分考虑当前设备的audiopolicy route策略,选中不同的设备,得到的音量值也不一样。此外该方法受静音模式影响,如果手机处于静音状态,无论音量值为多少,得到的index均为0。如果需要不想受静音状态影响,建议使用getLastAudibleStreamVolume。

4. setStreamVolume音量设置会导致媒体音退出静音状态

问题描述:

  • MIUI允许用户在静音开启时静止媒体音。但静止状态时,一旦触发音量增操作,媒体音会立刻退出静音状态。有许多视频播放或直播app,会在退出app时保存当前音量值,重开app时将音量值restore。这个操作经常会错误的将用户的静音状态解除。
  • app会catch按键时间,调用setStreamVolume来控制媒体音量。某些情况会导致音量调节异常。

建议:

  • 当应用返回时,通过isStreamMute或getRingerMode方法获取手机当前状态,如果处于媒体音或者铃声状态处于静音,则不执行setStreamVolume操作。
  • 音量的控制尽可能的交由系统处理,少用setStreamVolume设置音量。

小米开发平台联系人信息使用规范

小米开发平台联系人信息使用规范

1. 背景

曾经 MIUI 系统用户反馈过联系人丢失问题,其中一部分的原因是第三方应用不遵循 Android 标准规范,进行了非自身帐号的联系人删除,导致在第三方应用同步或者恢复联系人数据时,出现了小米帐户联系人被删除而丢失的情况。

显然,这样的情形无论对于用户还是系统,都是无法接受的。

2. 规范

Android 平台上联系人数据是有帐户归属的概念,各个应用或者服务管理各自帐户的联系人数据。虽然可以跨帐户读取联系人数据,但却不应该跨帐户删除联系人数据,否则,多个应用或者服务如果对数据处理逻辑不一致(大多数情况会是这样),就会出现数据丢失,或频繁修改数据。

联系人资料作为用户的核心数据之一,不允许也不应该被第三方应用删除。因为无法直接一一规范第三方应用,也无法预判可能即将进行不规范操作的应用,所以只能在系统层面进行第三方应用行为约束,保证小米帐户联系人不会被第三方应用后台直接删除,保证用户的数据安全。

3. 限制

如果第三方应用在 MIUI 系统上出现了不规范的删除行为,会弹窗提示:“XXX 正在删除小米帐户上的联系人,为了保护信息安全,本次删除已被禁止。您可使用系统通讯录删除联系人。”。

那么结果就是,要么用户一次次点击确认以继续不规范的删除(基本不可能),要么用户无法使用对应的服务(不利于应用和用户)。

因此,建议第三方应用不要删除小米帐户的联系人数据,在合理的操作范围内一同给用户最好的体验。

小米开发平台用户可拒绝应用获取Android ID说明

小米开发平台用户可拒绝应用获取Android ID说明

一、变更说明

应用下载安装时,默认开启 虚拟身份ID开关,用户可在虚拟身份ID授权管理中关闭应用获取虚拟身份ID(OAID和Android ID)

若想体验该功能,请将小米手机升级至21.8.1及之后的开发版,手机设置-隐私保护-保护隐私-特殊权限设置-虚拟身份管理。

二、系统版本

Android 11及以上版本

三、虚拟身份ID关闭后返回值

若用户关闭了应用获取虚拟身份,通过Settings.Secure.getString(Settings.Secure.ANDROID_ID)获取到Android ID返回值见下表:

MIUI开发版Settings.Secure.getString(Settings.Secure.ANDROID_ID)返回值
21.7.31之前不关注
21.8.1-21.9.15
21.9.16之后0000000000000000
MIUI稳定版
MIUI12.5之前不关注
MIUI130000000000000000 

四、应用适配

不需要应用适配,但需要开发者检查验证是否影响应用正常使用,并进行相应调整。

小米开发平台 禁止应用获取MAC地址说明

小米开发平台 禁止应用获取MAC地址说明

一、变更说明:

在Android 11及以上机型中,应用无法通过

NetworkInterface#getHardwareAddress

二、获取到设备mac地址

对于targetSdkVersion<30的App,将会返回一个占位符02:00:00:00:00:00;

对于targetSdkVersion>=30的App,将会直接返回null(与原生特性一致)。

三、应用适配

本功能自身无需适配,但需要开发者检查验证是否影响应用正常使用,并进行相应调整。

四、功能体验

若想体验该功能,机型和版本要求如下:

小米11-21.7.13以上的开发版

小米开发平台机型信息获取说明

小米开发平台机型信息获取说明

简介

小米手机机型信息或有相关调整更新,为方便App开发者准确获取相关信息,现提供以下信息供开发者使用。

1、机型名称

新增ro.product.marketname属性,值为手机设备名(上市机型名称);

历史已发布手机上设备型号的属性ro.product.model依然有效,以后新发布的机型上不再生效。请开发者及时适配。

接口说明与示例

建议应用优先读取属性ro.product.marketname,如果返回值不为空,这个值就是手机设备型号。如果此属性返回值为空,则读取之前的属性ro.product.model值。

参考代码:

public static String getDeviceName() {
    String deviceName = "";
    try {
        Class SystemProperties = Class.forName("android.os.SystemProperties");
        Method get = SystemProperties.getDeclaredMethod("get", String.class, String.class);
        deviceName = (String) get.invoke(SystemProperties, "ro.product.marketname", "");
        if (TextUtils.isEmpty(deviceName)) {
            deviceName = (String) get.invoke(SystemProperties, "ro.product.model", "");
        }
    } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
        e.printStackTrace();
    }
    return deviceName;
}

2、CPU 型号信息

GKI 2.0 之前,/proc/cpuinfo 中包含 Hardware 相关信息,应用可以从中读取 cpu 型号信息;

GKI 2.0 之后,/proc/cpuinfo 中不再包含 Hardware 相关信息,应用无法从中读取到 cpu 型号信息,需要从 android.os.SystemProperties 中读取 ro.soc.model 属性信息。参考说明:关于 GKI 内核版本说明

CPU 型号获取方法

建议优先从 android.os.SystemProperties 中读取 ro.soc.model 属性信息,若此属性信息为空,则再从 /proc/cpuinfo 中读取 Hardware 信息即可。

小米开发平台设备标识体系说明

小米开发平台设备标识体系说明

1.前言

《移动智能终端补充设备标识体系规范》由小米起草,并与中国信息通信研究院共同研究制定。此标准旨在规范移动智能终端补充设备标识体系的体系架构、功能要求、接口要求以及安全要求,使设备生产企业统一开发接口,为移动应用开发者提供统一调用方式,方便移动应用接入、减小维护成本。移动智能终端补充设备标识体系架构共涉及四类实体,包括开发者、开发者开发的应用软件、移动智能终端设备的操作系统、用户及用户使用的设备。为保护用户用户的隐私和标识设备的唯一性,根据不同使用对象和不同用途,基于移动智能终端设备,分别生成设备唯一标识符、匿名设备标识符、开发者匿名设备标识符和应用匿名设备标识符,将这四个设备标识符构成补充设备标识体系。

2.获取移动智能终端补充设备标识SDK

2.1获取移动安全联盟MSA提供的统一SDK,适用于含小米在内的多家手机厂商

获取地址:http://msalliance.icoc.bz/col.jsp?id=120

2.2获取小米提供的SDK,仅适用于小米手机

IdentifierManager#isSupported  判断是否支持获取设备标识

IdentifierManager#getUDID 获取设备唯一标识符

IdentifierManager#getOAID 获取匿名设备标识符

IdentifierManager#getVAID 获取开发者匿名设备标识符

IdentifierManager#getAAID 获取应用匿名设备标识符

小米开发平台Activity Embedding 适配指南

小米开发平台Activity Embedding 适配指南

1、功能介绍

为了利用大屏幕的显示区域,Google在Jetpack WindowManager中添加了Activity Embedding,实现同一应用内不同activity分屏显示,开发者可以通过配置XML文件或者进行Jectpack WindowManager API调用确定如何显示activity(并排或堆叠)。

目前支持以下两种逻辑跳转,第一种是可以实现左容器界面固定:

第二种是支持多层次导航:

另外系统会自动维护对小屏幕的显示,当应用在小屏幕的设备上时,activity会相互堆叠,在大屏幕上,activity会并排显示,对于折叠屏,会随着设备折叠和展开而堆叠和并排显示activity。

2、快速上手

JetPack WindowManager库添加了ActivityEmbeddingComponent,可以根据分屏规则创建容器实现应用内分屏,配置分屏规则涉及到下面几个步骤:

2.1 添加依赖项

将WindowManager库依赖项添加到应用的build.gradle文件中

implementation("androidx.window:window:1.1.0-alpha02")

2.2 配置分屏规则

2.2.1 XML静态配置

放在res资源文件的xml文件夹下

    <!-- The split configuration for activities. -->
    
    <resources
      xmlns:window="http://schemas.android.com/apk/res-auto">
      <!-- Automatically split the following activity pairs. -->
      <SplitPairRule
        window:clearTop="true"
        window:splitMinWidth="600dp">
        <SplitPairFilter
          window:primaryActivityName=".SplitActivityList"
          window:secondaryActivityName=".*"/>
      </SplitPairRule>
    
      <!-- Automatically launch a placeholder for the list activity. -->
    
      <SplitPlaceholderRule
        window:placeholderActivityName=".SplitActivityListPlaceholder"
        window:splitRatio="0.3"
        window:splitMinWidth="600dp">
        <ActivityFilter
          window:activityName=".SplitActivityList"/>
      </SplitPlaceholderRule>    
      
    <!-- Automatically launch a full activity. -->
    
    <ActivityRule window:alwaysExpand="true">
        <ActivityFilter 
            window:activityName=".FullActivity"/>
    </ActivityRule>
    
    </resources>

2.2.2 代码动态配置

运行时定义分屏的配置,开发者可以在startActivity或者onCreate时使用

splitController.registerRule(new SplitPairRule(newFilters));

splitController.unRegisterRule(new SplitPairRule(newFilters));

来动态添加/移除规则

protected void onCreate(@Nullable Bundle savedInstanceState) {
    Set<SplitPairFilter> pairFilters = new HashSet<>();
    SplitPairFilter filter = new SplitPairFilter(primaryActivityComponetName, 
            secondaryActivityComponetName, 
            null);
    pairFilters.add(filter);
    SplitPairRule pairRule = new SplitPairRule(pairFilters, 
            SplitRule.FINISH_ADJACENT, 
            SplitRule.FINISH_ALWAYS, 
            true, 
            600, 
            600, 
            0.3f, 
            LayoutDirection.LOCALE);
    SplitController splitController = SplitController.getInstance();
    splitController.registerRule(pairRule);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

2.3 将规则定义通知库

使用Jetpack Startup库在加载应用的其他组件和启动 activity 之前执行初始化。如需启用启动功能,请在应用的 build 文件中添加库依赖项:

implementation("androidx.startup:startup-runtime:1.1.0")

并在manifest中添加:

<!-- AndroidManifest.xml -->
<provider android:name="androidx.startup.InitializationProvider"
  android:authorities="${applicationId}.androidx-startup"
  android:exported="false"
  tools:node="merge">
  <!-- This entry makes ExampleWindowInitializer discoverable. -->
  <meta-data android:name="**androidx.window.sample.embedding.ExampleWindowInitializer**"
    android:value="androidx.startup" />
</provider>

2.4 添加初始化程序类实现

通过将包含定义 (main_split_config) 的 xml 资源文件的 ID 提供给 SplitController.initialize() 来设置规则:

class ExampleWindowInitializer extends Initializer<SplitController> {

  @Override
  SplitController create(Context context) {
    SplitController.initialize(context, R.xml.main_split_config);
    return SplitController.getInstance(context);
  }

  @Override
  List<Class<? extends Initializer<?>>> dependencies() {
    return emptyList();
  }

}

2.5 XML配置文件参数解析

参数含义
SplitPairRule分屏配对情景(两侧容器均有具体activity),对应SplitPairFilter
splitRatio分屏比默认为0.5f,即左右5:5分屏 ,对于IM类应用,可考虑设置分屏比为0.3f,即左右3:7分屏
splitMinWidth默认配置600dp,宽度达到600dp才可以分屏,主窗口可分屏显示的最小窗口宽度
splitMinSmallestWidth默认配置600dp,主窗口可分屏显示的最小sw值
finishPrimaryWithSecondary默认为  false,true:若secondary container中所有activity都finish,则primary container中创建分屏的activity也会finish,不推荐应用主动配置此项。(androidx.window:window:1.1.0-alpha03中默认是SplitRule.FINISH_ADJACENT)
finishSecondaryWithPrimary默认为 ture, true:若primary container中所有activity都finish,则secondary container中所有activity也会finish,不推荐应用主动配置此项。(androidx.window:window:1.1.0-alpha03中默认是SplitRule.FINISH_ALWAYS)
clearTop默认为 false,true:启动activity窗口分屏,存在相同的primary container,若新建secondary container,则原secondary container中的activity会被finish掉,推荐应用配置,避免右分屏出现多实例
SplitPairFilter分屏配对关系(必需配置)
primaryActivityName分屏的primay activity component name
secondaryActivityName分屏的secondary activity component name
secondaryActivityAction分屏的secondary activity 启动的action (配置此项需要在启动的时候添加action) 
参数含义
SplitPlaceholderRule用来描述分屏下的占位,对应ActivityFilter(必需配置)
placeholderActivityIntentName通过同时打开两个activity创建分屏,secondary占位activity component name
componentName组件名
intentActionintent
splitRatio分屏比默认为0.5f,即左右5:5分屏
splitMinWidth默认配置600dp,宽度达到600dp才可以分屏主窗口可分屏显示的最小窗口宽度
splitMinSmallestWidth默认配置600dp,主窗口可分屏显示的最小sw值
ActivityFilter适配占位规则的activity(必需配置)
参数含义
ActivityRule需要全屏显示的activity,对应ActivityFilter(必需配置
alwaysExpand取值”true”或false true:启动的activity全屏显示
componentName组件名
intentActionintent
ActivityFilter适配全屏规则的activity(必需配置)

2.6 应用主动适配生效

2.6.1 MIUI meta-data标记

AndroidManifest.xml中声明下列meta-data字段。设置为true表示app已经主动适配,系统不会反向适配

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
    <meta-data android:name="embedded" android:value="true"/>
</application>

注意:

Android 13后该meta-data将不使用

2.6.2 Google property标记

Google官方添加property字段,用来标识APP是否允许系统反向适配Activity Embedding

true表示允许系统反向适配,false不允许系统反向适配

注意:自适配Activity Embedding和不允许系统反向适配Activity Embedding的APP需要设置为false

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
    <property 
        android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE" 
        android:value="false" />
</applicatio

3、适配指导

3.1 关于应用横竖屏

3.1.1 PAD 竖屏下进入ActivityEmbedding

平板设备处于竖屏状态时,我们推荐应用采用左下图的全屏显示样式,而不是进入ActivityEmbedding。竖屏下的左右分屏显示会导致信息量过多,难以聚焦。如果您的应用在竖屏下进入了ActivityEmbedding,原因是:应用设置的进入ActivityEmbedding的阈值splitMinWidth过小

  • 获取屏幕大小

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

R版本之后:WindowManager.getMaximumWindowMetrics() 。Jetpack WindowManager中支持使用WindowMetrics maximumWindowMetrics = WindowMetricsCalculator.

getOrCreate().computeMaximumWindowMetrics(activity);

  • 获取当前窗口大小

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

R版本之后:WindowManager.getCurrentWindowMetrics() 。Jetpack WindowManager中支持使用WindowMetrics maximumWindowMetrics = WindowMetricsCalculator.

getOrCreate(). computeCurrentWindowMetrics(activity);

推荐解决方案:开发者根据不同屏幕大小的设备动态计算进入分屏的阈值。可使用如下计算方式:

// 获取屏幕大小
WindowMetrics currentWindowMetrics = WindowMetricsCalculator.getOrCreate().
        computeCurrentWindowMetrics(activity);
// 获取当前窗口大小
WindowMetrics maximumWindowMetrics = WindowMetricsCalculator.getOrCreate().
        computeMaximumWindowMetrics(activity);
// 通过configuration获取desity
float density = (float) activity.getResources().getConfiguration().densityDpi / 160f;
// 根据屏幕大小动态设置window:splitMinWidth="600dp"大小,控制竖屏下不进ActivtiyEmbedding
// 为了保证折叠屏外屏下不进入ActivityEmbedding,需要设置一下splitMinSmallestWidth的值,默认600dp
int splitMinWidth = (int) (Math.min(maximumWindowMetrics.getBounds().width(), 
        maximumWindowMetrics.getBounds().height()) / density + 1);

3.1.2 PAD无法横屏

原因:ActivityEmbedding 只是改变activity的显示位置,不会强制更改方向,需要app支持横屏

推荐方案:第一个启动的activity方向设置为behind以及全屏启动的activity设置为behind

3.1.3 折叠屏控制在外屏竖屏显示

为了避免您适配ActivityEmbedding后在折叠屏外屏出现如下图所示的情况,需要控制应用在折叠屏外屏竖屏显示,监听 WindowInfoTracker 来手动设置方向,具体使用参考官方文档https://developer.android.com/guide/topics/large-screens/make-apps-fold-aware

3.2 支持resizeable

应用需要在 AndroidManifest.xml 文件的 application 或者 actvivity 标签中添加 resizeableActivity=true 的属性。

若配置android:resizeableActivity=“false”,会导致无法分屏显示

<application
    android:resizeableActivity="true">
    <activity
          android:resizeableActivity="true" />
</application>

3.3 关于Configuration

当应用在折叠屏内外屏或平板横竖屏交替使用时,activity的大小会发生变化,我们强烈推荐Activity在大小切换时不重启来保证良好的连续性体验,遵循google规范,在android:configChanges 属性增加 screenSize|screenLayout|orientation|smallestScreenSize,并在Activity的onConfigurationChanged回调中更新宽高刷新各个子布局。

3.4 视频全屏

分屏时请求横屏还是在分屏下显示,无法横屏全屏

建议配置activity 始终填满任务窗口:

<ActivityRule  
    window:alwaysExpand="true">
    <ActivityFilter    
        window:activityName=".FullActivity"/>
</ActivityRule>

3.5 clearTop标记

若配置为true

启动activity窗口分屏,存在相同的primary container,会创建新的secondary container,原来的secondary container中的activity会被finish掉,避免右分屏出现多实例。开发者可以根据需要对单独的SplitPairRule 进行配置clearTop=”true”。

用户折叠手机时,屏幕 B 在屏幕 A 之上,屏幕 A 又在菜单之上。当用户从屏幕 B 进行返回导航时,系统会显示屏幕 A 而不是Menu。

可以将分屏配置为通过 clearTop 清除之前的辅助容器,并正常启动新的 activity。

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

3.6 new Task与Launch Flag

当分屏任务窗口中的 activity 启动新任务中的 activity 时,新任务将与包含分屏的任务分开并显示在全窗口中。

在分屏任务窗口中,Launch flag同样起作用,可以复用activity。

3.7 多进程规则匹配

问题:同一Task任务下,子进程配置activity成全屏不生效

采用Jetpack Startup不指定android:process时默认在主进程初始化SplitController,在xml文件夹中的分屏规则不会在子进程生效

<!-- AndroidManifest.xml -->
<provider android:name="androidx.startup.InitializationProvider"
  android:authorities="${applicationId}.androidx-startup"
  android:exported="false"
  tools:node="merge">
  <!-- This entry makes ExampleWindowInitializer discoverable. -->
  <meta-data android:name="**androidx.window.sample.embedding.ExampleWindowInitializer**"
    android:value="androidx.startup" />
</provider>

目前解决方案是需要重写Application在onCreate方法中对需要规则匹配的进程初始化SplitController并且加载静态规则。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        SplitController.initialize(getApplicationContext(), R.xml.split_config);
        super.onCreate();
    }
}

3.8 响应分屏状态变化

ActivityEmbedding下如何判断activity是否处于分屏以及window mode变化,如何实时响应分屏状态变化。

3.8.1 判断是否在左右分屏

左右分屏下,activity中的configuration的mode是multi-window(与系统分屏下的mode一样,在进行部分业务处理时需要注意)

全屏状态下,activity中的configuration的mode是fullscreen

判断当前activity是否在左右分屏状态,可以通过SplitController.getInstance().isActivityEmbedded(Activity activity)进行区分,返回true表示处于分屏。

3.8.2 如何响应分屏状态变化

为了知道 activity 何时在分屏中,可以向SplitController注册一个监听器来监听分屏状态的变化。然后,相应地调整界面:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    splitController
        .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback());
}

class SplitInfoChangeCallback extends Consumer<List<SplitInfo>> {
    public void accept(List<SplitInfo> splitInfoList) {
        findViewById<View>(R.id.infoButton).visibility =
            !splitInfoList.isEmpty()) ? View.GONE : View.VISIBLE;
    }
}

可以在任何生命周期状态下进行回调,包括当 activity 停止时。通常应在onStart()中注册监听器,在onStop()中取消注册监听器。

3.9 占位Activity Placeholder

如需创建带有占位符的分屏,请创建一个占位符并将其与主要 activity 相关联:

<SplitPlaceholderRule  
window:placeholderIntentName=".Placeholder">  
<ActivityFilter    
    window:activityName=".Main"/>
</SplitPlaceholderRule>

注意:开发者不需要自己启动placeholder,会创建多个TaskFragment导致启动异常,在真正显示主页面时建议不启动placeholder,可以动态注册规则,比如在进入主界面前先跳转到登录界面,如果启动了placeholder有可能会导致异常。

小米大屏设备适配说明4

小米大屏设备适配说明4

5.4.1 如何支持分屏

支持分屏的方式非常简单,只需要声明一个属性。从 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

5.4.2 如何进一步优化分屏模式

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

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

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

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

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

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

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

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

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

5.4.6处理 Configuration Changes

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

5.4.7给内容更多空间

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

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

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

5.4.9 MIUI 分屏支持哪些设备

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

5.4.10 分屏如何调试

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

6、开发与测试方式

6.1 模拟器调试

1)小米折叠屏可以在Android Studio里面选择模拟折叠设备进行参照:

2)平板设备可以在Android Studio里面选择模拟平板设备进行参照:

6.2 命令模拟调试

1)可以通过如下adb命令来模拟小米MIX FOLD 2折叠屏手机切换折叠和展开的状态:

//切换到展开状态

adb shell wm size 1914×2160

//切换到折叠状态

adb shell wm size 1080×2520

2)可以通过如下adb命令来切换到平板设备的分辨率,例如:

//切换到2560×1600分辨率

adb shell wm size 1600×2560

6.3 云测平台真机测试

可以使用 小米云测平台

以折叠屏为例,点击小米 MIX Fold 2机器,进入以下页面:

右侧可以上传apk文件,左侧真机可以实时操作

6.4 测试用例建议

6.4.1 用例1

在折叠屏的展开态下打开应用,应用的界面显示正常

1.测试步骤

(1)在折叠屏展开时,打开应用,观察各个页面的显示情况;

2.预期结果

(1)应用的所有页面均显示正常,没有任何变形、截断、模糊、缺失等问题;

6.4.2 用例2

在折叠屏的折叠态下打开应用,应用的界面显示正常

1.测试步骤

(1)在折叠屏折叠时,打开应用,观察各个页面的显示情况;

2.预期结果

(1)应用的所有页面均显示正常,没有任何变形、截断、模糊、缺失等问题;

6.4.3 用例3

在折叠屏的展开态下打开应用,应用的界面功能交互正常

1.测试步骤

(1)在折叠屏展开时,打开应用,依次点击各个页面的按钮;

2.预期结果

(1)应用的所有页面按钮均可以正常响应,没有任何无反应/crash/anr等问题;

6.4.4 用例4

在折叠屏的展开态和折叠态之间切换时,应用的界面显示和功能交互均正常

1.预制条件:应用已经适配了连续性

2.测试步骤

(1)在折叠屏展开时,打开应用;

(2)切换到折叠态,观察页面显示是否正常;

(3)点击页面的各个按钮,观察功能交互是否正常;

(4)在折叠态下打开应用;

(5)切换到展开态,观察页面显示是否正常;

(6)点击页面的各个按钮,观察功能交互是否正常。

3.预期结果

(1)应用在状态切换过程中,没有任何页面闪退重启、变形、截断、模糊、缺失等问题;

(2)应用界面的所有按钮功能均交互正常。

6.4.5 用例5

在分屏模式下,应用的界面显示和功能交互均正常

1.预制条件:应用支持分屏

2.测试步骤

(1)打开应用,进入分屏模式;

(2)观察页面显示是否正常;

(3)点击页面的各个按钮,观察功能交互是否正常。

3.预期结果

(1)应用在分屏模式下,没有任何页面变形、截断、模糊、缺失等问题;

(2)应用界面的所有按钮功能均交互正常。

6.4.6 用例6

进入应用的小窗模式,应用的界面显示和功能交互均正常

1.预制条件:应用支持小窗

2.测试步骤

(1)进入应用的小窗模式;

(2)观察页面显示是否正常;

(3)依次点击各个页面的按钮,观察功能交互是否正常。

3.预期结果

(1)应用在小窗模式下,没有任何页面变形、截断、模糊、缺失等问题;

(2)应用的所有页面按钮均可以正常响应,没有任何无反应/crash/anr等问题;

小米大屏设备适配说明3

小米大屏设备适配说明3

3.5 H5页面适配

我们推荐项目使用响应式设计,实现同一页面自动适配多种屏幕尺寸的效果。

3.5.1 元标签设置

通常情况下,开发者应使用元标签将视口大小调整为屏幕宽度,以确保元素尺寸按照预期显示。

<meta name="viewport" content="width=device-width,initial-scale=1">

开发者可以提供额外的参数来限制用户的缩放行为,一般来说这样做可以优化普通用户体验,减少误操作,但会影响视力障碍用户进行缩放操作,造成访问性问题。

<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">

3.5.2 使用相对 CSS 长度单位

我们推荐开发者在响应式开发中尽量使用相对长度单位,包括:em、rem、%、vw、vh。以下是这些单位的一般使用场景。

em尺寸与当前字体大小相关
rem尺寸相对固定,基于断点与屏幕宽度成阶梯性相关
%尺寸与父元素相关
vw区别于 rem,尺寸与屏幕宽度成线性相关
vh尺寸与屏幕高度成线形相关

上述的只是一般情况,实际开发中使用哪个单位还需视设计要求而定,合理使用长度单位能起到事半功倍的效果。

3.5.3 布局方式

在响应式开发中,开发者应优先使用以下方式进行布局,构建适应性更强的页面。

A.Flexbox(弹性布局)

使用display: flex 可令容器元素启用弹性布局。即使在不确定容器内元素尺寸的情况下,开发者也可以灵活控制元素的大小比例和对齐方式。

参考资料

B.Grid(栅格布局)

Flexbox 可以很好地解决同一行/列内元素的布局问题,Grid 则更适合用于拥有多行/列的二维布局。使用display: grid 可令容器元素启用栅格布局。开发者可灵活控制行列间距、大小比例、对齐方式。

参考资料

3.5.4 CSS 媒体查询

使用 CSS 媒体查询技术,可以使同一元素的样式在不同设备下有不同的表现。通常,我们使用设备屏幕宽度作为判断设备类型的依据,以下是我们推荐的断点划分规则。

屏幕宽度范围设备类型
320 px < width ≤ 480px手机竖屏、折叠屏小屏竖屏
480 px < width ≤ 680px折叠屏展开竖屏
680 px < width ≤ 960px手机横屏、折叠展开横屏、折叠屏小屏横屏
1200px < width桌面端设备

一般情况下,建议开发者使用媒体查询统一控制根字体大小,简化适配流程。

html {  
  font-size: 16px;
}
@media screen and (min-width: 480px) {
  html {
    font-size: 17px;
  }
}
@media screen and (min-width: 680px) {
  html {
    font-size: 18px;
  }
}

3.5.5 Javascript 媒体查询

在一些特殊情况下,页面需要根据断点进行更加复杂的调整。如页面文案,交互逻辑等需要随断点而变化。此时需要使用 Javascript 进行更加细微的调整,我们推荐开发者使用matchMedia 方法进行断点识别,断点划分方案建议与 CSS 方案保持一致。

参考资料

3.5.6 其他建议

1)使用响应式图片,用于图片进行尺寸匹配的场景。

img {
  /* 按比例拉伸或缩放至父元素宽度 */
  width: 100%  }
img {
 /* 按比例缩放至父元素宽度,不拉伸 */
  max-width: 100%  
}
img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* 按比例拉伸或缩放至父元素宽高 */
  object-fit: cover;
  object-position: center;
}

2)为块级元素添加 max-width 而非 width,避免在更小尺寸的显示设备上宽度溢出。

4、平行窗口适配指南

推荐采用Android 12L方案进行适配,您可自主定义activity在左右分屏间的跳转方式。

详细参考Android适配文档SampleActivity Embedding 适配指南

平行窗口适配情况标记

应用主动适配12L平行窗口,需要在AndroidMainfest.xml文件设置标记。

<!-- support 12L Activity Embedding for large screen -->
        <meta-data
            android:name="embedded"
            android:value="true" />

5、进阶开发适配指南(小窗、迷你窗及分屏)

5.1 小窗迷你窗功能介绍

5.1.1 小窗

在多任务处理场景下,小窗意在解决临时使用某个应用的场景,例如:在使用游戏应用时,不便离开,但想发消息给朋友,此时就可以借助小窗打开第二应用

5.1.2 迷你窗

迷你小窗意在解决应用临时挂机的场景,例如:等待网约车、等待游戏更新、观看直播等

5.2 小窗迷你窗适配指南

小窗适配技术文档

MIUI的小窗是基于Android的多窗口Freeform方案实现的。

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

多窗口适配支持文档

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

5.2.1 常用适配方法

(1) 如何支持小窗

支持小窗的方式非常简单,只需要声明一个属性,直接在AndroidManifest.xml中 配置属性android:resizeableActivity=”true” ,不支持小窗直接配置为false,系统默认是支持小窗的。

(2)判断自己是否在小窗  
Activity.onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration  newConfig)  
Activity.isInMultiWindowMode()   判断是否处于多窗口                                                    
Activity.getWindowingMode()  当返回5时表示小窗,需要反射调用
String config = context.getResources().getConfiguration().toString();
boolean isInFreeformWindow = config.contains("freeform");
(3)获取小窗的位置/大小等信息

context.getResources().getConfiguration().windowConfiguration.getBounds(),其中Configuration对象里也有小窗的模式以及bounds,所以我们也需要重写ConfigurationChanged方法,并对界面布局做相应调整,如切换布局、调整控件位置和间距等。

(4)Activity大小切换时不重启适配

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

(5)特别需要注意的适配点,焦点窗口适配

迷你小窗和小窗都有可能成为不是焦点窗口(Focus Window),其中迷你小窗一定是没有焦点的,小窗和小窗下面的全屏应用是可以随意进行焦点切换的,touch在哪个窗口,哪个窗口就是焦点窗口,所以就要保证应用在没有焦点的前提下也是可以正常运行的,特别是视频类app。关键方法:Activity.onWindowFocusChanged(boolean hasFocus)

(6)获取屏幕宽高有些APP会跟随屏幕大小动态计算View的大小,但是有些接口在多窗口形式下面并不试用,要注意以下几点:
  • 在获取Display 对象 或者获取 Resources对象时使用Activity的Context 不要使用Application的Context;
  • 获取Resources对象时不要用context.getResources().getSystem()用context.getResources()就行;
  • Display下有getMetrics getRealMetrics getSize getRealSize四个方法, getRealMetrics和 getRealSize 永远会返回屏幕的宽高,无论应用是否在多屏下, 而getMetrics和getSize会可以获取Activity的大小。
(7)获取View的位置

获取View的位置有两个方法:

  • View.getLocationInWindow(int[] outLocation) 返回距离View所在的Window左上角的距离;
  • View.getLocationOnScreen 返回距离屏幕左上角的距离(由于在小窗模式下应用的位置可以移动, 所以该返回值有时并非是你想要的,所以要慎用)。

5.2.2 其他说明

全局自由窗口功能在MIUI12安卓Q版本以上支持。

如果应用想要启用小窗或者分屏模式下的拖拽功能,请参考如下google资料进行适配:

Android Developer上的拖放文档

AOSP里的DragAndDrop样例应用

github Demo下载连接

5.3 分屏功能介绍

分屏场景意在解决长时间并行使用两个应用时的场景,例如:一边听网课,一边记笔记等;一边看视频,一边逛知乎、微博等

5.4 分屏适配技术文档

https://dev.mi.com/console/doc/detail?pId=1161

小米大屏设备适配说明2

小米大屏设备适配说明2

2.3 不同类型应用适配建议

针对不同行业,不同类型应用,我们推荐以下五种布局方案:多列布局,Tab拉伸布局,分栏布局,左右布局和横屏拉宽。

适配方式应用所属行业
多列布局金融理财,居家生活,时尚购物,影音视听,医疗健康,学习教育
Tab拉伸布局新闻资讯,图书阅读,摄影摄像,短视频
分栏布局聊天社交,效率办公
左右布局实用工具,音乐,时钟,天气,体育运动
横屏拉宽游戏,旅游交通

针对以上适配方式,具体描述和详细示意图如下(左图为APP在手机上的效果,中图为APP在折叠屏内屏上的效果,右图为APP在大屏设备上的效果)

A.多列布局

在应用首页,此类适配下顶部和底部的Tab拉伸到适合大屏设备页面的大小,同时首页主要功能键/APP推荐位置数量增加。

上图为应用商店类APP手机&大屏设备适配示意

上图为视频类APP手机&大屏设备适配示意

B.Tab拉伸

Tab拉伸适配下,页面主要内容不变,页面两侧加上与主要页面背景色同色/相近的背景,同时顶部和底部的Tab拉伸到适合大屏设备页面的大小。顶部按键数量相比于手机可有相应提升。

上图为新闻资讯类APP手机&大屏设备适配示意

上图为短视频类APP手机&大屏设备适配示意

C.分栏布局

分栏适配下,大屏设备上的页面分栏模式与电脑端的IM社交应用保持一致。

上图为社交IM类APP手机&大屏设备适配示意

D.左右布局

左右布局下,顶部和底部的Tab拉伸到适合大屏设备页面的大小,内部页面左右分列,左侧展示主要的文字性内容,右侧展示相应的图片,视频等。

上图为音乐类APP手机&大屏设备适配示意

E.横屏拉宽

此类适配下,APP界面在大屏设备内横屏时,横向铺满屏幕

3、通用开发适配指南

3.1 屏幕尺寸兼容性

应用resizeable 能力支持

运行 Android 的设备多种多样,尤其是当前已出现或未来可能出现的折叠屏设备,它们有着不同的屏幕尺寸和像素密度,而且在折叠屏设备从展开到折叠的切换过程中,同一个设备可能出现多种屏幕尺寸的使用状态。您在做app设计和开发的时候应该考虑支持不同屏幕尺寸和像素密度,以确保界面能够在各类屏幕上美观地呈现。

折叠屏之所以需要适配,是因为我们的应用有可能在运行的过程中,所在的屏幕尺寸发生了变化,这种情况对现有项目多少都会产生一些问题。

所以折叠屏适配的本质是:

当应用运行时,屏幕的尺寸、密度或比例发生了变化,应用能够继续在变化后的屏幕上正常显示和正常运行

允许改变应用尺寸

要适配折叠屏,首先是要让应用支持动态改变尺寸,我们需要根据应用面向的API Level (targetSdkVersion)进行支持resizeable能力在 manifest 中的 Application 或对应的 Activity 下声明:

  • 如果应用程序面向API Level 24以上(targetSdkVersion>=24),系统将默认应用支持resizeable能力。
  • 如果应用程序面向API Level 24以下(targetSdkVersion< 24),需要应用在manifest中显式的声明android:resizeableActivity=true, 才可以支持resizeable能力 。
android: resizeableActivity = ["true" | "false"]

备注:虽然安卓提供了申请受限屏幕能力,但强烈建议您为应用设计resizeable能力,因为一旦您声明了受限屏幕比例(最大或最小)这意味着,当您的app运行在一个屏幕比例超出了您声明的范围,您的应用程序在屏幕上将出现黑边等现象。

详细信息请参阅Android开发者指南

3.2 横竖屏状态获取

需要通过判断AppWindow的宽高比判断是横屏还是竖屏。

(1)宽:高 >= 1为横屏

(2)宽:高 < 1为竖屏

传递给应用的Configuration中的orientation会标识当前是横屏还是竖屏

(1)Configuration.ORIENTATION_LANDSCAPE为横屏

(2)Configuration.ORIENTATION_PORTRAIT为竖屏

3.3 横屏适配情况标记

应用主动适配大屏设备横屏,采用适配全屏的多列布局、Tab拉伸、分栏布局、左右布局、横屏拉宽等方式,需要在AndroidMainfest.xml文件设置标记。设置了此标记,横屏下会优先使用应用自己适配的横屏布局,而不是进入平行窗口下的横屏模式。

<!-- support landscape for large screen(PAD) -->
        <meta-data
            android:name="LandscapeForPad"
            android:value="true" />

3.4 折叠屏连续性支持

折叠连续性的定义:应用在可折叠设备上运行时,当屏幕发生折叠/展开动作,处于前台的应用会从一个屏幕转换到另一个屏幕,为提供出色的用户体验,确保当前应用能在转换后能连续无缝、正常地运行,应用应在同一位置以相同状态恢复,并且依然保持可用良好的UI布局。

三方应用需在 AndroidManifest.xml 文件的 application层级中添加 android.supports_size_changes或者

miui.supportAppContinuity

属性,以便声明应用支持连续性:

<application>      
    <meta-data
        android:name="android.supports_size_changes"
        android:value="true" />
</application>
<application>      
    <meta-data
        android:name="miui.supportAppContinuity"
        android:value="true" />
</application>

Activity的配置应该尽量支持resizeableActivity。应用需要在 AndroidManifest.xml 文件的 application 或者 actvivity 标签中添加 resizeableActivity=true 的属性。若应用不支持多窗口(例如小窗、分屏等)而声明了resizeableActivity=false,则在折叠屏折叠/展开场景中,可能出现UI布局问题、或者进入到原生的安卓兼容模式(SizeCompatMode)。

<application
    android:resizeableActivity="true">
 
    <activity
          android:resizeableActivity="true" />
</application>

若应用的 targetSDK 为 24 或以上,即便不设置 resizeableActivity 属性,其默认值也为 true。

在设备发生屏幕切换后,应用应能妥善地保存界面状态或者支持配置变更。

  • 保存界面状态

在应用Activity 被系统销毁重走生命周期时,应用需要及时保存和恢复 Activity 的界面状态,应用可以在 onSaveInstanceState 和 onRestoreInstanceState 方法中保存和恢复数据状态:

@Override
public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) { 
   super.onSaveInstanceState(outState, outPersistentState);
} 

@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {    
   super.onRestoreInstanceState(savedInstanceState);
}
  • 支持配置变更

应用也可以支持配置变更,保持 Activity 不重走生命周期,这时需要应用在 AndroidManifest.xml 中配置如下 configChanges 属性:

<activity android:configChanges="screenSize|smallestScreenSize|screenLayout" />

添加 android:configChanges 后,Activity 或者 Fragment 就会收到 onConfigurationChanged() 的回调,此方法会收到传递的Configuration对象,从而指定新设备配置。通过读取

Configuration中的字段确定新配置,然后通过更新界面所用资源进行适当的更改。调用此方法时,Activity 的Resources对象会相应地进行更新,并根据新配置返回资源,以便您在系统不重启 Activity 的情况下轻松重置界面元素。

代码示例:

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {         
   super.onConfigurationChanged(newConfig); 
   Toast.makeText(this, "onConfigurationChanged newWidth="
   + newConfig.screenWidthDp  + ", newHeight=" 
   + newConfig.screenHeightDp, Toast.LENGTH_SHORT).show();
}

MIUI针对折叠屏设备新增了针对app侧的wm_on_configuration_changed_called回调EventLog,结合系统侧configuration_changed回调EventLog日志和”Config changes”调用的SystemLog,可以方便您快速定位、排查与onConfigurationChanged回调有关的问题。

Log示例:

// 系统侧触发configurationChanged事件日志(EventLog)
07-20 12:43:31.521 1000 2677 2778 I configuration_changed: 536874240
// App侧触发configurationChanged回调事件日志(EventLog),4个参数分别代表userId、目标Activity、Configuration、耗时。
07-20 12:43:31.701 10086 5585 5585 I wm_on_configuration_changed_called: [0,com.miui.home.launcher.Launcher,{1.0 ?mcc?mnc [zh_CN] ldltr sw696dp w696dp h738dp 440dpi lrg hdr widecg port ?pc finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1914, 2160) mAppBounds=Rect(0, 0 - 1914, 2116) mMaxBounds=Rect(0, 0 - 1914, 2160) mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=home mAlwaysOnTop=undefined mRotation=ROTATION_0 mInSplitScreen=false} as.2 s.24 fontWeightAdjustment=0 themeChanged=0 themeChangedFlags=0 extraData = Bundle[{}] dt/m/d},96]
// Config changes关键字SystemLog
2022-07-14 09:54:11.891 2485-2638/system_process I/ActivityTaskManager: Config changes=20000d00 {1.0 ?mcc?mnc [zh_CN] ldltr sw696dp w696dp h738dp 440dpi lrg hdr widecg port ?pc finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1914, 2160) mAppBounds=Rect(0, 0 - 1914, 2116) mMaxBounds=Rect(0, 0 - 1914, 2160) mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0 mInSplitScreen=false} as.2 s.8214 fontWeightAdjustment=0 themeChanged=0 themeChangedFlags=0 extraData = Bundle[{}] dt/s/d}
  • 适配调试命令

在对三方应用折叠屏连续性支持情况测试的过程中,我们发现部分应用虽自行处置了配置变更,在AndroidManifest.xml中配置了screenSize|smallestScreenSize|screenLayout等configChanges属性,并回调onConfigurationChanged方法对屏幕做了适配,Activity不会重建,也不会重走生命周期。但应用的Activity仍然存在很多控件显示问题。

为了带给用户良好的使用体验,我们在系统中忽略了这部分app的configChanges属性配置,强制让应用的Activity执行重建,并重走生命周期,以达到界面完美显示的效果,我们将这种行为称为relaunch。

为了方便您在我们的设备上进行适配,我们提供了如下命令帮助您屏蔽上述的relaunch行为。

屏蔽relaunch行为,是我们在经过充分测试评估后,认为您的应用即使执行relaunch,也无法达到连续性适配测试标准,甚至因relaunch重走生命周期,导致折叠展开后亮屏变慢、回到桌面、闪退、ANR等问题,因此屏蔽relaunch行为,旨在屏蔽上述亮屏变慢、回到桌面、闪退、ANR等一系列连续性、稳定性问题。

如果您希望通过relaunch重建Activity生命周期来适配折叠屏连续性,您需要关注上述的连续性、稳定性问题,避免在重建Activity生命周期中执行耗时操作,影响折叠展开时的应用性能、亮屏速率等,并避免在onDestroy里调用finish,以免折叠展开后回到桌面。

通过如下命令,您的应用在没有配置screenSize|smallestScreenSize|screenLayout等configChanges属性的情况下,折叠展开操作时仍能正常回调onConfigurationChanged方法,Activity不会重建,生命周期不会重走,但与此同时,在折叠屏折叠展开操作时可能会有通知提醒您应用显示存在异常,需要重启。您无需理会上述系统通知,它不会对您的适配造成任何影响。

adb shell wm dump -setForceDisplayCompatMode 应用包名 blocklist

上述命令会导致您的应用无法执行relaunch重建,即使您的应用没有配置screenSize|smallestScreenSize|screenLayout等configChanges属性,但您的应用仍能正常回调onConfigurationChanged方法。为了您的应用能自行处置screenSize|smallestScreenSize|screenLayout等configChanges属性,您可以直接使用下面的命令,该命令会将您的应用恢复到Android 原生的处置配置变更的流程,便于您进行适配工作。与此同时,该命令会帮助您屏蔽掉重启通知,以免对您的适配工作造成打扰。

adb shell wm dump -setForceDisplayCompatMode 应用包名 allowlist

如果您希望恢复relaunch状态,可执行如下命令(尽管我们不建议您这么做,仍然希望您能通过configChanges进行适配工作):

adb shell wm dump -setForceDisplayCompatMode 应用包名 relaunchlist

请注意:以上命令行为不会在设备中持久化保存,在设备重启后就会失效。如有需要,您可再次执行上述命令。此外,执行命令后,您需要将您的应用从最近任务移除后重新启动,来保证命令生效。

如您已完成应用连续性适配,可以联系我们,在测试您的应用达到连续性标准后,我们会通过云控的方式将您的应用从relaunch名单中移除,待您的设备收到云控更新后,您的应用将不会再执行relaunch行为。

  • 页面布局适配

Android 10 (API 级别 29) 或更高版本支持更多种宽高比。对于可折叠设备而言,设备类型可以是超长、超薄的屏幕(例如屏幕宽高比为 21:9 的折叠设备),也可以是 1:1 的屏幕,如要与尽可能多的设备兼容,需要尽量多针对以下屏幕宽高比测试自己的应用:

如果无法支持上述某些高宽比,可以使用 maxAspectRatio 以及 minAspectRatio 来指明自己应用可以处理的最高宽高比和最低宽高比,此属性需要在 AndroidManifest.xml 文件的 application 或者 actvivity 标签中添加。例如:

<application    
    android:maxAspectRatio="2.4">     
<activity          
        android:maxAspectRatio="2.4" />
</application>

详细信息请参阅Android开发者指南

小米大屏设备适配说明1

1、概述

1.1 小米大屏设备介绍

1.1.1 折叠屏形态与尺寸

折叠屏是一种柔性屏,可以进行弯曲、折叠,比如上下折叠,左右折叠等。

Xiaomi MIX Fold 和 Xiaomi MIX Fold 2 均为左右内折叠设备,其展开状态和折叠状态屏幕大小和分辨率如下:

外屏内屏
尺寸分辨率尺寸分辨率
Xiaomi MIX Fold6.52英寸2520×8408.01英寸2480×1860
Xiaomi MIX Fold 26.56英寸2520×10808.02英寸2160×1914

判断是否为折叠屏设备的接口为:

//可通过反射调用 SystemProperties 的 persist.sys.muiltdisplay_type 属性值来进行判断
boolean isFoldDisplay = SystemProperties.getInt("persist.sys.muiltdisplay_type", 0) == 2;

折叠/展开态的判断接口为:

// 0 UNKNOWN
// 1 折叠
// 2 半折(MIX Fold 2支持)
// 3 展开
final int posture = Settings.Global.getInt(this.getContentResolver(), "device_posture", 0);

1.1.2 平板设备形态与尺寸

小米平板设备屏幕大小和分辨率信息如下:

尺寸分辨率
小米平板511 英寸2560 x 1600
小米平板5 Pro11 英寸2560 x 1600
小米平板5 Pro 5G11 英寸2560 x 1600
小米平板5 Pro 12.412.4 英寸2560 x 1600

判断是否为平板设备的接口为:

public static boolean isTablet(){    
/**应用需反射调用*/    
     return SystemProperties.get("ro.build.characteristics").contains("tablet");}

应用也可以动态判断当前窗口显示在大屏下:

public static boolean isTablet(Context context) {    
    return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;}

1.1.3 可扩展性

小米大屏设备品类形态未来将会有更多丰富的产品,因此应用适配后需要支持一定程度拉伸。

1.2 文档适用对象

需要适配小米大屏设备应用的第三方开发者、UX设计师、系统工程师、测试

2、UX设计与软件形态适配建议

2.1 基础体验设计

2.1.1 字体

  • 优先调用系统默认字体来保证整台设备的阅读体验一致性,除非有特殊需求;
  • 排版及字号字重应保证清晰可读;
  • 在折叠屏上,建议保持内外屏字号大小一致,展开大屏字号大小如要放大,最大不能超过外屏小屏的1.2倍。

2.1.2 图片、视频

  • 这些资源不应该出现变形、模糊或者显示不全的问题,影响体验;
  • 在折叠屏上,建议图片、视频等信息内容在内外屏采用动态布局,保持图片视频等内容大小一致;展开大屏的图片视频等内容大小如要放大,最大不能超过屏幕高度。

2.1.3 弹窗

  • 弹窗在应用设计中比较常见,如运营内容、广告营销等,建议弹窗大小适中,且有清晰的供用户关闭的按钮;
  • 在折叠屏上,建议弹窗大小内外屏保持一致;展开大屏的弹窗如要放大,最大不能超过屏幕高度的70%。

2.2 页面布局设计

2.2.1 动态布局

  • 大屏设备能够在竖屏和横屏之间无缝切换,折叠屏设备还能够在内屏和外屏之间无缝切换,拥有强大的功能和内容体验。
  • 应用可能会遇到以下几种窗口情况

基于以上情况,推荐几种布局方式

A.拉伸

仅卡片区域、显示区域等拉伸,文字大小图标大小均保持一致

B.缩放

根据窗口区域等比缩放内容信息

C.拓展/增量/减量

根据窗口区域增加或减少项,项的尺寸大小保持不变

D.重新布局

根据窗口区域重新对内容进行布局,以适应更好的信息阅读

小米开发平台 MIX前置摄像头适配说明

小米开发平台 MIX前置摄像头适配说明

1. 背景

由于MIX系列手机的前置摄像头在屏幕右下角,使用前置摄像头需要将手机倒过来,所以需要应用开发者在用到前置相机的页面把相机画面和UI都倒转过来。

2. Activity适配方法

MIX系列手机支持页面180度方向倒转,只需配置sensorPortrait属性即可。

2.1. 直接在AndroidManifest.xml里面配置

示例如下:AndroidManifest配置

<activity android:name=".MainActivity"    android:screenOrientation="sensorPortrait">    <intent-filter>        <action android:name="android.intent.action.MAIN" />        <category android:name="android.intent.category.LAUNCHER" />    </intent-filter></activity>

2.2. 在代码里面配置

代码示例如下:代码配置方法

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);    setContentView(R.layout.activity_main);}

3. Camera Device适配方法

3.1. UI上给用户以适当的提示,引导用户倒置手机

3.2. 结合屏幕显示方向来设置Camera的显示方向

主要是调用Camera中的setDisplayOrientation接口代码配置方法

final void setDisplayOrientation(int degrees);// Set the clockwise rotation of preview display in degrees.

示例:

代码配置方法

public static void setCameraDisplayOrientation(Activity activity,                        int cameraId, android.hardware.Camera camera) {    android.hardware.Camera.CameraInfo info =        new android.hardware.Camera.CameraInfo();    android.hardware.Camera.getCameraInfo(cameraId, info);    int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();    int degrees = 0;    switch (rotation) {        case Surface.ROTATION_0: degrees = 0break;        case Surface.ROTATION_90: degrees = 90break;        case Surface.ROTATION_180: degrees = 180break;        case Surface.ROTATION_270: degrees = 270break;    }    int result;    if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {        result = (info.orientation + degrees) % 360;        result = (360 - result) % 360// compensate the mirror    else // back-facing        result = (info.orientation - degrees + 360) % 360;    }    camera.setDisplayOrientation(result);}


可以在OrientationListener.onOrientationChanged(int orientation)回调中进行check,如果display的rotation与之前不同,则执行上述操作,重新设置Camera的显示方向。

Jpeg拍照方向(通过Camera的Parameters.setRotation设置的rotation)与屏幕是否翻转显示无关,维持原有逻辑即可。

小米开发平台屏幕指纹设备适配说明

小米开发平台屏幕指纹设备适配说明

1. 什么是屏下指纹

小米 8 透明探索版中,我们引入屏下指纹这项新技术,即指纹传感器嵌入屏幕下。

由于是屏下指纹,所以需要出现指纹图标,提示用户指纹传感器的位置,但这样会和部分需要使用指纹验证的业务产生冲突。

2. 屏下指纹相关接口

由于各个开发者的指纹验证逻辑和 UI 样式非常不一样,所以系统不打算统一大家的样式,而是会提供相关接口,由各业务组做针对性地适配。

2.1. 是否为屏下指纹设备

// true means FOD project
PRODUCT_PROPERTY_OVERRIDES += ro.hardware.fp.fod=true

2.2. 指纹传感器的位置、大小(每次指纹验证都需要重新获取)

以下是 Android O 的接口:

//location of FOD sensor's top left corner in pixel, the top left corner of screen is (0,0)
//persist.sys.fp.fod.location.X_Y 表示sensor区域左上角的坐标,以pixel为单位,以物理屏幕左上角为(0,0)计算。
persist.sys.fp.fod.location.X_Y = 453,1640 //sensor的大小(单位也是pixel) persist.sys.fp.fod.size.width_height = 173,173

以下是 Android P 的接口(应谷歌要求,需要加入「vendor」字样)

//location of FOD sensor's top left corner in pixel, the top left corner of screen is (0,0)
// persist.vendor.sys.fp.fod.location.X_Y 表示sensor区域左上角的坐标,以pixel为单位,以物理屏幕左上角为(0,0)计算。
persist.vendor.sys.fp.fod.location.X_Y = 453,1640

//sensor的大小(单位也是pixel)
persist.vendor.sys.fp.fod.size.width_height = 173,173

因为同一款设备,市场上可能会同时存在 Android O 和 Android P 两个版本,所以开发者需要根据 Android 版本调用不同的接口,以获取正确的位置信息。

2.3. 控制指纹图标显示、消失

调用标准的指纹监听接口即可显示指纹图标。

2.4. 指定指纹图标的颜色(黑色或白色)

由于各个业务组的界面不尽相同,我们会提供一个接口,让业务方指定要用「黑色」还是「白色」的指纹图标。

复用原有 flag 中的「0」「1」标志(原flag标志没有使用),0表示黑色,1表示白色。

mFm.authenticate(null, mCancellationSignal, 0, callback, null); // 黑色指纹图标(默认)
mFm.authenticate(null, mCancellationSignal, 1, callback, null); // 白色指纹图标

3. 屏下指纹适配建议

3.1. UI 避开指纹传感器的位置

业务方可以通过上述接口获取指纹图标的位置和大小,建议控件、键盘等元素都避开那个区域。

3.2. 不建议同时支持密码验证和指纹验证

由于指纹图标会挡住键盘,所以不建议开发者同时支持指纹和密码验证,而是做成互斥的模式,类似微信支付。

3.3. 考虑是否需要调整正确、错误的反馈

如果第三方调用,系统不处理指纹验证正确或错误的反馈,交由开发者自行处理。开发者可以根据自身业务逻辑,考虑是否需要针对屏下指纹调整相关反馈。

4. FAQ

4.1. 有几款机型支持屏下指纹

小米机型中,目前支持屏下指纹的设备为「小米 8 透明探索版」和「小米8 屏幕指纹版」。

4.2. 如何测试

拿到设备后,建议同时测试以下两个版本的效果:

  • MIUI 9 稳定版,即出厂自带的版本,截至2018年底仍会是该机型用户的主要版本。
  • MIUI 10 开发版或稳定版,后续 MIUI 10 将成为 MIUI 系统的主流,又因为 System UI 的代码架构在MIUI 10有较多调整,所以建议同时覆盖测试。

顺便附上ROM下载链接如下: