From 76c3603693d4c6101523de11fe1aeaac2cc2bfb0 Mon Sep 17 00:00:00 2001 From: TMC Date: Wed, 11 Nov 2026 21:25:41 +0100 Subject: [PATCH] Turn the app drawer into a one-column list with letter headers PRD M2 (AD-1 to AD-3, AD-7, AD-8, HS-7). The drawer keeps Launcher3's adapter, sorting, live updates, click, long-press menu and drag; only what differs from the grid is new. - FlexeraDrawer (flexera/src): column count, row styling, and a letter-header adapter item - flexera/res/layout/all_apps_icon.xml overrides Launcher3's grid cell with a horizontal row (46dp icon, name beside it, 64dp tall) - AlphabeticalAppsList adds a header item at the start of each letter group and points the fast-scroll section at it; Private Space keeps its own framing - BaseAllAppsAdapter creates and binds the header view type and styles rows - DeviceProfileOverrides forces one drawer column - DeviceProfile.getAllAppsIconStartMargin returns the row padding for a one-column drawer, which keeps the search bar full width - Suggested apps row above the list is off by default Tested on the Android 17 emulator: scrolling, tap to launch, long-press menu, drag to the home screen, and live removal and return of an app all work. The right-edge fast scroller did not respond to scripted drags and still needs a hand test. Co-Authored-By: Claude Fable 5.2 --- flexera/res/layout/all_apps_icon.xml | 21 +++++ .../flexera_all_apps_section_header.xml | 15 ++++ flexera/res/values/flexera_config.xml | 3 + flexera/res/values/flexera_dimens.xml | 16 ++++ .../src/app/flexera/drawer/FlexeraDrawer.kt | 77 +++++++++++++++++++ .../app/lawnchair/DeviceProfileOverrides.kt | 5 +- src/com/android/launcher3/DeviceProfile.java | 8 ++ .../allapps/AlphabeticalAppsList.java | 11 +++ .../launcher3/allapps/BaseAllAppsAdapter.java | 11 +++ 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 flexera/res/layout/all_apps_icon.xml create mode 100644 flexera/res/layout/flexera_all_apps_section_header.xml create mode 100644 flexera/res/values/flexera_dimens.xml create mode 100644 flexera/src/app/flexera/drawer/FlexeraDrawer.kt diff --git a/flexera/res/layout/all_apps_icon.xml b/flexera/res/layout/all_apps_icon.xml new file mode 100644 index 0000000000..35e1338262 --- /dev/null +++ b/flexera/res/layout/all_apps_icon.xml @@ -0,0 +1,21 @@ + + + diff --git a/flexera/res/layout/flexera_all_apps_section_header.xml b/flexera/res/layout/flexera_all_apps_section_header.xml new file mode 100644 index 0000000000..4fe50bbf59 --- /dev/null +++ b/flexera/res/layout/flexera_all_apps_section_header.xml @@ -0,0 +1,15 @@ + + + diff --git a/flexera/res/values/flexera_config.xml b/flexera/res/values/flexera_config.xml index 0e872da73f..29f6bb8dfa 100644 --- a/flexera/res/values/flexera_config.xml +++ b/flexera/res/values/flexera_config.xml @@ -14,4 +14,7 @@ false + + + false diff --git a/flexera/res/values/flexera_dimens.xml b/flexera/res/values/flexera_dimens.xml new file mode 100644 index 0000000000..ef0d5370aa --- /dev/null +++ b/flexera/res/values/flexera_dimens.xml @@ -0,0 +1,16 @@ + + + + 64dp + + 46dp + 20dp + 17sp + 24dp + + 56dp + + 44dp + 6dp + 14sp + diff --git a/flexera/src/app/flexera/drawer/FlexeraDrawer.kt b/flexera/src/app/flexera/drawer/FlexeraDrawer.kt new file mode 100644 index 0000000000..4258cfaf13 --- /dev/null +++ b/flexera/src/app/flexera/drawer/FlexeraDrawer.kt @@ -0,0 +1,77 @@ +package app.flexera.drawer + +import android.content.Context +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.android.launcher3.BubbleTextView +import com.android.launcher3.R +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem + +/** + * Flexera Launcher's app drawer: a single-column alphabetical list with letter headers + * (PRD AD-2 and AD-3), built on Launcher3's all-apps adapter rather than replacing it. + * + * Launcher3 keeps doing the work it already does well: loading apps, sorting them, live updates, + * click, long-press menu and drag. This object supplies only what differs from the stock grid: + * - the column count ([COLUMNS]), + * - the look of a row ([styleAppRow]; the row layout itself is flexera/res/layout/all_apps_icon.xml, + * which overrides Launcher3's layout of the same name), and + * - a letter header row ([VIEW_TYPE_SECTION_HEADER] and the functions that create and bind it). + */ +object FlexeraDrawer { + + /** The drawer is one column wide. */ + const val COLUMNS = 1 + + /** + * Adapter view type of a letter header row. Launcher3 uses bits 1 to 10 and Lawnchair's search + * results use bits 10 to 22, so this stays clear of both. + */ + const val VIEW_TYPE_SECTION_HEADER = 1 shl 24 + + /** Creates the adapter item for the header that starts the section named [sectionName]. */ + @JvmStatic + fun sectionHeader(sectionName: String): AdapterItem = SectionHeaderItem(sectionName) + + @JvmStatic + fun createSectionHeader(inflater: LayoutInflater, parent: ViewGroup): View = + inflater.inflate(R.layout.flexera_all_apps_section_header, parent, false) + + @JvmStatic + fun bindSectionHeader(view: View, item: AdapterItem) { + (view as TextView).text = (item as? SectionHeaderItem)?.sectionName.orEmpty() + } + + /** + * Finishes turning a freshly inflated all-apps icon into a list row. Launcher3 sizes the view + * as a grid cell (icon above a small label); a row needs its own height, a wider gap between + * icon and name, and a label large enough to read in a list. + */ + @JvmStatic + fun styleAppRow(icon: BubbleTextView) { + val res = icon.resources + icon.layoutParams.height = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_height) + icon.compoundDrawablePadding = res.getDimensionPixelSize(R.dimen.flexera_drawer_row_icon_gap) + icon.setTextSize(TypedValue.COMPLEX_UNIT_PX, res.getDimension(R.dimen.flexera_drawer_row_text_size)) + } + + /** + * How far a row's icon sits from the edge of the list. Launcher3 uses its grid version of + * this number to line the search bar up with the first icon, so the list supplies its own. + */ + @JvmStatic + fun rowStartPadding(context: Context): Int = + context.resources.getDimensionPixelSize(R.dimen.flexera_drawer_row_padding_start) + + /** + * A letter header. It carries its letter so that the adapter's diffing notices when the + * header at a position changes, for example after an app is installed or removed. + */ + class SectionHeaderItem(val sectionName: String) : AdapterItem(VIEW_TYPE_SECTION_HEADER) { + override fun isContentSame(other: AdapterItem): Boolean = + other is SectionHeaderItem && other.sectionName == sectionName + } +} diff --git a/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt b/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt index 01b9ad6d36..c20c3671af 100644 --- a/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt +++ b/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt @@ -158,8 +158,9 @@ class DeviceProfileOverrides @Inject constructor( fun applyUi(idp: InvariantDeviceProfile) { // apply grid size - idp.numAllAppsColumns = numAllAppsColumns - idp.numDatabaseAllAppsColumns = numAllAppsColumns + // Flexera Launcher: the drawer is a one-column list, whatever the column preference says. + idp.numAllAppsColumns = app.flexera.drawer.FlexeraDrawer.COLUMNS + idp.numDatabaseAllAppsColumns = app.flexera.drawer.FlexeraDrawer.COLUMNS idp.numFolderRows[INDEX_DEFAULT] = numFolderRows idp.numFolderColumns[INDEX_DEFAULT] = numFolderColumns diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 4e22812b41..2bf7f0ce2f 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -1979,6 +1979,14 @@ public class DeviceProfile { /** The margin between the edge of all apps and the edge of the first icon. */ public int getAllAppsIconStartMargin(Context context) { + // Flexera Launcher: in the one-column drawer list the first icon sits at the row's start + // padding. The grid maths below would instead centre it in a full-width cell, which + // squeezed the search bar down to a small pill. + if (numShownAllAppsColumns == app.flexera.drawer.FlexeraDrawer.COLUMNS) { + return (Utilities.isRtl(context.getResources()) ? allAppsPadding.right + : allAppsPadding.left) + + app.flexera.drawer.FlexeraDrawer.rowStartPadding(context); + } int allAppsSpacing; if (isVerticalBarLayout()) { // On phones, the landscape layout uses a different setup. diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 7dadc50a76..7301fedb73 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -491,6 +491,17 @@ public class AlphabeticalAppsList implement Log.d(TAG, "Adding apps with sections. HasPrivateApps: " + hasPrivateApps); for (int i = 0; i < appList.size(); i++) { AppInfo info = appList.get(i); + // Flexera Launcher: start each letter group with a header row (PRD AD-3). The header + // takes this position, so the fast-scroll section created below lands on the header. + // Private Space keeps Launcher3's own framing and gets no headers. + if (!hasPrivateApps && !info.sectionName.equals(lastSectionName)) { + mAdapterItems.add(app.flexera.drawer.FlexeraDrawer.sectionHeader(info.sectionName)); + Log.d(TAG, "addAppsWithSections: adding sectionName: " + info.sectionName + + " with header at position: " + position); + lastSectionName = info.sectionName; + mFastScrollerSections.add(new FastScrollSectionInfo(info.sectionName, position)); + position++; + } // Apply decorator to private apps. if (hasPrivateApps) { mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info, diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 6f995b5ae3..e06e668151 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -243,7 +243,13 @@ public abstract class BaseAllAppsAdapter ex // Ensure the all apps icon height matches the workspace icons in portrait mode. icon.getLayoutParams().height = mActivityContext.getDeviceProfile().getAllAppsProfile().getCellHeightPx(); + // Flexera Launcher: the drawer is a list, so size and style this as a row. + app.flexera.drawer.FlexeraDrawer.styleAppRow(icon); return new ViewHolder(icon); + case app.flexera.drawer.FlexeraDrawer.VIEW_TYPE_SECTION_HEADER: + // Flexera Launcher: letter header that starts each group in the drawer list. + return new ViewHolder(app.flexera.drawer.FlexeraDrawer.createSectionHeader( + mLayoutInflater, parent)); case VIEW_TYPE_EMPTY_SEARCH: return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, false)); @@ -325,6 +331,11 @@ public abstract class BaseAllAppsAdapter ex } break; } + case app.flexera.drawer.FlexeraDrawer.VIEW_TYPE_SECTION_HEADER: + // Flexera Launcher: letter header in the drawer list. + app.flexera.drawer.FlexeraDrawer.bindSectionHeader( + holder.itemView, mApps.getAdapterItems().get(position)); + break; case VIEW_TYPE_EMPTY_SEARCH: { AppInfo info = mApps.getAdapterItems().get(position).itemInfo; if (info != null) {