Android 折叠屏适配攻略

1,540 阅读6分钟

一、背景

各个厂商都在发布折叠屏手机,整体折叠屏手机的占有率越来越高。基于此我们于去年推进了折叠屏的适配,本篇整体梳理一下在适配折叠屏过程中,我们做了哪些事情,遇到了哪些坑点。

二、Activity Embedding

首先在Android 12L及以上的版本,推荐使用 Google官方的折叠屏适配方案。即Activity Embedding。目前用于适配折叠屏的window库已经升级到1.3.0版本: androidx.window:window:1.3.0,我们在适配时还是1.1.0-alpha03。接下来简单介绍一下具体的适配步骤。

1、配置

使用Activity嵌入需要引入两个库:

implementation androidx.window:window:1.3.0
implementation androidx.startup:startup-runtime:1.1.1

如果是使用最新的window库需要添加以下配置

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

官方的描述是:

将 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 属性添加到应用清单文件中的 元素,然后将该值设置为 true。 在 WindowManager 版本 1.1.0-alpha06 及更高版本中,除非将该属性添加到清单中并设置为 true,否则系统会停用 activity 嵌入分屏。

由于笔者的工程中使用的是1.1.0-alpha03版本,所以我们工程是没有添加这个配置的。

2、使用XML的方式配置分屏

官方给出的配置

<resources
    xmlns:window="http://schemas.android.com/apk/res-auto">

    <!-- Define a split for the named activities. -->
    <SplitPairRule
        window:splitRatio="0.33"
        window:splitLayoutDirection="locale"
        window:splitMinWidthDp="840"
        window:splitMaxAspectRatioInPortrait="alwaysAllow"
        window:finishPrimaryWithSecondary="never"
        window:finishSecondaryWithPrimary="always"
        window:clearTop="false">
        <SplitPairFilter
            window:primaryActivityName=".ListActivity"
            window:secondaryActivityName=".DetailActivity"/>
    </SplitPairRule>

    <!-- Specify a placeholder for the secondary container when content is
         not available. -->
    <SplitPlaceholderRule
        window:placeholderActivityName=".PlaceholderActivity"
        window:splitRatio="0.33"
        window:splitLayoutDirection="locale"
        window:splitMinWidthDp="840"
        window:splitMaxAspectRatioInPortrait="alwaysAllow"
        window:stickyPlaceholder="false">
        <ActivityFilter
            window:activityName=".ListActivity"/>
    </SplitPlaceholderRule>

    <!-- Define activities that should never be part of a split. Note: Takes
         precedence over other split rules for the activity named in the
         rule. -->
    <ActivityRule
        window:alwaysExpand="true">
        <ActivityFilter
            window:activityName=".ExpandedActivity"/>
    </ActivityRule>

</resources>

具体的字段含义笔者在这里就不详细描述了,否则就和关闭官方文档没有区别了。具体详细的配置大家可以看官方的指引:developer.android.com/guide/topic…

若要使config配置生效需要利用启动库添加配置。在笔者的适配过程中我们利用这个功能分别配置了针对折叠屏与平板的适配:

public class CustomWindowInitializer implements Initializer<SplitController> {

    @NonNull
    @Override
    @androidx.window.core.ExperimentalWindowApi
    public SplitController create(@NonNull Context context) {
        if (DeviceUtils.isTabletDevice()) {
            SplitController.initialize(context, R.xml.split_config_tablet);
        } else {
            SplitController.initialize(context, R.xml.split_config);
        }
        return SplitController.getInstance();
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        return new ArrayList<>();
    }
}

之所以这样配置,是因为我们希望在Android平板上也可以有分屏效果。

split_config 与 split_config_tablet 两个文件不同的,只有 splitRatio 这个字段。在split_config中splitRatio为0.5,意思是1:1,在split_config_tablet中splitRatio为0.33,意味着主副两个屏幕为3:7。

另外就是在PlaceholderActivity中,需要自行处理这个Activity何时关闭。例如当用户将折叠屏关闭时,这个PlaceholderActivity就需要被关闭了。可以在PlaceholderActivity中重写 onConfigurationChanged 方法来触发检测,然后调用SplitController.getInstance().isActivityEmbedded(activity);接口来判断当前是否为折叠屏状态。

三、各个厂商的适配

在上一个章节简单介绍了如何利用Google官方的配置来利用Activity嵌入的方式适配折叠屏,不过官方仅支持Android12L及以上的用户,在Android12以下上面的配置就不生效了。

不过国内的各个厂商早在Google官方出解决方案之前就输出了厂商上适配方案(不知道Google设计方案时,是否参考了国内厂商的方案),在12及以下我们可以使用厂商的适配方案。

各个厂商的适配方案有些不同,这里我们区分不同的厂商介绍。

华为

华为官方称呼这套适配方案为平行视界。 需要先在清单文件中添加配置:

<meta-data android:name="EasyGoClient" android:value="true" />

然后在“assets”目录下新建配置文件“easygo.json”

{
  "easyGoVersion": "1.0",
  "client": "com.huawei.example",
  "logicEntities": [
    {
      "head": {
        "function": "magicwindow",
        "required": "true"
      },
      "body": {
        "mode": "0",
        "defaultDualActivities": {
          "mainPages": "com.huawei.example.Main1Activity",
          "relatedPage": "com.huawei.example.A0Activity"
        },
        "transActivities": [
          "com.huawei.example.A1Activity",
          "com.huawei.example.A2Activity"
        ],
        "Activities": [
          {
            "name": "com.huawei.example.AFullScreenActivity",
            "defaultFullScreen": "true"
          },
          {
            "name": "com.huawei.example.BFullScreenActivity",
            "lockSide": "primary"
          }
        ],
        "UX": {
          "supportRotationUxCompat": "false",
          "isDraggable": "false",
          "supportDraggingToFullScreen": "PAD|FOLD"
        }
      }
    }
  ]
}

字段的解释可以看官方的文档。

需要注意的是在华为手机上适配折叠屏有一些官方没有说明的项需要注意:

  • 添加以上配置后,App不会立即显示为分屏状态,需要进入系统设置中搜索【应用分栏视图】找到对应的应用,开启开关,这样应用才会有分屏效果
  • 这个开关对于绝大部分应用是关闭的,对于购物类或者白名单应用是默认开启的,例如微信就是默认开启的。所以如果你也想你的App是默认开启的,需要联系官方的运营人员沟通

OPPO

OPPO的Find N折叠屏系列也卖的不错,其官方也有输出针对折叠屏的适配。OPPO称其为平行视窗。不过首先OPPO官方或者说整个金标联盟都推荐,在Android12以上的设备有限使用Google 的 Activity 嵌入的方式。然后在低版本再使用厂商的适配规则。

基于此可以在清单文件中添加以下配置,当应用同时接入Activity Embedding与OPPO自研平行视窗时,系统会根据ROM的版本优先支持原生的Activity Embedding,如果ROM版本不支持Activity Embedding,则支持自研的平行视窗。此时应用需要在manifest中添加如下标识,并指定为 false 。

<application>
    <property
      android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
      android:value="false" />
</application>

另外有意思的是OPPO配置平行视窗的方式与华为的完全一样,这里就不赘述了。

vivo

vivo 与OPPO华为又有一些不同。

需要在app 的AndroidManifest.xml 文件中 标签内新增 <meta-data android:name="VivoMultiWindow" android:value="true" />

在 assert 目录下新建配置文件 “vivo_multiwindow.json”,模板示例如下:

{

    "funtouchPlugVersion": "1.1",
    "package": "com.vivo.test",
    "elements": [
        {
            "fun": {
                    "name": "multi-landscape",
                    "enable": "true"
                    },
            "config": {
                    "mode": 1,
                    "action": [
                        {
                            "pages": "com.vivo.test.MainActivity",
                            "attachedPage": "com.vivo.test.primaryActivity1"
                        },
                        {
                            "from": "com.vivo.test.FromActivity1",
                            "to": "com.vivo.test.ToActivity"
                        },
                        {
                            "from": "com.vivo.test.FromActivity2",
                            "to": "*"
                        }
                    ],
                    "Activities": {
                        "fullscreenActivities": [
                            "com.vivo.test.fullScreenActivity1",
                            "com.vivo.test.fullScreenActivity2"
                        ],

                        "transitActivities": [
                            "com.vivo.test.transitionActivity1",
                            "com.vivo.test.transitionActivity2",
                            "com.vivo.test.transitionActivity3"
                            ],
                    },
                    "EX": {
                        "draggable": "true",
                        "statusBarVisible": "true",
                        "videoAutoFullscreen": "true",
                        "isRelaunchForResizing": "false",
                        "supportDoubleResume": "true",
                        "windowsGravity": [
                            {
                                "deviceType": "FOLD",
                                "gravity": "1|2"
                            },
                            {
                                "deviceType": "PAD",
                                "gravity": "900|1654"
                            }
                        ],
                        "splitBarColor": "0xffe5e5e5"
                    }

                }

        }

    ]

}

字段的含义基本与其他几家都差别不大。

小米

在小米的官方文档中没有找到小米自己的适配文档,而是直接介绍了官方的Activity嵌入适配方式。

四、其他的坑

1、华为需要自行开启分屏适配

这点前面有介绍过,华为需要用户自行在设置中找到【应用分栏视图】设置项,选取对应的App进行开始开启。初期由于不清楚,添加了适配逻辑之后一直没有分屏的效果排查了很久,后面翻华为的论坛才看到相关的内容。

2、华为手机修改配置后最好升级版本号

调整本地分屏配置后,可能会出现不生效的情况,此时可以考虑卸载安装,或者升高App的版本号。

3、一些代码上的逻辑调整

添加分屏逻辑之后,需要整体排查应用中关于页面宽度、坐标点位的逻辑。

举例:

  • 之前是通过获取屏幕宽度作为Activity宽度的逻辑,这种逻辑在折叠屏分屏中数据就不对了。
  • 之前使用MotionEvent#getRowX方法获取坐标点,这里折叠屏需要减掉主屏宽度

4、判断OPPO折叠屏的方法

    public static boolean isOPPOFold() {
        boolean isFold = false;
        try {
            Class<?> cls = Class.forName("com.oplus.content.OplusFeatureConfigManager");
            Method instance = cls.getMethod("getInstance");
            Object configManager = instance.invoke(null);
            Method hasFeature = cls.getDeclaredMethod("hasFeature", String.class);
            Object object = hasFeature.invoke(configManager, "oplus.hardware.type.fold");
            if (object instanceof Boolean) {
                isFold = (boolean) object;
            }
        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return isFold;
    }

5、小米判断折叠屏

boolean isFoldDisplay = SystemProperties.getInt("persist.sys.muiltdisplay_type", 0) == 2;

6、vivo判断折叠屏

private static boolean isVivoFoldableDevice(){
    try {
        Class<?>  c= Class.forName("android.util.FtDeviceInfo");
        Method  m = c.getMethod("getDeviceType");
        Object dType = m.invoke(c);
        Log.d("fold","getDeviceType="+dType);
        return "foldable".equals(dType);
    } catch (Exception e) {
        e.printStackTrace();

    }
    return false;
}

参考文档