Android 折叠屏实践

Android 折叠屏实践

分两种模式介绍, EasyGo与官方Activity Embedding,而EasyGo在实践过程又存在差异。

第一种官方提供的分屏

  • 借助 Jetpack WindowManager 库,应用开发者可为新的设备外形规格和多窗口环境提供支持。该库为 API 版本 14 及更高版本提供通用的 API 接口。初始版本以可折叠设备为目标,不过未来版本将支持更多屏幕类型和窗口功能。
scss 复制代码
dependencies {
    implementation("androidx.window:window:1.2.0")

    // For Java-friendly APIs to register and unregister callbacks
    implementation("androidx.window:window-java:1.2.0")

    // For RxJava2 integration
    implementation("androidx.window:window-rxjava2:1.2.0")

    // For RxJava3 integration
    implementation("androidx.window:window-rxjava3:1.2.0")

    // For testing
    implementation("androidx.window:window-testing:1.2.0")
}
  • 让应用具备折叠感知能力 Jetpack WindowManager 中的 WindowInfoTracker 接口会公开窗口布局信息。该接口的 windowLayoutInfo() 方法会返回一个 WindowLayoutInfo 数据流,该数据流会将可折叠设备的折叠状态告知您的应用。WindowInfoTracker getOrCreate() 方法会创建一个 WindowInfoTracker 实例。

    Jetpack WindowManager 的 WindowLayoutInfo 类会以 DisplayFeature 元素列表的形式提供显示窗口的功能。 FoldingFeature 是一种 DisplayFeature,它提供了有关可折叠设备显示屏的信息,其中包括:

    • state:设备的折叠状态,即 FLATHALF_OPENED
    • orientation:折叠边或合页的方向,即 HORIZONTALVERTICAL
    • occlusionType:折叠边或合页是否遮住了显示屏的一部分,即 NONEFULL
    • isSeparating:折叠边或合页是否创建了两个逻辑显示区域,即 true 或 false

注意: 虽然可折叠设备上的合页允许设备折叠到各种角度,但 FoldingFeature 不会在 API 中公开相应角度。不同的设备有不同的报告范围,传感器的准确度也可能会因设备而异;因此,基于精确合页角度的动画或逻辑必须根据设备进行微调。

  1. AndroidManifest.xml配置分屏属性
xml 复制代码
<property
    android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
    android:value="true" />
 <!-- The app itself supports activity embedding, so a system override is not needed. -->
 <!-- 当应用同时接入Activity Embedding与平行视窗时,系统会根据ROM的版本优先支持原生的Activity Embedding,如果ROM版本不支持Activity Embedding,则支持自研的平行视窗。-->
<property
    android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
    android:value="false" />
  1. 需要文件形式写入需要配置启动属性
xml 复制代码
<provider android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- Make SplitInitializer discoverable by InitializationProvider. -->
    <meta-data android:name="${applicationId}.SplitInitializer"
        android:value="androidx.startup" />
</provider>
  1. res/xml/main_split_config.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources
    xmlns:window="http://schemas.android.com/apk/res-auto">

    <!-- 单独处理需要分屏页面,可多组Define a split for the named activities. -->
<!--    <SplitPairRule-->
<!--        window:splitRatio="0.5"-->
<!--        window:splitLayoutDirection="locale"-->
<!--        window:splitMinWidthDp="600"-->
<!--        window:splitMaxAspectRatioInPortrait="alwaysAllow"-->
<!--        window:finishPrimaryWithSecondary="never"-->
<!--        window:finishSecondaryWithPrimary="always"-->
<!--        window:clearTop="false">-->
<!--        <SplitPairFilter-->
<!--            window:primaryActivityName=".MainActivity"-->
<!--            window:secondaryActivityName=".PlaceholderActivity"/>-->
<!--    </SplitPairRule>-->

    <!-- 默认分屏页面 Specify a placeholder for the secondary container when content is
         not available. -->
    <!-- 0.5对半分,local分屏朝向一般左右,600dp折叠大屏与正常屏宽度分水岭  -->
    <SplitPlaceholderRule
        window:placeholderActivityName=".PlaceholderActivity"
        window:splitRatio="0.5"
        window:splitLayoutDirection="locale"
        window:splitMinWidthDp="600"
        window:splitMaxAspectRatioInPortrait="alwaysAllow"
        window:stickyPlaceholder="true">
        <ActivityFilter
            window:activityName=".ActivityAActivity"/>
    </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=".MainActivity"/>
    </ActivityRule>

</resources>
  1. 代码中配置 SplitInitializer
less 复制代码
public class SplitInitializer implements Initializer<RuleController> {

    @NonNull
    @Override
    public RuleController create(@NonNull Context context) {
        RuleController ruleController = RuleController.getInstance(context);
        ruleController.setRules(
                RuleController.parseRules(context, R.xml.main_split_config)
        );
        return ruleController;
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        return Collections.emptyList();
    }
}
  1. 代码中分屏处理等同于main_split_config.xml
scss 复制代码
    RuleController rule = RuleController.getInstance(context);
//            rule.clearRules();
            Set<SplitPairFilter> pairFilters = new HashSet<>();
            SplitPairFilter filter = new SplitPairFilter(new ComponentName(context, context.getClass()),
                    new ComponentName(context.getPackageName(), "*"),
                    null);
            pairFilters.add(filter);
            SplitPairRule pairRule = new SplitPairRule(pairFilters, new SplitAttributes
                    .Builder()
                    .setSplitType(isFullScreen ? SplitAttributes.SplitType.SPLIT_TYPE_EXPAND : SplitAttributes.SplitType.SPLIT_TYPE_EQUAL)
                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE).build(),
                    context.getClass().getName(),
                    SplitRule.FinishBehavior.NEVER,
                    SplitRule.FinishBehavior.NEVER,
                    false,
                    SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT,
                    SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT,
                    SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT, EmbeddingAspectRatio.ALWAYS_ALLOW, EmbeddingAspectRatio.ALWAYS_ALLOW);
            rule.addRule(pairRule);
less 复制代码
RuleController rule = RuleController.getInstance(context);
if (rule.getRules() != null && !rule.getRules().isEmpty()) {
    for (EmbeddingRule ruleRule : rule.getRules()) {
        if (ruleRule instanceof SplitPairRule) {
            SplitPairRule r = (SplitPairRule) ruleRule;
            for (SplitPairFilter rFilter : r.getFilters()) {
                Log.e("TAG", context.getClass().getName() + ",TAG>>>>" + rFilter.getPrimaryActivityName().getClassName() + "," + rFilter.getSecondaryActivityName().getClassName());
                if (rFilter.getPrimaryActivityName().getClassName().contains(context.getClass().getName())) {
                      rule.removeRule(r);
                    break;
                }
            }

        }

    }
}
scss 复制代码
Set<ActivityFilter> pairFilters = new HashSet<>();
ActivityFilter filter = new ActivityFilter(new ComponentName(context, filterActivity.getClass()), null);
pairFilters.add(filter);
Intent defTarget = new Intent(context, previewActivity.getClass());
defTarget.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule(context.getClass().getSimpleName(), pairFilters, defTarget, false, SplitRule.FinishBehavior.ADJACENT, SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT, SplitRule.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW, SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT, EmbeddingAspectRatio.ALWAYS_ALLOW, EmbeddingAspectRatio.ALWAYS_ALLOW, new SplitAttributes.Builder().setSplitType(SplitAttributes.SplitType.SPLIT_TYPE_EQUAL).setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE).build());
RuleController rule = RuleController.getInstance(context);
if (null == rule.getRules() || rule.getRules().isEmpty() || !rule.getRules().contains(splitPlaceholderRule)) {
    rule.addRule(splitPlaceholderRule);
} else {
    Log.e("TAG", "exist SplitPlaceholderRule activity" + context.getClass().getName());
}
  1. 官方EMBEDDING分屏效果

分屏时注意 android:launchMode="singleTask" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen|fontScale|uiMode" startActivityForResult

第二种EasyGo分屏方式

  • 清单文件配置AndroidManifest.xml
ini 复制代码
<meta-data
    android:name="EasyGoClient"
    android:value="true" />
  • assets文件中新建easygo.json分屏配置文件,模板如下
css 复制代码
{
  "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"
        }
      }
    }
  ]
}
  • 点击查看Easygo配置指南,注意项
    • launchMode启动模式对配置影响,推荐使用singleTask,留意singleInstance在前后推屏的断层
    • easyGoVersion版本号对配置的影响
    • configChanges对折叠开合生命周期的影响
    • ActivitiesdefaultFullScreenlockSide执行先后对分屏的影响
    • 华为折叠屏手机分屏前,前往设置平行世界打开当前配置项目开关,使功能生效,不生效时需要重启,或者卸载重装,或者增加版本号再重启重装
    • vivo折叠屏手机分屏前,前往设置折叠屏专区>应用多窗口显示打开当前配置项目开关,使功能生效
    • oppo折叠屏没有实践,看文档两者都支持
    • ResourcesContextdimens对字体布局大小影响。res.updateConfiguration(configuration,res.getDisplayMetrics())createConfigurationContext(configuration).getResources()
    • 获取Activity是否运行在分屏状态的接口
ini 复制代码
//华为
String config = context.getResources().getConfiguration().toString();
boolean isInMagicWindow = config.contains("hw-magic-windows");
context为Activity的context
java 复制代码
//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;
}
ini 复制代码
//oppo
public static boolean isOPPOTablet() {
    boolean isTablet = 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.tablet");
        if (object instanceof Boolean) {
            isTablet = (boolean) object;
        }
    } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        e.printStackTrace();
    }

    return isTablet;
}
相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android