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 小时前
Rust移动开发:Rust在Android端集成使用介绍
android·程序人生·rust·kotlin·学习方法
Erorrs4 小时前
Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口
android·java·数据库
东坡大表哥5 小时前
【Android】常见问题集锦
android
ShuQiHere6 小时前
【ShuQiHere】️ 深入了解 ADB(Android Debug Bridge):您的 Android 开发利器!
android·adb
魔法自动机7 小时前
Unity3D学习FPS游戏(9)武器音效添加、创建敌人模型和血条
android·学习·游戏
未来之窗软件服务9 小时前
业绩代码查询实战——php
android·开发语言·php·数据库嵌套
开心呆哥10 小时前
【Android Wi-Fi 操作命令指南】
android·python·pytest
睡觉谁叫10 小时前
一文解秘Rust如何与Java互操作
android·java·flutter·跨平台
----云烟----20 小时前
如何更改Android studio的项目存储路径
android·ide·android studio
YunFeiDong20 小时前
Android Studio打包时不显示“Generate Signed APK”提示信息
android·ide·android studio