在之前的Android車載應用開發(fā)與分析(1) - Android Automotive概述與編譯中了解了如何下載以及編譯面向車載IVI的Android系統(tǒng),一切順利的話,運行模擬器,等待啟動動畫播放完畢后,我們所能看到的第一個APP就是車載android的桌面,而這就是本篇文章的重點 - CarLauncher。
本篇文章以解析Android 11 源碼中CarLauncher
為主。為了便于閱讀源碼,現(xiàn)將CarLauncher
的源碼整理成可以導入Android Studio的結構,源碼地址:https://github.com/linux-link/CarLauncher。由于CarLauncher
對于源碼存在依賴,該項目不能直接運行,引入jar依賴的方式也不完全正確,僅供閱讀使用。
本篇文章中的功能以及源碼分析基于android-11.0.0_r43,CarLauncher
源碼位于 packages/apps/Car/Launcher
1.Launcher 與 CarLauncher
Launcher是安卓系統(tǒng)中的桌面啟動器,安卓系統(tǒng)的桌面UI統(tǒng)稱為Launcher。Launcher是安卓系統(tǒng)中的主要程序組件之一,安卓系統(tǒng)中如果沒有Launcher就無法啟動安卓桌面,Launcher出錯的時候,安卓系統(tǒng)會出現(xiàn)“進程 com.android.launcher 意外停止”的提示窗口。這時需要重新啟動Launcher。
《百度百科 - launcher》
Launcher
是android系統(tǒng)的桌面,是用戶接觸到的第一個帶有界面的APP。它本質上就是一個系統(tǒng)級APP,和普通的APP一樣,它界面也是在Activity上繪制出來的。
雖然Launcher
也是一個APP,但是它涉及到的技術點卻比一般的APP要多。CarLauncher作為IVI系統(tǒng)的桌面,需要顯示系統(tǒng)中所有用戶可用app的入口,顯示最近用戶使用的APP,同時還需要支持在桌面上動態(tài)顯示如地圖、音樂在內各個APP內部的信息,在桌面顯示地圖并與之進行簡單的交互。地圖開發(fā)的工作量極大,Launcher
顯然不可能引入地圖的SDK再開發(fā)一個地圖應用,那么如何在不擴大工作量的前提下動態(tài)的顯示地圖就成了CarLauncher
的一個技術難點。
2.CarLauncher功能分析
原生的Carlaunher
代碼并不復雜,主要是協(xié)同SystemUI完成以下兩個功能。
-
顯示 可以快捷操作的 『首頁』
-
顯示 所有APP入口的 『桌面』
需要注意的是,只有紅框中的內容才屬于CarLauncher
的內容,紅框之外的屬于SystemUI
的內容。雖然SystemUI
在下方的NaviBar有6個按鈕,但是只有點擊首頁和App桌面才會進入CarLauncher,點擊其它按鈕都會進入其它APP,所以都不在本篇文章的分析范圍。
3.CarLauncher 源碼分析
CarLauncher
的源碼結構如下:
3.1 Android.dp
CarLauncher的android.bp
相對比較簡單,定義了CarLauncher的源碼結構,和依賴的類庫。如果你對android.bp
完全不了解,可以先看一下 Android.bp入門教程 學習一下基礎的語法,再來回過頭來看CarLauncher的android.bp
相信會容易理解很多。
android_app {
name: "CarLauncher",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
// 允許使用系統(tǒng)的hide api
platform_apis: true,
required: ["privapp_whitelist_com.android.car.carlauncher"],
// 簽名類型 : platform
certificate: "platform",
// 設定apk安裝路徑為priv-app
privileged: true,
// 覆蓋其它類型的Launcher
overrides: [
"Launcher2",
"Launcher3",
"Launcher3QuickStep",
],
optimize: {
enabled: false,
},
dex_preopt: {
enabled: false,
},
// 引入靜態(tài)庫
static_libs: [
"androidx-constraintlayout_constraintlayout-solver",
"androidx-constraintlayout_constraintlayout",
"androidx.lifecycle_lifecycle-extensions",
"car-media-common",
"car-ui-lib",
],
libs: ["android.car"],
product_variables: {
pdk: {
enabled: false,
},
},
}
上述Android.bp中我們需要注意一個屬性overrides
,它表示覆蓋的意思。在系統(tǒng)編譯時Launcher2
、Launcher3
和Launcher3QuickStep
都會被CarLauncher
取代,前面三個Launcher并不是車機系統(tǒng)的桌面,車載系統(tǒng)中會用CarLauncher
這個定制新的桌面取代掉其它系統(tǒng)的桌面。同樣的,如果我們不想使用系統(tǒng)中自帶的CarLauncher
,那么也需要在overrides
中覆蓋掉CarLauncher
。在自主開發(fā)的車載Android系統(tǒng)中這個屬性我們會經常用到,用我們自己定制的各種APP來取代系統(tǒng)中默認的APP,比如系統(tǒng)設置等等。
3.2 AndroidManifest.xml
Manifest文件中我們可以看到CarLauncher所需要的權限,以及入口Activity。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.carlauncher">
<uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS" />
<!-- System permission to host maps activity -->
<uses-permission android:name="android.permission.ACTIVITY_EMBEDDING" />
<!-- System permission to send events to hosted maps activity -->
<uses-permission android:name="android.permission.INJECT_EVENTS" />
<!-- System permission to use internal system windows -->
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<!-- System permissions to bring hosted maps activity to front on main display -->
<uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
<!-- System permission to query users on device -->
<uses-permission android:name="android.permission.MANAGE_USERS" />
<!-- System permission to control media playback of the active session -->
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<!-- System permission to get app usage data -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<!-- System permission to query all installed packages -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<!-- To connect to media browser services in other apps, media browser clients
that target Android 11 need to add the following in their manifest -->
<queries>
<intent>
<action android:name="android.media.browse.MediaBrowserService" />
</intent>
</queries>
<application
android:icon="@drawable/ic_launcher_home"
android:label="@string/app_title"
android:supportsRtl="true"
android:theme="@style/Theme.Launcher">
<activity
android:name=".CarLauncher"
android:clearTaskOnLaunch="true"
android:configChanges="uiMode|mcc|mnc"
android:launchMode="singleTask"
android:resumeWhilePausing="true"
android:stateNotNeeded="true"
android:windowSoftInputMode="adjustPan">
<meta-data
android:name="distractionOptimized"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".AppGridActivity"
android:exported="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.Launcher.AppGridActivity">
<meta-data
android:name="distractionOptimized"
android:value="true" />
</activity>
</application>
</manifest>
關于Manifest我們重點來了解其中一些不常用的標簽即可。
<queries/>
<queries/>
是在Android 11 上為了收緊應用權限而引入的。用于,指定當前應用程序要與之交互的其他應用程序集,這些其他應用程序可以通過 package、intent、provider。示例:
<queries>
<package android:name="string" />
<intent>
...
</intent>
<provider android:authorities="list" />
...
</queries>
更多內容可以參考:Android Developers | <queries>
android:clearTaskOnLaunch = "true"
每次啟動都啟動根Activity,并清理其他的Activity。
android:configChanges="uiMode|mcc|mnc"
關于android:configChanges就不廢話了,直接上一張相對完整的表格供參考。
VALUE | DESCRIPTION |
---|---|
mcc | 國際移動用戶識別碼所屬國家代號是改變了,sim被偵測到了,去更新mcc MCC是移動用戶所屬國家代號 |
mnc | 國際移動用戶識別碼的移動網號碼是改變了, sim被偵測到了,去更新mnc MNC是移動網號碼,最多由兩位數(shù)字組成,用于識別移動用戶所歸屬的移動通信網 |
locale | 用戶所在區(qū)域發(fā)生變化。例如:用戶切換了語言時,切換后的語言會顯示出來 |
touchscreen | 觸摸屏發(fā)生改變 |
keyboard | 鍵盤發(fā)生了改變。例如:用戶介入了外部的鍵盤 |
keyboardHidden | 鍵盤的可用性發(fā)生了改變 |
navigation | 導航發(fā)生了變化 |
screenLayout | 屏幕的顯示發(fā)生了變化。例如:不同的顯示被激活 |
fontScale | 字體比例發(fā)生了變化。例如:選擇了不同的全局字體 |
uiMode | 用戶的模式發(fā)生了變化 |
orientation | 屏幕方向改變了。例如:橫豎屏切換 |
smallestScreenSize | 屏幕的物理大小改變了。例如:連接到一個外部的屏幕上 |
android:resumeWhilePausing = "true"
當前一個Activity還在執(zhí)行onPause()方法時(即在暫停過程中,還沒有完全暫停),允許該Activity顯示(此時Activity不能申請任何其他額外的資源,比如相機)
android:stateNotNeeded="true"
這個屬性默認情況為false,若設為true,則當Activity重新啟動時不會調用onSaveInstanceState方法,onCreate()方法中的Bundle參數(shù)將永遠為null。在一些特殊場合下,由于用戶按了Home鍵,該屬性設置為true時,可以保證不用保存原先的狀態(tài)引用,一定程度上節(jié)省空間資源。
android:name="distractionOptimized"
設定當前Activity處于活動狀態(tài),是否導致駕駛員分心,在國外車載Android應用程序需要遵守Android官方制定《駕駛員分心指南》,這個規(guī)則在國內使用的很少,具體請參考****Driver Distraction Guidelines | Android Open Source Project
3.3 AppGridActivity
**AppGridActivity**
用來顯示系統(tǒng)中所有的APP,為用戶的使用提供入口。
作為應用開發(fā)者,我們需要關注以下兩個功能是如何實現(xiàn)的:
- 顯示系統(tǒng)中所有的APP,并過濾掉一些不需要顯示在桌面的APP(例如:后臺的Service)
- 顯示最近使用的APP
顯示系統(tǒng)中所有的APP(All App)
CarLauncher
中用于篩選所有APP的方法都集中在AppLauncherUtils
/**
* 獲取我們希望在啟動器中以未排序的順序看到的所有組件,包括啟動器活動和媒體服務。
*
* @param blackList 要隱藏的應用程序(包名稱)列表(可能為空)
* @param customMediaComponents 不應在Launcher中顯示的媒體組件(組件名稱)列表(可能為空),因為將顯示其應用程序的Launcher活動
* @param appTypes 要顯示的應用程序類型(例如:全部或僅媒體源)
* @param openMediaCenter 當用戶選擇媒體源時,啟動器是否應導航到media center。
* @param launcherApps {@link LauncherApps}系統(tǒng)服務
* @param carPackageManager {@link CarPackageManager}系統(tǒng)服務
* @param packageManager {@link PackageManager}系統(tǒng)服務
* @return 一個新的 {@link LauncherAppsInfo}
*/
@NonNull
static LauncherAppsInfo getLauncherApps(
@NonNull Set<String> blackList,
@NonNull Set<String> customMediaComponents,
@AppTypes int appTypes,
boolean openMediaCenter,
LauncherApps launcherApps,
CarPackageManager carPackageManager,
PackageManager packageManager,
CarMediaManager carMediaManager) {
if (launcherApps == null || carPackageManager == null || packageManager == null
|| carMediaManager == null) {
return EMPTY_APPS_INFO;
}
// 檢索所有符合給定intent的服務
List<ResolveInfo> mediaServices = packageManager.queryIntentServices(
new Intent(MediaBrowserService.SERVICE_INTERFACE),
PackageManager.GET_RESOLVED_FILTER);
// 檢索指定packageName的Activity的列表
List<LauncherActivityInfo> availableActivities =
launcherApps.getActivityList(null, Process.myUserHandle());
Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(
mediaServices.size() + availableActivities.size());
Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());
// Process media services
if ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {
for (ResolveInfo info : mediaServices) {
String packageName = info.serviceInfo.packageName;
String className = info.serviceInfo.name;
ComponentName componentName = new ComponentName(packageName, className);
mediaServicesMap.put(componentName, info);
if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
appTypes, APP_TYPE_MEDIA_SERVICES)) {
final boolean isDistractionOptimized = true;
Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());
AppMetaData appMetaData = new AppMetaData(
info.serviceInfo.loadLabel(packageManager),
componentName,
info.serviceInfo.loadIcon(packageManager),
isDistractionOptimized,
context -> {
if (openMediaCenter) {
AppLauncherUtils.launchApp(context, intent);
} else {
selectMediaSourceAndFinish(context, componentName, carMediaManager);
}
},
context -> {
// 返回系統(tǒng)中所有MainActivity帶有Intent.CATEGORY_INFO 和 Intent.CATEGORY_LAUNCHER的intent
Intent packageLaunchIntent =
packageManager.getLaunchIntentForPackage(packageName);
AppLauncherUtils.launchApp(context,
packageLaunchIntent != null ? packageLaunchIntent : intent);
});
launchablesMap.put(componentName, appMetaData);
}
}
}
// Process activities
if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
for (LauncherActivityInfo info : availableActivities) {
ComponentName componentName = info.getComponentName();
String packageName = componentName.getPackageName();
if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,
appTypes, APP_TYPE_LAUNCHABLES)) {
boolean isDistractionOptimized =
isActivityDistractionOptimized(carPackageManager, packageName,
info.getName());
Intent intent = new Intent(Intent.ACTION_MAIN)
.setComponent(componentName)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 獲取app的name,和 app的圖標
AppMetaData appMetaData = new AppMetaData(
info.getLabel(),
componentName,
info.getBadgedIcon(0),
isDistractionOptimized,
context -> AppLauncherUtils.launchApp(context, intent),
null);
launchablesMap.put(componentName, appMetaData);
}
}
}
return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
}
通過LauncherApps.getActivityList(),獲得的List<LauncherActivityInfo>包含了系統(tǒng)中所有配置了Intent#ACTION_MAIN
和Intent#CATEGORY_LAUNCHER
的Activity信息。
String LauncherActivityInfogetLabel() : 獲取app的name
String LauncherActivityInfo.getComponentName() : 獲取app的Mainactivity信息
Drawable LauncherActivityInfo.getBadgedIcon(0) : 獲取App的圖標
最后,當用戶點擊圖標時,雖然也是通過startActivity啟動App,但是ActivityOptions可以讓我們決定目標APP在哪個屏幕上啟動,這對當前車載多屏系統(tǒng)而言非常重要。
static void launchApp(Context context, Intent intent) {
ActivityOptions options = ActivityOptions.makeBasic();
// 在當前的屏幕上啟動目標App的Activity
options.setLaunchDisplayId(context.getDisplayId());
context.startActivity(intent, options.toBundle());
}
顯示最近使用的APP (Recent APP)
Android系統(tǒng)中提供了UsageStatusManager
來提供對設備使用情況歷史記錄和統(tǒng)計信息的訪問,UsageStatusManager
使用android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS
,getAppStandbyBucket()
,queryEventsForSelf(long,long)
,方法時不需要添加額外的權限。但是除此以外的方法都需要android.permission.PACKAGE_USAGE_STATS
權限。
/**
* 請注意,為了從上一次boot中獲得使用情況統(tǒng)計數(shù)據(jù),設備必須經過干凈的關閉過程。
*/
private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) {
ArrayList<AppMetaData> apps = new ArrayList<>();
if (appsInfo.isEmpty()) {
return apps;
}
// 獲取從1年前開始的使用情況統(tǒng)計數(shù)據(jù),返回如下條目:
// "During 2017 App A is last used at 2017/12/15 18:03"
// "During 2017 App B is last used at 2017/6/15 10:00"
// "During 2018 App A is last used at 2018/1/1 15:12"
List<UsageStats> stats =
mUsageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_YEARLY,
System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS,
System.currentTimeMillis());
if (stats == null || stats.size() == 0) {
return apps; // empty list
}
stats.sort(new LastTimeUsedComparator());
int currentIndex = 0;
int itemsAdded = 0;
int statsSize = stats.size();
int itemCount = Math.min(mColumnNumber, statsSize);
while (itemsAdded < itemCount && currentIndex < statsSize) {
UsageStats usageStats = stats.get(currentIndex);
String packageName = usageStats.mPackageName;
currentIndex++;
// 不包括自己
if (packageName.equals(getPackageName())) {
continue;
}
// TODO(b/136222320): 每個包都可以獲得UsageStats,但一個包可能包含多個媒體服務。我們需要找到一種方法來獲取每個服務的使用率統(tǒng)計數(shù)據(jù)。
ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager,
packageName);
// 免除媒體服務的后臺和啟動器檢查
if (!appsInfo.isMediaService(componentName)) {
// 不要包括僅在后臺運行的應用程序
if (usageStats.getTotalTimeInForeground() == 0) {
continue;
}
// 不要包含不支持從啟動器啟動的應用程序
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
continue;
}
}
AppMetaData app = appsInfo.getAppMetaData(componentName);
// 防止重復條目
// e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00
if (app != null && !apps.contains(app)) {
apps.add(app);
itemsAdded++;
}
}
return apps;
}
3.4 CarLauncher
CarLauncher
和普通的Android應用一樣,都會有一個主布局文件,并且在Activity的onCreate中進行一系列初始化工作,所以我們先從CarLauncher
的布局文件和onCreate方法開始分析。
CarLauncher的主布局文件
這里我們只分析橫屏的布局文件,源碼中其實還包含了豎屏和多窗口的布局。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr"
tools:context=".CarLauncher">
<!-- 省略 -->
<View
android:id="@+id/top_line"
style="@style/HorizontalLineDivider"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
style="@style/CardViewStyle"
// ...
>
<!-- 用來顯示地圖的 ActivityView -->
<android.car.app.CarActivityView
android:id="@+id/maps"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<!-- 顯示天氣 -->
<FrameLayout
android:id="@+id/contextual"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/main_screen_widget_margin"
android:layoutDirection="locale"
app:layout_constraintBottom_toTopOf="@+id/divider_horizontal"
app:layout_constraintLeft_toRightOf="@+id/divider_vertical"
app:layout_constraintRight_toLeftOf="@+id/end_edge"
app:layout_constraintTop_toBottomOf="@+id/top_edge" />
<!-- 顯示本地音樂 -->
<FrameLayout
android:id="@+id/playback"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/main_screen_widget_margin"
android:layoutDirection="locale"
app:layout_constraintBottom_toTopOf="@+id/bottom_edge"
app:layout_constraintLeft_toRightOf="@+id/divider_vertical"
app:layout_constraintRight_toLeftOf="@+id/end_edge"
app:layout_constraintTop_toBottomOf="@+id/divider_horizontal" />
<View
android:id="@+id/bottom_line"
style="@style/HorizontalLineDivider"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
CarLauncher
的布局文件很簡單,幾乎沒什么值得解釋的地方。
初始化Android桌面
初始化Android桌面的工作大多都是在CarLuancher.onCreate方法中完成,該方法代碼如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在多窗口模式下『car_launcher_multiwindow』不顯示“地圖”面板。
// 注意:拆分屏幕的CTS測試與啟動器默認活動的活動視圖不兼容
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
}
// 初始化『天氣』和『音樂』fragment
initializeFragments();
// 監(jiān)聽ActivityView狀態(tài),并啟動地圖面板
mActivityView = findViewById(R.id.maps);
if (mActivityView != null) {
mActivityView.setCallback(mActivityViewCallback);
}
// 監(jiān)聽屏幕狀態(tài),并啟動地圖面板
mDisplayManager = getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(mDisplayListener, mMainHandler);
}
上述代碼中的ActivityView
是用來顯示地圖的一個ViewGroup。ActivityView
的狀態(tài)回調如下:
private final ActivityView.StateCallback mActivityViewCallback = new ActivityView.StateCallback() {
@Override
public void onActivityViewReady(ActivityView view) {
if (DEBUG) Log.d(TAG, "onActivityViewReady(" + getUserId() + ")");
mActivityViewReady = true;
startMapsInActivityView();
maybeLogReady();
}
@Override
public void onActivityViewDestroyed(ActivityView view) {
if (DEBUG) Log.d(TAG, "onActivityViewDestroyed(" + getUserId() + ")");
mActivityViewReady = false;
}
@Override
public void onTaskMovedToFront(int taskId) {
if (DEBUG) {
Log.d(TAG, "onTaskMovedToFront(" + getUserId() + "): started=" + mIsStarted);
}
try {
if (mIsStarted) {
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
am.moveTaskToFront(CarLauncher.this.getTaskId(), /* flags= */ 0);
}
} catch (RuntimeException e) {
Log.w(TAG, "Failed to move CarLauncher to front.");
}
}
};
設定回調的作用就是在ActivityView
初始化完畢后,啟動地圖。
private void startMapsInActivityView() {
if (mActivityView == null || !mActivityViewReady) {
return;
}
// 如果我們碰巧被重新呈現(xiàn)為多顯示模式,我們將跳過在“Activity”視圖中啟動內容,因為我們無論如何都會被重新創(chuàng)建。
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
return;
}
// 在“活動可見性測試(ActivityVisibilityTests)”的顯示關閉時不要啟動地圖。
if (getDisplay().getState() != Display.STATE_ON) {
return;
}
try {
mActivityView.startActivity(getMapsIntent());
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Maps activity not found", e);
}
}
private Intent getMapsIntent() {
// 為應用程序的主Activity創(chuàng)建一個意圖,不指定要運行的特定Activity,而是提供一個選擇器來查找該Activity。
return Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS);
}
在onCreate中還會監(jiān)聽當前的屏幕狀態(tài),每當屏幕的邏輯顯示屬性(如大小和密度)發(fā)生更改時,都會在ActivityView
中重新顯示地圖。
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {}
@Override
public void onDisplayRemoved(int displayId) {}
@Override
public void onDisplayChanged(int displayId) {
if (displayId != getDisplay().getDisplayId()) {
return;
}
// startMapsInActivityView()將檢查顯示器的狀態(tài)。
startMapsInActivityView();
}
};
ActivityView
允許將Activity啟動到自身的任務容器。本質上是一個虛擬屏幕,所以在此ActivityView
中啟動的活動受適用于在VirtualDisplay
上啟動的相同規(guī)則的限制。
如果想在我們自己項目中使用ActivityView
,需要從Android源碼移植,Android SDK中并沒有提供ActivityView相關的API。
注意:由于ActivityView中的hosted maps活動當前處于虛擬屏幕,因此系統(tǒng)認為該活動始終位于前面。以直接intent啟動“地圖”Activity將不起作用。要在real display上啟動“地圖”活動,需要使用{****@link*** *Intent#CATEGORY_APP_MAPS}類別將intent發(fā)送給launcher,launcher將在real display上啟動Activity。
有關VirtualDisplay
的內容可以參考這篇老哥的博客:Android P 圖形顯示系統(tǒng)(四) Android VirtualDisplay解析