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
:设备的折叠状态,即FLAT
或HALF_OPENED
orientation
:折叠边或合页的方向,即HORIZONTAL
或VERTICAL
occlusionType
:折叠边或合页是否遮住了显示屏的一部分,即NONE
或FULL
isSeparating
:折叠边或合页是否创建了两个逻辑显示区域,即 true 或 false
注意: 虽然可折叠设备上的合页允许设备折叠到各种角度,但
FoldingFeature
不会在 API 中公开相应角度。不同的设备有不同的报告范围,传感器的准确度也可能会因设备而异;因此,基于精确合页角度的动画或逻辑必须根据设备进行微调。
- 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" />
- 需要文件形式写入需要配置启动属性
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>
- 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>
- 代码中配置 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();
}
}
- 代码中分屏处理等同于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());
}
- 官方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
对折叠开合生命周期的影响Activities
中defaultFullScreen
和lockSide
执行先后对分屏的影响- 华为折叠屏手机分屏前,前往设置
平行世界
打开当前配置项目开关,使功能生效,不生效时需要重启,或者卸载重装,或者增加版本号再重启重装 - vivo折叠屏手机分屏前,前往设置
折叠屏专区>应用多窗口显示
打开当前配置项目开关,使功能生效 - oppo折叠屏没有实践,看文档两者都支持
Resources
、Context
、dimens
对字体布局大小影响。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;
}