Launcher is the first interface to be seen after the android mobile phone is launched, that is, the desktop of the mobile phone system. Let's take the native Launcher 3 of android as an example to see how the interface layout and display data are acquired to briefly analyze the android mobile phone desktop.
The first Activity shown in Launcher is Launcher.java. Let's look at the layout file launcher.xml
Then look at the corresponding UI display, you can also look at the layout above.//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>
This interface mainly displays Shortcut and Widget. The Shortcut in the bottom line is special. setIsHotseat attribute is set. When it is displayed, it will be judged whether it is displayed on the bottom line or not. In addition, the number of displays is also default configuration in dw_phone_hotseat.xml. When users want to put other APK shortcuts in this line, it will root. The remaining space after removing the displayed icons determines whether to display the icons separately or in a folder with other apk icons.
All of this data is stored in the database launcher.db.
Hotseat is a subclass of FrameLayout, which retrieves DeviceProfile objects from the getDeviceProfile in Launcher, which contain configuration files for specific devices.
Get all device_profiles in InvariantDeviceProfile.java by getPredefinedDeviceProfiles
The ArrayList < Invariant Device Profile > profiles are obtained by parsing device_profiles.xml reading. FindClosestDevice Profiles and invDistributed Interpolate are finally invoked to obtain the appropriate Invariant Device Profile according to the width and height of the mobile screen. The Invariant Device Profile contains the configuration of the number allowed to be displayed in each row and column.//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; }
Here's a simple configuration of device_profiles.xmlpackages/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; }
default_workspace_4x4.xml/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>
dw_phone_hotseat.xmlpackages/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>
Favorites and Workspaces are stored in the favorites and workspace Screens tables in launcher.db, which are only displayed by default. When a user manually puts an application into this block, it will decide whether to display an icon separately or together with another icon in a folder according to the current remaining space. The current situation can be known by default_workspace_4x4.xml. The default configuration on the main screen shows those apks, of course, all of which are saved in the database, so that when the user drags an apk, it will be written to the corresponding database. Here is just the logic to write the default configuration to the database.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>
So much, let's first look at the display of the main interface, and then see how the information of all desktop apk s is obtained by calling getActivityList. The first parameter, packageName, is null, querying all the application collection lists configured with intent Action as ACTION_MAIN and Category as CATEGORY_LAUNCHER.//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(); } }
After entering the main interface is an AllApps ContainerView, inheriting the FrameLayout, the setAdapter set in the constructor is AllAppsGridAdapter AllAppsGridAdapter extends RecyclerView.Adapter, and here calls mAppsRecyclerView.setApps(mApps) to pass in all apps information.//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; }
Call onCreateViewHolder and onBindViewHolder in AllAppsGridAdapter to display view//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); ... }
Next, let's look at the construction method of AllAppsGridAdapter, which passes in View.OnClickListener and View.OnLongClickListener//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); //Set the click event listener for each control, and each desktop icon is a custom 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 is initialized in the construction method of AllAppsContainerView, as follows:public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) { ... }
You can see that View.OnClickListener of AllAppsGrid Adapter comes from mLauncher, Launcher.java, and View.OnLongClickListener passes in this, which is handled in this class's onLongClick, so that all item's click events are handled in Launcher's onClick.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); }
//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) {//If the apk calls startAppShortcutOrInfoActivity, start the corresponding Activity startAppShortcutOrInfoActivity(v); } else if (tag instanceof LauncherAppWidgetInfo) { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v); } } }
When you click on the icon of each application, you call startAppShortcutOrInfoActivity(View v)
In this method, the startActivitySafely of this class is then called, and eventually in this Launcher AppsCompat. getInstance (this). startActivityForProfileprivate 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); } }
In this way, every click on an icon will start a corresponding Activity. At first, I just want to see the process of clicking on apk to start the application. I found the click event of apk icon for half a day. In this incidental record, of course, there are many desktop layout information from the database, update layout information and so on. It is not mentioned that the analysis is mainly based on the acquisition of apk information and icon click event. Yes, when there are multiple applications in the phone with the following two attributes, you will see that the user will choose which desktop to use.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); }
Here's a brief summary of the desktop display process<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" />
1. After the mobile phone starts, it will start the desktop first. In Launcher's database, there will be detailed information of each apk's display location, specifically in the way of coordinates.
2. Query all apk information that needs to be displayed on the desktop through getActivityList and return a collection.
3. The main interface is a custom AllApps Recycler View inheriting Recycler View. Setting up AllApps Grid Adapter, how many BubbleTextView s will be created according to the size of the list set above (that is, the desktop icon you see).