1.简介
简单点说就是在一个窗口上显示2个activity,这里学习的是同一个app的不同页面, 其实不同的app也可以同时显示在桌面上,这里不研究
- 官方文档:
developer.android.google.cn/guide/topic...
source.android.google.cn/docs/core/d...
- 效果可以简单理解为一个屏幕上显示2个activity页面,如下图
- 手机目前应该是不支持的,平板,可折叠设备才可能支持。
- 大多数搭载 Android 12L(API 级别 32)及更高版本的大屏幕设备均支持 activity 嵌入。
- activity 嵌入无需重构代码。至于应用如何显示其 activity(是并排还是堆叠)时,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定。
- 简单学习下这个库的用法,最后再看下settings app里如何使用的
2.基本需求
2.1. 库
首先需要这个库
arduino
implementation 'androidx.window:window:1.1.0-beta02'
需要注意的是,引用这个库可能和其他库冲突,自己需要解决冲突,这个库里引用的kotlin库是1.8.10的,所以工程里的kotlin版本改一下
bash
plugins {
//..
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
}
2.2.清单文件
告知系统您的应用已实现 activity 嵌入
将 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED
属性添加到应用清单文件中的 <application> 元素,然后将该值设置为 true
ini
<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>
3.两种规则配置
现在就是添加规则了,有3种规则可以添加,分两种方法:
- 一种是写个xml文件,然后工具类直接解析使用,
- 一种是自己java代码里写配置属性
3.1.xml文件
在res/xml目录下,新建一个资源文件 xxx.xml,内容如下
xml
<?xml version="1.0" encoding="utf-8"?>
<!-- main_split_config.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>
主要有3种规则,下边简单的介绍下作用,里边枚举参数的值,自己可以打开库文件里的res文件values.xml里有
>SplitPairRule
字面意思,分屏配对规则,就是配置下什么条件下可以分屏(比如宽度满足多少dp),如何分屏(左右分屏显示,上下分屏显示),页面关闭规则,主屏和副屏对应的activity是啥
- splitRatio :主屏和分屏页面分割比列,默认0.5
- splitLayoutDirection :屏幕分割方向,可以是左右,也可以是上下,有4种,具体见后边
- splitMinWidthDp:指定两个 activity 在屏幕上同时显示所需的最小显示宽度 (默认600dp)
- splitMaxAspectRatioInPortrait :垂直方向最大宽高比的限制,默认1.4
- splitMaxAspectRatioInLandscape :横屏的时候宽高比最大值限制,默认不限制
- finishPrimaryWithSecondary :指定当辅助容器中的所有 activity 结束时,主要分屏容器中的 activity 是否结束(永不)。
- finishSecondaryWithPrimary :指定当主要容器中的所有 activity 结束时,辅助分屏容器中的 activity 是否结束(始终)。
- clearTop :whether the existing secondary container on top and all activities in it should be destroyed when a new split is created using this rule
>>splitLayoutDirection
locale表示的是系统自己推断当前的方向,是ltr和rtl其中一种
ini
<attr format="enum" name="splitLayoutDirection">
<enum name="locale" value="0"/>
<enum name="ltr" value="1"/>
<enum name="rtl" value="2"/>
<enum name="topToBottom" value="3"/>
<enum name="bottomToTop" value="4"/>
</attr>
>>SplitPairFilter
- primaryActivityName : 表明主屏的activity是哪个
- secondaryActivityName : 表明副屏的activity是哪个
- 可以有多个SplitPairFilter
ini
<SplitPairFilter
window:primaryActivityName=".ListActivity"
window:secondaryActivityName=".DetailActivity"/>
>SplitPlaceholderRule
这个就是副屏页面没有设置的时候,默认显示的activity,配置一般和SplitPairRule一样
- placeholderActivityName :指定一个占位的activity
- window:splitRatio="0.33"
- window:splitLayoutDirection="locale"
- window:splitMinWidthDp="840"
- window:splitMaxAspectRatioInPortrait="alwaysAllow"
- window:stickyPlaceholder="false">
ActivityFilter
指定哪些主屏需要这个占位activity
- window:activityName :主屏的class name
>ActivityRule
这个规则的优先级最高,比如这里配置某个activity A是全屏模式,那么在SplitPairRule里配置的A的分屏模式规则就失效了。
-
alwaysExpand :一般设置为ture,表示指定的activity永远是全屏模式,不进行分屏
-
ActivityFilter :指定哪些activity 需要永远全屏显示。
3.1.1.RuleController
如下,我们可以在application里添加如下代码,解析xml,自动配置好规则
scss
RuleController.getInstance(context).apply {
setRules(RuleController.parseRules(context, R.xml.main_split_config))
}
3.2.代码
和上边的xml对应,有3种rule可以添加
>代码
使用起来比较简单,我们一般写个工具类,完事在自己的application里初始化即可。
kotlin
class MyApplication:Application() {
override fun onCreate() {
super.onCreate()
MyEmbeddingController(this).initRules()
}
>工具类
scss
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.window.embedding.*
//..
class MyEmbeddingController(context: Context) {
private val mContext: Context = context
private val mRuleController: RuleController
init {
mRuleController = RuleController.getInstance(context)
}
fun initRules() {
//这里最好判断下系统是否支持分屏,不支持的话下边的初始化就没啥意义
println("log========support==${SplitController.getInstance(mContext).splitSupportStatus}")
mRuleController.clearRules()
//2个参数,分别是主屏和分屏对应的组件名
val splitPairFilter = SplitPairFilter(ComponentName(mContext, HomeActivity::class.java),
ComponentName(mContext, TestActivity::class.java),
null)
val splitPairFilter2 = SplitPairFilter(ComponentName(mContext, MyActivity::class.java),
ComponentName(mContext, HomeActivity::class.java),
null)
//集合,可以添加多个filter
val filterSet = setOf(splitPairFilter,splitPairFilter2)
//分屏效果配置,type参数指明主屏所占的比列,direction表示布局方向,上下,左右
val splitAttributes: SplitAttributes =
SplitAttributes.Builder().setSplitType(SplitAttributes.SplitType.ratio(0.33f))
.setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT).build()
val splitPairRule =
SplitPairRule.Builder(filterSet)
.setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(600)
.setMinSmallestWidthDp(400)
.setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ratio(1.5f))
.setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
.setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
.setClearTop(false)
.build()
//添加规则
mRuleController.addRule(splitPairRule)
//一个参数,指明要匹配的组件名
val activityFilter = ActivityFilter(ComponentName(mContext, HomeActivity::class.java), null)
val activityFilter2 = ActivityFilter(ComponentName(mContext, MyActivity::class.java), null)
//集合,可以添加多个
val actFilterSet = setOf(activityFilter,activityFilter2)
//builder的第二个参数就是占位activity的intent
val splitPlaceholderRule =
SplitPlaceholderRule.Builder(actFilterSet, Intent(mContext, PlaceholderActivity::class.java))
.setDefaultSplitAttributes(splitAttributes)
.setMinWidthDp(600)
.setMinSmallestWidthDp(400)
.setMaxAspectRatioInLandscape(EmbeddingAspectRatio.ratio(1.5f))
.setSticky(true)
.build()
//添加规则
mRuleController.addRule(splitPlaceholderRule)
val activityFilter3 = ActivityFilter(ComponentName(mContext, MyActivity::class.java), null)
val activityFilterSet = setOf(activityFilter3)
//就一个setAlwaysExpand方法,一般设置为true,表明这些activity需要全屏,不进行分屏
val activityRule =ActivityRule.Builder(activityFilterSet).setAlwaysExpand(true).build()
//添加规则
//mRuleController.addRule(activityRule)
}
}
>SplitPairRule
- equla条件
kotlin
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SplitPairRule) return false
if (!super.equals(other)) return false
if (filters != other.filters) return false
if (finishPrimaryWithSecondary != other.finishPrimaryWithSecondary) return false
if (finishSecondaryWithPrimary != other.finishSecondaryWithPrimary) return false
if (clearTop != other.clearTop) return false
return true
}
>SplitPlaceholderRule
- 这里有个问题,除非是同一个placeholderIntent对象,否则肯定是不一样的。
- Intent没有重写equal方法,所以判断相等与否应该是地址比较,所以如果不是指向同一个对象,那结果都是false
kotlin
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SplitPlaceholderRule) return false
if (!super.equals(other)) return false
if (placeholderIntent != other.placeholderIntent) return false
if (isSticky != other.isSticky) return false
if (finishPrimaryWithPlaceholder != other.finishPrimaryWithPlaceholder) return false
if (filters != other.filters) return false
return true
}
简单点说下就是下边这个是false
csharp
new Intent().equals(new Intent()))
>结论
- SplitPlaceholderRule有可能添加多个数据一样的,因为Intent的比较不相等,所以认为是新的rule
- SplitPairRule 数据一样的,不会重复添加
3.2.1.SplitPairFilter
- SplitPairRule的过滤器,用于查找一对活动是否应该放在分屏中。
- 当新活动从主活动启动时使用它,先判断过滤器是否匹配主屏activity
- 启动新活动的时候判断是否匹配副屏activity,都满足的话就会保存这个过滤器
kotlin
class SplitPairFilter internal constructor(
private val _primaryActivityName: ActivityComponentInfo,
private val _secondaryActivityName: ActivityComponentInfo,
val secondaryActivityIntentAction: String?
)
- secondaryActivityIntentAction :增加一个action,其实就是对副屏组件再增加一个action的判断条件,可为null
3.2.2.ComponentName
有3个构造方法,最终其实都是获取一个包名和class名
- 这里主要说下,第二个参数可以传个"*"表示所有的class
- 也可以是 com.xxx.xxx.ui.*,表示包名的ui目录下的所有activity
less
public ComponentName(@NonNull String pkg, @NonNull String cls) {
if (pkg == null) throw new NullPointerException("package name is null");
if (cls == null) throw new NullPointerException("class name is null");
mPackage = pkg;
mClass = cls;
}
less
public ComponentName(@NonNull Context pkg, @NonNull String cls) {
if (cls == null) throw new NullPointerException("class name is null");
mPackage = pkg.getPackageName();
mClass = cls;
}
less
public ComponentName(@NonNull Context pkg, @NonNull Class<?> cls) {
mPackage = pkg.getPackageName();
mClass = cls.getName();
}
还有2个静态方法获取实例,不带点的其实就是普通的构造方法,带点的会拼接上包名
less
public static @NonNull ComponentName createRelative(@NonNull String pkg, @NonNull String cls) {
final String fullName;
if (cls.charAt(0) == '.') {
// Relative to the package. Prepend the package name.
fullName = pkg + cls;
} else {
// Fully qualified package name.
fullName = cls;
}
return new ComponentName(pkg, fullName);
}
less
public static @NonNull ComponentName createRelative(@NonNull Context pkg, @NonNull String cls) {
return createRelative(pkg.getPackageName(), cls);
}
用法如下
arduino
ComponentName.createRelative("com.xxx.xxx",".ui.*)
//其实就相当于 ComponentName("com.xxx.xxx","com.xxx.xxx.ui.*)
3.2.3.SplitType
主屏和副屏的分割比例
scss
SplitAttributes.SplitType.ratio(0.33f)
默认值是这个
ini
val SPLIT_TYPE_EQUAL = ratio(0.5f)
还有两种类型,以后再看,暂时不清楚啥意思
3.2.3.LayoutDirection
布局方向,有4种,其中LOCALE表示的是LEFT_TO_RIGHT和RIGHT_TO_LEFT其中的一种,具体哪种按照应用当前的方向决定的
ini
companion object {
val LOCALE = LayoutDirection("LOCALE", 0)
val LEFT_TO_RIGHT = LayoutDirection("LEFT_TO_RIGHT", 1)
val RIGHT_TO_LEFT = LayoutDirection("RIGHT_TO_LEFT", 2)
val TOP_TO_BOTTOM = LayoutDirection("TOP_TO_BOTTOM", 3)
val BOTTOM_TO_TOP = LayoutDirection("BOTTOM_TO_TOP", 4)
3.2.4.FinishBehavior
less
companion object {
/** Never finish the associated container. */
@JvmField
val NEVER = FinishBehavior("NEVER", 0)
/**
* Always finish the associated container independent of the current presentation mode.
*/
@JvmField
val ALWAYS = FinishBehavior("ALWAYS", 1)
/**
* Only finish the associated container when displayed adjacent to the one being
* finished. Does not finish the associated one when containers are stacked on top of
* each other.
*/
@JvmField
val ADJACENT = FinishBehavior("ADJACENT", 2)
3.2.5.其他方法
- setMaxAspectRatioInPortrait :垂直方向最大宽高比,比这个大的话,那么取消分屏,默认值 ratio(1.4f)
- setMaxAspectRatioInLandscape :横屏的时候最大宽高比,比这个大的话,取消分屏, 默认值always_allow
- setMinWidthDp :分屏要求的容器窗口的最小宽度,dp值,默认值600dp
- setMinHeightDp: 分屏要求的容器窗口的最小高度,dp值,默认值600dp
- setMinSmallestWidthDp:这个是任意方向的最小宽度的最小值,默认值600dp
scss
//这种方法的参数必须大于1
EmbeddingAspectRatio.ratio(1.6f)
//永远允许,也就是不限制宽高比
val ALWAYS_ALLOW = EmbeddingAspectRatio("ALWAYS_ALLOW", 0f)
//永远不允许,啥宽高比都不允许
val ALWAYS_DISALLOW = EmbeddingAspectRatio("ALWAYS_DISALLOW", -1f)
- setSticky(true) :试了下,姿势不对,没发现效果,以后再补充说明
4.问题记录
.1.startActivityForResult
startActivityForResult 的页面不要用分屏,会出问题的,比如第一次点击跳转黑屏一下,页面跳转成功了,不过显示在底部了,再次点击才出现分屏效果(其实相当于页面打开了2次)
5.settings app
这里看下settings app里如何使用这个库的
5.1.库引用
首先看下settings目录下的Android.bp文件,如下,可以看到引用了window库
arduino
// Build the Settings APK
android_library {
name: "Settings-core",//lib的名字
//..
static_libs: [
//..
"androidx.window_window",
//..
android_app {
name: "Settings",
//..
static_libs: ["Settings-core"], //引用上述lib
5.2.SettingsApplication
在自定义的application里使用自定义的工具类进行分屏规则初始化
scala
public class SettingsApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
final ActivityEmbeddingRulesController controller =
new ActivityEmbeddingRulesController(this);
controller.initRules();
}
5.3.ActivityEmbeddingRulesController
ini
public ActivityEmbeddingRulesController(Context context) {
mContext = context;
mRuleController = RuleController.getInstance(context);
}
>initRules
可以看到,初始化只设置了主页的placeholder activity,以及不需要分屏的页面,并没有设置splitPair,那应该是其他地方设置了?
scss
public void initRules() {
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext)) {
//不支持分屏,就不用初始化了
return;
}
mRuleController.clearRules();
//为homepage页设置 placeholder rule
registerHomepagePlaceholderRule();
//添加一些不需要分屏的activity规则
registerAlwaysExpandRule();
}
>isEmbeddingActivityEnabled
arduino
public static boolean isEmbeddingActivityEnabled(Context context) {
//这个是配置里的设定
final boolean isFlagEnabled = FeatureFlagUtils.isEnabled(context,
FeatureFlagUtils.SETTINGS_SUPPORT_LARGE_SCREEN);
//这个是判断系统是否支持分屏
final boolean isSplitSupported = SplitController.getInstance(context).isSplitSupported();
return isFlagEnabled && isSplitSupported;
}
>FeatureFlagUtils
先判断global setting,再判断Systemproperties,最后是默认配置(这个默认是true)
scss
public static boolean isEnabled(Context context, String feature) {
// Override precedence:
// Settings.Global -> sys.fflag.override.* -> static list
// Step 1: check if feature flag is set in Settings.Global.
String value;
if (context != null) {
value = Settings.Global.getString(context.getContentResolver(), feature);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
}
// Step 2: check if feature flag has any override.
// Flag name: [persist.]sys.fflag.override.<feature>
value = SystemProperties.get(getSystemPropertyPrefix(feature) + feature);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
// Step 3: check if feature flag has any default value.
value = getAllFeatureFlags().get(feature);
return Boolean.parseBoolean(value);
}
>registerHomepagePlaceholderRule
- 分屏模式,homePage页面,右侧默认显示NetworkDashboardActivity页面
scss
private void registerHomepagePlaceholderRule() {
final Set<ActivityFilter> activityFilters = new HashSet<>();
//为下边2个主屏类设置占位activity
addActivityFilter(activityFilters, SettingsHomepageActivity.class);
addActivityFilter(activityFilters, Settings.class);
//这个是占位activity,就是右侧默认显示的页面
final Intent intent = new Intent(mContext, Settings.NetworkDashboardActivity.class);
intent.putExtra(SettingsActivity.EXTRA_IS_SECOND_LAYER_PAGE, true);
SplitAttributes attributes = new SplitAttributes.Builder()
.setSplitType(SplitAttributes.SplitType.ratio(
ActivityEmbeddingUtils.getSplitRatio(mContext)))//0.3636
.build();
final SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder(
activityFilters, intent)
.setMinWidthDp(ActivityEmbeddingUtils.getMinCurrentScreenSplitWidthDp())
.setMinSmallestWidthDp(ActivityEmbeddingUtils.getMinSmallestScreenSplitWidthDp())
.setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
.setSticky(false)
.setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ADJACENT)
.setDefaultSplitAttributes(attributes)
.build();
mRuleController.addRule(placeholderRule);
}
>registerAlwaysExpandRule
指定几个需要全屏显示的页面
scss
private void registerAlwaysExpandRule() {
final Set<ActivityFilter> activityFilters = new HashSet<>();
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlags.SETTINGS_SEARCH_ALWAYS_EXPAND)) {
//搜索页
final Intent searchIntent = FeatureFactory.getFactory(mContext)
.getSearchFeatureProvider()
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent);
}
//指纹相关的几个页面
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
//头像选择页面
addActivityFilter(activityFilters, AvatarPickerActivity.class);
ActivityRule activityRule = new ActivityRule.Builder(activityFilters).setAlwaysExpand(true)
.build();
mRuleController.addRule(activityRule);
}又
5.4.分屏规则添加的地方
- 初始化工具ActivityEmbeddingRulesController只添加了SplitPlaceholderRule以及ActivityRule
- 8.7小节添加了分屏规则
- AvatarViewMixin类里,头像的点击事件里,添加了分屏规则(homepage对account)
- home page menu点击的时候会注册,见5.5
5.5.TopLevelSettings.java
如下,点击的时候会为homepage注册分屏subSetting,而我们跳转的页面都是subSetting
scss
public boolean onPreferenceTreeClick(Preference preference) {
if (isDuplicateClick(preference)) {
return true;
}
// Register SplitPairRule for SubSettings.
//每次preference点击的时候会注册一次,见5.6
ActivityEmbeddingRulesController.registerSubSettingsPairRule(getContext(),
true /* clearTop */);
setHighlightPreferenceKey(preference.getKey());
return super.onPreferenceTreeClick(preference);
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
new SubSettingLauncher(getActivity())
.setDestination(pref.getFragment())
.setArguments(pref.getExtras())
.setSourceMetricsCategory(caller instanceof Instrumentable
? ((Instrumentable) caller).getMetricsCategory()
: Instrumentable.METRICS_CATEGORY_UNKNOWN)
.setTitleRes(-1)
.setIsSecondLayerPage(true)
.launch();
return true;
}
5.6.registerSubSettingsPairRule
- 参考5.5,homepage页面的选项点击跳转的activity都是SubSettings
- 给主屏 Settings,SettingsHomepageActivity 设置副屏SubSettings
- 给主屏 DeepLinkHomepageActivity,DeepLinkHomepageActivityInternal 设置副屏SubSettings
arduino
public static void registerSubSettingsPairRule(Context context, boolean clearTop) {
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
return;
}
registerTwoPanePairRuleForSettingsHome(
context,
new ComponentName(context, SubSettings.class),
null /* secondaryIntentAction */,
clearTop);
registerTwoPanePairRuleForSettingsHome(
context,
COMPONENT_NAME_WILDCARD,//通配符,所有的activity都可以
Intent.ACTION_SAFETY_CENTER,//这里只检查action
clearTop
);
}
public static void registerTwoPanePairRuleForSettingsHome(Context context,
ComponentName secondaryComponent,
String secondaryIntentAction,
boolean clearTop) {
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
return;
}
registerTwoPanePairRuleForSettingsHome(
context,
secondaryComponent,
secondaryIntentAction,
true /* finishPrimaryWithSecondary */,
true /* finishSecondaryWithPrimary */,
clearTop);
}
public static void registerTwoPanePairRuleForSettingsHome(Context context,
ComponentName secondaryComponent,
String secondaryIntentAction,
boolean finishPrimaryWithSecondary,
boolean finishSecondaryWithPrimary,
boolean clearTop) {
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) {
return;
}
registerTwoPanePairRule(
context,
new ComponentName(context, Settings.class),
secondaryComponent,
secondaryIntentAction,
finishPrimaryWithSecondary ? SplitRule.FinishBehavior.ADJACENT
: SplitRule.FinishBehavior.NEVER,
finishSecondaryWithPrimary ? SplitRule.FinishBehavior.ADJACENT
: SplitRule.FinishBehavior.NEVER,
clearTop);
registerTwoPanePairRule(
context,
new ComponentName(context, SettingsHomepageActivity.class),
//...
// We should finish HomePageActivity altogether even if it shows in single pane for all deep
// link cases.
通过deep link 这种打开的,副屏关闭的时候主屏也关闭,FinishBehavior.ALWAYS
registerTwoPanePairRule(
context,
new ComponentName(context, DeepLinkHomepageActivity.class),
secondaryComponent,
secondaryIntentAction,
finishPrimaryWithSecondary ? SplitRule.FinishBehavior.ALWAYS
: SplitRule.FinishBehavior.NEVER,
finishSecondaryWithPrimary ? SplitRule.FinishBehavior.ALWAYS
: SplitRule.FinishBehavior.NEVER,
clearTop);
registerTwoPanePairRule(
context,
new ComponentName(context, DeepLinkHomepageActivityInternal.class),
//...
}
看下图片好说明,点击桌面快捷方式打开的wifi,如下图有个后退箭头的,点击后主屏也一起关闭了。
点击settings桌面图标打开的,如下,副屏是没有箭头的
6.SettingsHomepageActivity
这里测试分屏逻辑,平板横屏的时候有效果,先比较下分屏和普通情况的区别,看图
- 当前展示的preference多了一个高亮显示的效果
- settings那个标题没了,只剩下搜索框显示在顶部
- 背景颜色变了,preference的图标没了,
6.1.清单文件
如下,启动页是这个,这个是别名,目标看 targetActivity属性
ini
<!-- Alias for launcher activity only, as this belongs to each profile. -->
<activity-alias android:name="Settings"
android:label="@string/settings_label_launcher"
android:taskAffinity="com.android.settings.root"
android:launchMode="singleTask"
android:exported="true"
android:targetActivity=".homepage.SettingsHomepageActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity-alias>
6.2.onCreate
scss
static final int DEFAULT_HIGHLIGHT_MENU_KEY = R.string.menu_key_network;
//..
setContentView(R.layout.settings_homepage_container);
mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this);
//横屏模式,第一次进来这个返回的是false,onConfigurationChanged里会变为true
mIsTwoPane = mActivityEmbeddingController.isActivityEmbedded(this);
updateAppBarMinHeight();
initHomepageContainer();
updateHomepageAppBar();
updateHomepageBackground();
mLoadedListeners = new ArraySet<>();
initSearchBarView();
//获取默认高亮显示的menu key,分屏模式才用得到
final String highlightMenuKey = getHighlightMenuKey();
//..
mMainFragment = showFragment(() -> {
//加载fragment
final TopLevelSettings fragment = new TopLevelSettings();
fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
highlightMenuKey);
return fragment;
}, R.id.main_content);
// Launch the intent from deep link for large screen devices.
//打开deep link intent,具体看 8.7
launchDeepLinkIntentToRight();
updateHomepagePaddings();
updateSplitLayout();
}
>onConfigurationChanged
scss
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
final boolean newTwoPaneState = mActivityEmbeddingController.isActivityEmbedded(this);
//这里会重新更新ui
if (mIsTwoPane != newTwoPaneState) {
mIsTwoPane = newTwoPaneState;
updateHomepageAppBar();
updateHomepageBackground();
updateHomepagePaddings();
}
updateSplitLayout();
}
>updateHomepageAppBar
appBar里有两套布局,普通的和分屏模式的,根据当前状态隐藏显示对应的,相关布局见5.4.0以及5.4.1
scss
private void updateHomepageAppBar() {
if (!mIsEmbeddingActivityEnabled) {
return;
}
updateAppBarMinHeight();
if (mIsTwoPane) {
findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.GONE);
findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.VISIBLE);
findViewById(R.id.suggestion_container_two_pane).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.VISIBLE);
findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.GONE);
findViewById(R.id.suggestion_container_two_pane).setVisibility(View.GONE);
}
}
//updateAppBarMinHeight 分屏模式下,上下间距变小了
scss
private void updateAppBarMinHeight() {
final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height);
final int margin = getResources().getDimensionPixelSize(
mIsEmbeddingActivityEnabled && mIsTwoPane
? R.dimen.homepage_app_bar_padding_two_pane //6dp
: R.dimen.search_bar_margin); //16dp
findViewById(R.id.app_bar_container).setMinimumHeight(searchBarHeight + margin * 2);
}
>updateHomepageBackground
修改背景颜色,效果图可以看到,分屏模式背景颜色比较深
scss
private void updateHomepageBackground() {
if (!mIsEmbeddingActivityEnabled) {
return;
}
final Window window = getWindow();
final int color = mIsTwoPane
? getColor(R.color.settings_two_pane_background_color)
: Utils.getColorAttrDefaultColor(this, android.R.attr.colorBackground);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// Update status bar color
window.setStatusBarColor(color);
// Update content background.
findViewById(android.R.id.content).setBackgroundColor(color);
}
>updateHomepagePaddings
scss
private void updateHomepagePaddings() {
if (!mIsEmbeddingActivityEnabled) {
return;
}
if (mIsTwoPane) {
int padding = getResources().getDimensionPixelSize(
R.dimen.homepage_padding_horizontal_two_pane);
//分屏模式rv左右两侧增加了padding
mMainFragment.setPaddingHorizontal(padding);
} else {
mMainFragment.setPaddingHorizontal(0);
}
//设置的是preference的iconPaddingStart以及textPaddingStart
mMainFragment.updatePreferencePadding(mIsTwoPane);
}
>initSearchBarView
主要是给searchbar设置点击要跳转的intent,手机模式和分屏模式是两套布局,所以下边设置了两种
java
private void initSearchBarView() {
final Toolbar toolbar = findViewById(R.id.search_action_bar);
FeatureFactory.getFactory(this).getSearchFeatureProvider()
.initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);
if (mIsEmbeddingActivityEnabled) {
final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane);
FeatureFactory.getFactory(this).getSearchFeatureProvider()
.initSearchToolbar(this /* activity */, toolbarTwoPaneVersion,
SettingsEnums.SETTINGS_HOMEPAGE);
}
}
>updateSplitLayout
- mIsRegularLayout: 默认是true,普通模式,为false(宽度不够)是简洁模式(这里会隐藏图标)
- 判断条件宽度大于380dp,具体见下边isRegularHomepageLayout方法
scss
private void updateSplitLayout() {
if (!mIsEmbeddingActivityEnabled) {
return;
}
if (mIsTwoPane) {
if (mIsRegularLayout == ActivityEmbeddingUtils.isRegularHomepageLayout(this)) {
// 分屏模式下,主屏的宽度符合常规模式,不做特殊处理
return;
}
} else if (mIsRegularLayout) {
// One pane mode with the regular layout, not needed to change
return;
}
//能走到这里说明是非常规模式,默认值是ture,这里取反,为false
mIsRegularLayout = !mIsRegularLayout;
// Update search title padding
View searchTitle = findViewById(R.id.search_bar_title);
if (searchTitle != null) {
int paddingStart = getResources().getDimensionPixelSize(
mIsRegularLayout
? R.dimen.search_bar_title_padding_start_regular_two_pane
: R.dimen.search_bar_title_padding_start);
//修改下start padding
searchTitle.setPaddingRelative(paddingStart, 0, 0, 0);
}
// Notify fragments
getSupportFragmentManager().getFragments().forEach(fragment -> {
if (fragment instanceof SplitLayoutListener) {
//这个具体作用就是隐藏preference左侧的图标
((SplitLayoutListener) fragment).onSplitLayoutChanged(mIsRegularLayout);
}
});
}
scss
public void onSplitLayoutChanged(boolean isRegularLayout) {
iteratePreferences(preference -> {
if (preference instanceof HomepagePreferenceLayout) {
//可以看到,常规模式图标才可见
((HomepagePreferenceLayout) preference).getHelper().setIconVisible(isRegularLayout);
}
});
}
>isRegularHomepageLayout
判断条件,宽度大于380dp,这里需要注意一下,分屏模式下,主屏的宽度不是屏幕的宽度,而是屏幕宽度乘以规则里setSplitType规定的那个ratio(这里是0.3636)
java
public static boolean isRegularHomepageLayout(Activity activity) {
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
return dm.widthPixels >= (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, MIN_REGULAR_HOMEPAGE_LAYOUT_WIDTH_DP, dm);
}
6.3.settings_homepage_container.xml
ini
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/settings_homepage_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/main_content_scrollable_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.android.settings.widget.HomepageAppBarScrollingViewBehavior">
<LinearLayout
android:id="@+id/homepage_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/suggestion_container_two_pane"
layout="@layout/suggestion_container_two_pane"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/contextual_cards_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/contextual_card_side_margin"
android:layout_marginEnd="@dimen/contextual_card_side_margin"/>
<FrameLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:touchscreenBlocksFocus="false"
android:keyboardNavigationCluster="false">
<LinearLayout
android:id="@+id/app_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="@+id/homepage_app_bar_regular_phone_view"
layout="@layout/settings_homepage_app_bar_regular_phone_layout"/>
<include
android:id="@+id/homepage_app_bar_two_pane_view"
layout="@layout/settings_homepage_app_bar_two_pane_layout"
android:visibility="gone"/>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
6.4.appBar里的2种布局
手机模式以及分屏模式
>settings_homepage_app_bar_regular_phone_layout
这个是手机模式的,线性布局,
- 第一行右侧显示用户头像(不一定会显示,有判断条件)
- 第二行显示标题
- 第三行是一些提示内容,默认是空的,有条件显示
- 第四行就是search bar了
ini
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:orientation="vertical">
<ImageView
android:id="@+id/account_avatar"
android:layout_width="@dimen/avatar_length"
android:layout_height="@dimen/avatar_length"
android:layout_marginTop="@dimen/avatar_margin_top"
android:layout_marginEnd="@dimen/avatar_margin_end"
android:layout_gravity="end"
android:visibility="invisible"
android:accessibilityTraversalAfter="@id/homepage_title"
android:contentDescription="@string/search_bar_account_avatar_content_description"/>
<TextView
android:id="@+id/homepage_title"
android:text="@string/settings_label"
style="@style/HomepageTitleText"/>
<FrameLayout
android:id="@+id/suggestion_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<include layout="@layout/search_bar"/>
</LinearLayout>
>settings_homepage_app_bar_two_pane_layout
分屏模式下,水平方向,左侧是搜索框,右侧是用户头像(不一定显示)
ini
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/app_bar_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/homepage_app_bar_margin_horizontal_two_pane"
android:padding="@dimen/homepage_app_bar_padding_two_pane"
android:orientation="horizontal"
android:background="@drawable/homepage_app_bar_background">
<include layout="@layout/search_bar_two_pane_version"/>
<ImageView
android:id="@+id/account_avatar_two_pane_version"
android:layout_width="@dimen/avatar_length"
android:layout_height="@dimen/avatar_length"
android:layout_gravity="center"
android:contentDescription="@string/search_bar_account_avatar_content_description"/>
</LinearLayout>
>头像显示与否
默认是false,要显示的话改为true即可
arduino
public static boolean isAvatarSupported(Context context) {
if (!context.getResources().getBoolean(R.bool.config_show_avatar_in_homepage)) {
return false;
}
return true;
}
7.打开副屏页,主屏自动打开
7.1.wifi快捷键
比如下边这个是打开wifi的快捷方式的intent定义,正常来讲启动这个intent应该是打开一个wifi页面,可是在可以分屏的情况下,我们左侧还会同时打开小节6的homepage页面,并高亮显示wifi条目,如何做到的?
>shortcut
ini
<shortcut
android:shortcutId="manifest-shortcut-wifi"
android:icon="@drawable/ic_shortcut_wireless"
android:shortcutShortLabel="@string/wifi_settings" >
<intent android:action="android.settings.WIFI_SETTINGS" />
</shortcut>
>清单文件
ini
<activity
android:name="Settings$WifiSettingsActivity"
android:label="@string/wifi_settings"
android:icon="@drawable/ic_homepage_network"
android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter android:priority="1">
<action android:name="android.settings.WIFI_SETTINGS"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.network.NetworkProviderSettings"/>
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_network"/>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true"/>
</activity>
>WifiSettingsActivity
scala
public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
7.2.battery
>shortcut
ini
<shortcut
android:shortcutId="manifest-shortcut-battery"
android:icon="@drawable/ic_shortcut_battery"
android:shortcutShortLabel="@string/power_usage_summary_title" >
<intent android:action="android.intent.action.POWER_USAGE_SUMMARY" />
</shortcut>
>清单文件
ini
<activity
android:name="Settings$PowerUsageSummaryActivity"
android:label="@string/power_usage_summary_title"
android:exported="true"
android:icon="@drawable/ic_homepage_battery">
<intent-filter android:priority="1">
<action android:name="android.intent.action.POWER_USAGE_SUMMARY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter android:priority="51">
<action android:name="android.intent.action.MAIN" />
<category android:name="com.android.settings.SHORTCUT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.fuelgauge.batteryusage.PowerUsageSummary" />
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_battery"/>
</activity>
>PowerUsageSummaryActivity
scala
public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ }
7.3.DataUsage
>shortcut
ini
<shortcut
android:shortcutId="manifest-shortcut-data-usage"
android:icon="@drawable/ic_shortcut_data_usage"
android:shortcutShortLabel="@string/data_usage_summary_title">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="com.android.settings"
android:targetClass="com.android.settings.Settings$DataUsageSummaryActivity" />
</shortcut>
>清单文件
ini
<activity
android:name="Settings$DataUsageSummaryActivity"
android:label="@string/data_usage_summary_title"
android:exported="true"
android:icon="@drawable/ic_homepage_data_usage">
<intent-filter android:priority="1">
<action android:name="android.settings.DATA_USAGE_SETTINGS" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter android:priority="3">
<action android:name="android.intent.action.MAIN" />
<category android:name="com.android.settings.SHORTCUT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.datausage.DataUsageSummary" />
<meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
android:value="@string/menu_key_network"/>
</activity>
>DataUsageSummaryActivity
scala
public static class DataUsageSummaryActivity extends SettingsActivity { /* empty */ }
可以上到,上边启动的3个activity都是SettingsActivity的空实现,所以先研究下SettingsActivity的启动流程
8.SettingsActivity
- 在settings类里有上百个继承SettingsAcitity的静态类。
- 这些类的清单文件里,都有meta-data属性。
- name为com.android.settings.FRAGMENT_CLASS的,其value值就是activity内部要加载的Fragment
- name为com.android.settings.HIGHLIGHT_MENU_KEY(这个name不一定有,大部分是有的),其value值就是homepage要高亮显示的menu key
8.1.onCreate
小节 7里的3种activity,最终会走onCreate里的if方法,跳转到home页面,关闭当前页面,在home页面再次打开相关的页面
scss
protected void onCreate(Bundle savedState) {
//这个很重要,先解析meta data 获取要加载的fragment以及高亮显示的menu key
getMetaData();
//注意,这个getIntent方法重写了,不看的话你会奇怪它里边咋多了很多值
final Intent intent = getIntent();
if (shouldShowTwoPaneDeepLink(intent) && tryStartTwoPaneDeepLink(intent)) {
finish();
//小节7里的几个快捷方式会走这里,具体逻辑见 8.3之后
super.onCreate(savedState);
return;
}
super.onCreate(savedState);
//通过intent加载ui
createUiFromIntent(savedState, intent);
}
>getMetaData
ini
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
//meta data为null的话就不用解析了
if (ai == null || ai.metaData == null) return;
//读取我们要的2个值
mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
mHighlightMenuKey = ai.metaData.getString(META_DATA_KEY_HIGHLIGHT_MENU_KEY);
}
}
>getIntent
ini
public Intent getIntent() {
Intent superIntent = super.getIntent();
String startingFragment = getStartingFragmentClass(superIntent);
if (startingFragment != null) {
//创建一个新的intent,赋值旧intent的所有数据
Intent modIntent = new Intent(superIntent);
//要启动的fragment,正常情况就是meta data里解析的那个
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
Bundle args = superIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
if (args != null) {
args = new Bundle(args);
} else {
args = new Bundle();
}
//把原本的intent放到bundle里了
args.putParcelable("intent", superIntent);
modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
return modIntent;
}
return superIntent;
}
>getStartingFragmentClass
typescript
private String getStartingFragmentClass(Intent intent) {
//正常走这里,我们在getMetaData里应该能获取到这个值
if (mFragmentClass != null) return mFragmentClass;
//..
return intentClass;
}
8.2.createUiFromIntent
scss
protected void createUiFromIntent(Bundle savedState, Intent intent) {
//..
// Getting Intent properties can only be done after the super.onCreate(...)
final String initialFragmentName = getInitialFragmentName(intent);
//..
setContentView(R.layout.settings_main_prefs);
getSupportFragmentManager().addOnBackStackChangedListener(this);
if (savedState != null) {
//..
} else {
//加载fragment
launchSettingFragment(initialFragmentName, intent);
}
//判断下是否需要显示后退键
final boolean isActionBarButtonEnabled = isActionBarButtonEnabled(intent);
final ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(isActionBarButtonEnabled);
actionBar.setHomeButtonEnabled(isActionBarButtonEnabled);
actionBar.setDisplayShowTitleEnabled(true);
}
//这个是一个自定义的swtich bar,
mMainSwitch = findViewById(R.id.switch_bar);
// see if we should show Back/Next buttons
if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
//这里一堆是系统首次启动的时候设置页面,底部的几个按钮, 上一步,下一步,跳过等。
}
}
>launchSettingFragment
scss
void launchSettingFragment(String initialFragmentName, Intent intent) {
if (initialFragmentName != null) {
//正常应该走这里
setTitleFromIntent(intent);
Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true,
mInitialTitleResId, mInitialTitle);
} else {
// Show search icon as up affordance if we are displaying the main Dashboard
mInitialTitleResId = R.string.dashboard_title;
switchToFragment(TopLevelSettings.class.getName(), null /* args */, false,
mInitialTitleResId, mInitialTitle);
}
}
>switchToFragment
就是根据传入的fragmentName,实例化以后进行加载
scss
private void switchToFragment(String fragmentName, Bundle args, boolean validate,
int titleResId, CharSequence title) {
//..
Fragment f = Utils.getTargetFragment(this, fragmentName, args);
if (f == null) {
return;
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
//加载fragment
transaction.replace(R.id.main_content, f);
if (titleResId > 0) {
transaction.setBreadCrumbTitle(titleResId);
} else if (title != null) {
transaction.setBreadCrumbTitle(title);
}
transaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
}
8.3.shouldShowTwoPaneDeepLink
对于3个快捷方式,这里的结果是true
kotlin
private boolean shouldShowTwoPaneDeepLink(Intent intent) {
if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) {
return false;
}
//如果直接打开这个activity,那么isTaskRoot就是true,不满足
if (!isTaskRoot() && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
return false;
}
//快捷方式那3个的action都不是null
if (intent.getAction() == null) {
return false;
}
ActivityInfo info = intent.resolveActivityInfo(getPackageManager(),
PackageManager.MATCH_DEFAULT_ONLY);
//launch mode 是默认值 0
if (info.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
//单例模式不允许分屏,会返回
return false;
}
if (intent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) {
// Slice deep link starts the Intent using SubSettingLauncher. Returns true to show
// 2-pane deep link.
return true;
}
if (isSubSettings(intent)) {
return false;
}
if (intent.getBooleanExtra(SettingsHomepageActivity.EXTRA_IS_FROM_SETTINGS_HOMEPAGE,
/* defaultValue */ false)) {
return false;
}
if (TextUtils.equals(intent.getAction(), Intent.ACTION_CREATE_SHORTCUT)) {
return false;
}
//最终走到这里的
return true;
}
8.4.tryStartTwoPaneDeepLink
java
private boolean tryStartTwoPaneDeepLink(Intent intent) {
intent.putExtra(EXTRA_INITIAL_CALLING_PACKAGE, PasswordUtils.getCallingAppPackageName(
getActivityToken()));
final Intent trampolineIntent;
if (intent.getBooleanExtra(EXTRA_IS_FROM_SLICE, false)) {
//..
} else {
//走这里
trampolineIntent = getTrampolineIntent(intent, mHighlightMenuKey);
}
try {
if (userInfo.isManagedProfile()) {
} else {
//..
startActivity(trampolineIntent);
}
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
8.5.getTrampolineIntent
- 对大屏设备使用,返回一个带深层链接的跳床性质的intent
- 简单举例说明下,比如当前页面的intent是A,完事我们创建一个新的intent B,把intentA的信息封装在B里边,之后跳到B以后,在B里边再打开intent A
scss
public static Intent getTrampolineIntent(Intent intent, String highlightMenuKey) {
final Intent detailIntent = new Intent(intent);
// Guard against the arbitrary Intent injection.
if (detailIntent.getSelector() != null) {
detailIntent.setSelector(null);
}
// 根据这个action可以搜到8.6的信息
final Intent trampolineIntent = new Intent(ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)
.setPackage(Utils.SETTINGS_PACKAGE_NAME)
.replaceExtras(detailIntent);
// Relay detail intent data to prevent failure of Intent#ParseUri.
// If Intent#getData() is not null, Intent#toUri will return an Uri which has the scheme of
// Intent#getData() and it may not be the scheme of an Intent.
trampolineIntent.putExtra(
SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA,
detailIntent.getData());
detailIntent.setData(null);
trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI,
detailIntent.toUri(Intent.URI_INTENT_SCHEME));
trampolineIntent.putExtra(EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
highlightMenuKey);
trampolineIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
return trampolineIntent;
}
8.6.deep intent
ini
<!-- Activity for launching deep link page in 2-pane. -->
<activity android:name=".homepage.DeepLinkHomepageActivity"
android:label="@string/settings_label_launcher"
android:theme="@style/Theme.Settings.Home"
android:launchMode="singleTask"
android:exported="true"
android:enabled="false"
android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|smallestScreenSize"
android:permission="android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK">
<intent-filter>
<action android:name="android.settings.SETTINGS_EMBED_DEEP_LINK_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
//
scala
public class DeepLinkHomepageActivity extends SettingsHomepageActivity {
}
8.7.launchDeepLinkIntentToRight
回到SettingsHomepageActivity的这个方法重新看看
- 主要看下默认添加的分屏规则
scss
private void launchDeepLinkIntentToRight() {
if (!mIsEmbeddingActivityEnabled) {
return;
}
final Intent intent = getIntent();
//下边一堆intent的判断就不看了,就是8.4里设置的各种参数,这里验证下,确定是deep link的intent
if (intent == null || !TextUtils.equals(intent.getAction(),
ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) {
return;
}
if (!(this instanceof DeepLinkHomepageActivity
|| this instanceof DeepLinkHomepageActivityInternal)) {
Log.e(TAG, "Not a deep link component");
finish();
return;
}
final String intentUriString = intent.getStringExtra(
EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI);
if (TextUtils.isEmpty(intentUriString)) {
finish();
return;
}
final Intent targetIntent;
try {
targetIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException e) {
finish();
return;
}
final ComponentName targetComponentName = targetIntent.resolveActivity(getPackageManager());
if (targetComponentName == null) {
finish();
return;
}
ActivityInfo targetActivityInfo = null;
try {
targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName,
/* flags= */ 0);
} catch (PackageManager.NameNotFoundException e) {
finish();
return;
}
int callingUid = -1;
try {
callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken());
} catch (RemoteException re) {
finish();
return;
}
if (!hasPrivilegedAccess(callingUid, targetActivityInfo)) {
if (!targetActivityInfo.exported) {
finish();
return;
}
if (!isCallingAppPermitted(targetActivityInfo.permission)) {
finish();
return;
}
}
targetIntent.setComponent(targetComponentName);
// To prevent launchDeepLinkIntentToRight again for configuration change.
intent.setAction(null);
targetIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
targetIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
// Sender of intent may want to send intent extra data to the destination of targetIntent.
targetIntent.replaceExtras(intent);
targetIntent.putExtra(EXTRA_IS_FROM_SETTINGS_HOMEPAGE, true);
targetIntent.putExtra(SettingsActivity.EXTRA_IS_FROM_SLICE, false);
targetIntent.setData(intent.getParcelableExtra(
SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA));
// Only allow FLAG_GRANT_READ/WRITE_URI_PERMISSION if calling app has the permission to
// access specified Uri.
int uriPermissionFlags = targetIntent.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (targetIntent.getData() != null
&& uriPermissionFlags != 0
&& checkUriPermission(targetIntent.getData(), /* pid= */ -1, callingUid,
uriPermissionFlags) == PackageManager.PERMISSION_DENIED) {
Log.e(TAG, "Calling app must have the permission to access Uri and grant permission");
finish();
return;
}
//这里添加了分屏规则
// Set 2-pane pair rule for the deep link page.
ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
new ComponentName(getApplicationContext(), getClass()),
targetComponentName,
targetIntent.getAction(),
SplitRule.FinishBehavior.ALWAYS,
SplitRule.FinishBehavior.ALWAYS,
true /* clearTop */);
ActivityEmbeddingRulesController.registerTwoPanePairRule(this,
new ComponentName(getApplicationContext(), Settings.class),
targetComponentName,
targetIntent.getAction(),
SplitRule.FinishBehavior.ALWAYS,
SplitRule.FinishBehavior.ALWAYS,
true /* clearTop */);
//启动原本的activity
final UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class);
if (user != null) {
startActivityAsUser(targetIntent, user);
} else {
startActivity(targetIntent);
}
}
9.总结
- 学习如何在支持分屏的设备上,让app支持分屏
- 分屏规则有3种,可以通过xml设置,也可以通过代码添加
- 简单研究下settings app里如何添加分屏规则的,主要就是小节 5.5里添加的
- 学习了SettingsActivity里添加deep link的逻辑,关闭当前页面A,跳转到home page,再重新打开页面A,展示分屏效果