Launcher是android手機啓動後第一個看到的界面,即手機系統的桌面,下面咱們就以android原生的Launcher3爲例看看界面佈局和顯示的數據怎麼獲取的來簡要分析下android手機桌面java
Launcher中第一顯示的Activity爲Launcher.java,下面咱們主要看看這個佈局文件launcher.xmlandroid
//packages/apps/Launcher3/res/layout-land/launcher.xml
<!-- Full screen view projects under the status bar and contains the background -->
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.android.launcher3.dragndrop.DragLayer
android:id="@+id/drag_layer"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/workspace_bg"
android:importantForAccessibility="no"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
launcher:pageIndicator="@id/page_indicator" />
<!-- DO NOT CHANGE THE ID -->
<include layout="@layout/hotseat"
android:id="@+id/hotseat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="right"
launcher:layout_ignoreInsets="true" />
<include
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar_vert" />
<include layout="@layout/overview_panel"
android:id="@+id/overview_panel"
android:visibility="gone" />
<com.android.launcher3.pageindicators.PageIndicatorCaretLandscape
android:id="@+id/page_indicator"
android:layout_width="@dimen/dynamic_grid_page_indicator_height"
android:layout_height="@dimen/dynamic_grid_page_indicator_height"
android:layout_gravity="bottom|left"/>
<!-- A place holder view instead of the QSB in transposed layout -->
<View
android:layout_width="0dp"
android:layout_height="10dp"
android:id="@+id/workspace_blocked_row" />
<include layout="@layout/widgets_view"
android:id="@+id/widgets_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<include layout="@layout/all_apps"
android:id="@+id/apps_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
接着看下對應的UI顯示,也能夠對着上面的佈局看看數據庫
這個界面顯示的主要爲Shortcut和Widget,其中最下面一行的幾個Shortcut比較特殊,都設置了setIsHotseat屬性,在具體顯示的時候會根據來判斷是否是顯示在最下一行,另外顯示的個數也在dw_phone_hotseat.xml有默認配置的,當用戶還想再這個裏面放入其它apk快捷方式時,會根據除去已顯示的圖標後剩餘空間來決定是單獨顯示圖標,仍是和其它apk圖標放在一個文件夾裏顯示。
全部的這些數據都會放入到數據庫launcher.db中保存。app
Hotseat爲FrameLayout的子類,經過Launcher裏的getDeviceProfile獲取DeviceProfile對象,裏面包含特定設備的一些配置文件ide
在InvariantDeviceProfile.java中經過getPredefinedDeviceProfiles來獲取全部的device_profiles佈局
//packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) {
TypedArray a = context.obtainStyledAttributes(
Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile);
int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0);
int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0);
float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0);
profiles.add(new InvariantDeviceProfile(
a.getString(R.styleable.InvariantDeviceProfile_name),
a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0),
a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0),
numRows,
numColumns,
a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows),
a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns),
a.getInt(R.styleable.InvariantDeviceProfile_minAllAppsPredictionColumns, numColumns),
iconSize,
a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0),
a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns),
a.getFloat(R.styleable.InvariantDeviceProfile_hotseatIconSize, iconSize),
a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0)));
a.recycle();
}
}
} catch (IOException|XmlPullParserException e) {
throw new RuntimeException(e);
}
return profiles;
}
解析device_profiles.xml讀取得到ArrayList<InvariantDeviceProfile> profiles,最後根據手機屏幕寬高調用findClosestDeviceProfiles和invDistWeightedInterpolate獲取合適的InvariantDeviceProfile,InvariantDeviceProfile裏包含了每行每列容許顯示的個數等配置ui
packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
final float width, final float height, ArrayList<InvariantDeviceProfile> points) {
// Sort the profiles by their closeness to the dimensions
ArrayList<InvariantDeviceProfile> pointsByNearness = points;
Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps));
}
});
return pointsByNearness;
}
下面簡單貼了個device_profiles.xml的配置this
/packages/apps/Launcher3/res/xml/device_profiles.xml
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
<profile
launcher:name="Nexus 4"
launcher:minWidthDps="359"
launcher:minHeightDps="567"
launcher:numRows="4"
launcher:numColumns="4"
launcher:numFolderRows="4"
launcher:numFolderColumns="4"
launcher:minAllAppsPredictionColumns="4"
launcher:iconSize="60"
launcher:iconTextSize="13.0"
launcher:numHotseatIcons="5"
launcher:hotseatIconSize="56"
launcher:defaultLayoutId="@xml/default_workspace_4x4"
/>
...
</profiles>
default_workspace_4x4.xmlspa
packages/apps/Launcher3/res/xml/default_workspace_4x4.xml
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- Hotseat -->
<include launcher:workspace="@xml/dw_phone_hotseat" />
<!-- Bottom row -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
<favorite launcher:uri="#Intent;type=images/*;end" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
<favorite launcher:uri="market://details?id=com.android.launcher" />
</resolve>
</favorites>
dw_phone_hotseat.xmlrest
packages/apps/Launcher3/res/xml/dw_phone_hotseat.xml
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<!-- Dialer, Messaging, [All Apps], Browser, Camera -->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
<favorite launcher:uri="tel:123" />
<favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="1"
launcher:x="1"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
<favorite launcher:uri="sms:" />
<favorite launcher:uri="smsto:" />
<favorite launcher:uri="mms:" />
<favorite launcher:uri="mmsto:" />
</resolve>
<!-- All Apps -->
<resolve
launcher:container="-101"
launcher:screen="3"
launcher:x="3"
launcher:y="0" >
<favorite
launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
<favorite launcher:uri="http://www.example.com/" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="4"
launcher:x="4"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
<favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
</resolve>
</favorites>
其中Favorites和Workspaces都會保存在launcher.db裏的favorites和workspaceScreens表裏,上面只是默認顯示的,當用戶手動把某個應用放入這塊時,會根據當前的剩餘空間,來決定是單獨顯示一個icon仍是和另外一個圖標一塊兒顯示在一個文件夾裏,經過default_workspace_4x4.xml可知道當前主屏幕上默認配置顯示那些apk,固然全部這樣配置都會保存在數據庫中,這樣當用戶拖拽某個apk後,都會寫入到相應數據庫中的,下面只是貼了下把默認配置寫入數據庫的邏輯
//packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(getContext().getPackageManager());
if (partner != null && partner.hasDefaultLayout()) {
final Resources partnerRes = partner.getResources();
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHost);
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
createEmptyDB();
// Populate favorites table with initial favorites
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
createEmptyDB();
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
}
說了這麼多,先來看看主界面的顯示圖,再來看看全部桌面apk的信息是怎麼獲取,調用getActivityList獲取的,其中第一個參數packageName傳入的是null,查詢全部配置了intent Action爲ACTION_MAIN,Category爲CATEGORY_LAUNCHER的應用集合列表
//packages/apps/Launcher3/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
public List<LauncherActivityInfoCompat> getActivityList(String packageName,
UserHandleCompat user) {
final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mainIntent.setPackage(packageName);
List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0);
List<LauncherActivityInfoCompat> list =
new ArrayList<LauncherActivityInfoCompat>(infos.size());
for (ResolveInfo info : infos) {
list.add(new LauncherActivityInfoCompatV16(mContext, info));
}
return list;
}
進入主界面後就是一個AllAppsContainerView,繼承FrameLayout,在構造方法中設置的setAdapter爲AllAppsGridAdapter AllAppsGridAdapter extends RecyclerView.Adapter,而且在這裏調用了mAppsRecyclerView.setApps(mApps)傳入了全部的apps 信息
//packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java
@Override
protected void onFinishInflate() {
super.onFinishInflate();
...
// Load the all apps recycler view
mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
mAppsRecyclerView.setApps(mApps);
mAppsRecyclerView.setLayoutManager(mLayoutManager);
mAppsRecyclerView.setAdapter(mAdapter);
mAppsRecyclerView.setHasFixedSize(true);
mAppsRecyclerView.addOnScrollListener(mElevationController);
mAppsRecyclerView.setElevationController(mElevationController);
...
}
在AllAppsGridAdapter調用onCreateViewHolder和onBindViewHolder來顯示view
//packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_SECTION_BREAK:
return new ViewHolder(new View(parent.getContext()));
case VIEW_TYPE_ICON:
/* falls through */
case VIEW_TYPE_PREDICTION_ICON: {
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.all_apps_icon, parent, false);
icon.setOnClickListener(mIconClickListener); //設置每一個控件的點擊事件監聽,每一個桌面圖標都是一個自定義的BubbleTextView
icon.setOnLongClickListener(mIconLongClickListener);
icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
.getLongPressTimeout());
icon.setOnFocusChangeListener(mIconFocusListener);
// Ensure the all apps icon height matches the workspace icons
DeviceProfile profile = mLauncher.getDeviceProfile();
Point cellSize = profile.getCellSize();
GridLayoutManager.LayoutParams lp =
(GridLayoutManager.LayoutParams) icon.getLayoutParams();
lp.height = cellSize.y;
icon.setLayoutParams(lp);
return new ViewHolder(icon);
}
case VIEW_TYPE_EMPTY_SEARCH:
return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
parent, false));
case VIEW_TYPE_SEARCH_MARKET:
View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
parent, false);
searchMarketView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
}
});
return new ViewHolder(searchMarketView);
case VIEW_TYPE_SEARCH_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_search_divider, parent, false));
case VIEW_TYPE_PREDICTION_DIVIDER:
/* falls through */
case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_divider, parent, false));
default:
throw new RuntimeException("Unexpected view type");
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case VIEW_TYPE_ICON: {
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
case VIEW_TYPE_PREDICTION_ICON: {
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
case VIEW_TYPE_EMPTY_SEARCH:
TextView emptyViewText = (TextView) holder.mContent;
emptyViewText.setText(mEmptySearchMessage);
emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
Gravity.START | Gravity.CENTER_VERTICAL);
break;
case VIEW_TYPE_SEARCH_MARKET:
TextView searchView = (TextView) holder.mContent;
if (mMarketSearchIntent != null) {
searchView.setVisibility(View.VISIBLE);
} else {
searchView.setVisibility(View.GONE);
}
break;
}
if (mBindViewCallback != null) {
mBindViewCallback.onBindView(holder);
}
}
下面再來看看AllAppsGridAdapter構造方法,裏面傳入了View.OnClickListener和View.OnLongClickListener
public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
iconClickListener, View.OnLongClickListener iconLongClickListener) {
...
}
AllAppsGridAdapter是在AllAppsContainerView的構造方法中初始化的,以下:
public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Resources res = context.getResources();
mLauncher = Launcher.getLauncher(context);
mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
mApps = new AlphabeticalAppsList(context);
mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
mApps.setAdapter(mAdapter);
mLayoutManager = mAdapter.getLayoutManager();
mItemDecoration = mAdapter.getItemDecoration();
DeviceProfile grid = mLauncher.getDeviceProfile();
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && !grid.isVerticalBarLayout()) {
mRecyclerViewBottomPadding = 0;
setPadding(0, 0, 0, 0);
} else {
mRecyclerViewBottomPadding =
res.getDimensionPixelSize(R.dimen.all_apps_list_bottom_padding);
}
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
}
能夠看到AllAppsGridAdapter的View.OnClickListener來自mLauncher即Launcher.java,View.OnLongClickListener傳入的是this,即在本類的onLongClick中處理,這樣即全部item的click事件都在Launcher的onClick處理
//packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
public void onClick(View v) {
// Make sure that rogue clicks don't get through while allapps is launching, or after the
// view has detached (it's possible for this to happen if the view is removed mid touch).
if (v.getWindowToken() == null) {
return;
}
if (!mWorkspace.isFinishedSwitchingState()) {
return;
}
if (v instanceof Workspace) {
if (mWorkspace.isInOverviewMode()) {
showWorkspace(true);
}
return;
}
if (v instanceof CellLayout) {
if (mWorkspace.isInOverviewMode()) {
mWorkspace.snapToPageFromOverView(mWorkspace.indexOfChild(v));
showWorkspace(true);
}
return;
}
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {
onClickAppShortcut(v);
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
}
} else if ((FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && v instanceof PageIndicator) ||
(v == mAllAppsButton && mAllAppsButton != null)) {
onClickAllAppsButton(v);
} else if (tag instanceof AppInfo) {//若是是apk調用startAppShortcutOrInfoActivity,啓動對應Activity
startAppShortcutOrInfoActivity(v);
} else if (tag instanceof LauncherAppWidgetInfo) {
if (v instanceof PendingAppWidgetHostView) {
onClickPendingWidget((PendingAppWidgetHostView) v);
}
}
}
當點擊每一個應用的icon後,就會調用startAppShortcutOrInfoActivity(View v)
private void startAppShortcutOrInfoActivity(View v) {
ItemInfo item = (ItemInfo) v.getTag();
Intent intent = item.getIntent();
if (intent == null) {
throw new IllegalArgumentException("Input must have a valid intent");
}
boolean success = startActivitySafely(v, intent, item);
getUserEventDispatcher().logAppLaunch(v, intent);
if (success && v instanceof BubbleTextView) {
mWaitingForResume = (BubbleTextView) v;
mWaitingForResume.setStayPressed(true);
}
}
在這個方法裏接着調用本類的startActivitySafely,最終在這個LauncherAppsCompat.getInstance(this).startActivityForProfile
public void startActivityForProfile(ComponentName component, UserHandleCompat user,
Rect sourceBounds, Bundle opts) {
Intent launchIntent = new Intent(Intent.ACTION_MAIN);
launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
launchIntent.setComponent(component);
launchIntent.setSourceBounds(sourceBounds);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(launchIntent, opts);
}
這樣每點擊一個應該icon就會啓動一個對應的Activity,本開始只是想看看點擊apk啓動應用的流程,找到apk icon點擊事件找了半天,在這順帶記錄下,固然還有不少從數據庫獲取桌面佈局信息,更新佈局信息啥的在這沒有提到主要是以apk信息的獲取和icon單擊事件來分析的,當手機裏有多個應用了下面兩個屬性時,就會看到會讓用戶選擇用那個桌面
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> 下面簡單總結下桌面的顯示流程 一、手機啓動後會先啓動桌面,在Launcher的數據庫中會有每一個apk顯示位置的詳細信息,具體是以座標的方式。 二、經過getActivityList查詢全部須要顯示在桌面的apk信息,並返回一個集合。 三、主界面是一個自定義AllAppsRecyclerView繼承RecyclerView,設置了AllAppsGridAdapter會根據上面list集合的size建立多少個BubbleTextView(即看到的桌面圖標)。