-
Notifications
You must be signed in to change notification settings - Fork 0
003 grid reflow on column decrease
Date: 2026-02-13 Branch: dev Status: Implemented, builds successfully
When the user decreases the column count in settings, workspace items that no longer fit were permanently deleted from the database by LoaderCursor.checkItemPlacement(). This change adds an in-place database reflow that rearranges displaced items (moving them to available cells or new overflow screens) and resizes oversized widgets, instead of deleting them. Hotseat items are also preserved via a monotonic database count.
The AOSP grid migration system (GridSizeMigrationLogic) only runs when DeviceGridState.isCompatible() returns false, which checks dbFile equality. Since the square grid override changes numColumns without changing dbFile, needsToMigrate() returns false, no migration runs, and checkItemPlacement() deletes any items whose cellX + spanX > numColumns.
Increasing columns back doesn't restore the items -- they're gone from the database permanently.
Two complementary mechanisms:
-
SquareGridReflow -- An in-place database reflow that runs after migration but before item loading. Detects column decreases by comparing persisted
DeviceGridStatewith the current one, then rearranges items in a single SQLite transaction. -
Monotonic hotseat DB count --
numDatabaseHotseatIconsnever shrinks, socheckItemPlacement()won't reject hotseat items at positions from a previous (higher) column count. Items beyondnumShownHotseatIconsare invisible but preserved.
-
src/com/android/launcher3/model/SquareGridReflow.java-- In-place DB reflow utility (~275 lines)
-
src/com/android/launcher3/model/LoaderTask.java-- HookSquareGridReflow.reflowIfNeeded()after migration, before item loading -
src/com/android/launcher3/InvariantDeviceProfile.java-- Monotonic hotseat DB count viaHOTSEAT_MAX_DB_COUNTpreference -
src/com/android/launcher3/LauncherPrefs.kt-- AddedHOTSEAT_MAX_DB_COUNTconstant
-
HOTSEAT_MAX_DB_COUNT-- NewbackedUpItem("pref_hotseat_max_db_count", -1). Tracks the highestnumDatabaseHotseatIconsvalue ever used.
-
Monotonic hotseat tracking -- After setting
numDatabaseHotseatIcons = Math.max(numDatabaseHotseatIcons, userColumns), readsHOTSEAT_MAX_DB_COUNT. If the current value is higher, persists it. If the persisted value is higher, uses it instead. This ensurescheckItemPlacement()never rejects hotseat items at old positions.
-
Reflow hook -- In
loadWorkspaceImpl(), afterattemptMigrateDb()and beforeloadDefaultFavoritesIfNecessary():
if (mIDP.isSquareGrid) {
SquareGridReflow.reflowIfNeeded(mContext, dbController.getDb(), mIDP);
}The reflow has two phases: detection and rearrangement.
Detection (reflowIfNeeded):
- Compares persisted
DeviceGridState(context)column count with currentDeviceGridState(idp) - Only reflows when columns decreased
- Always saves the current state to prevent stale comparisons
- Skips on first load (persisted columns <= 0)
Rearrangement (reflow):
-
Visible row computation -- Uses
getMaxVisibleRows()which readsDeviceProfile.numRowsfromidp.supportedProfiles(already computed byderiveSquareGridRows()at this point). This ensures items are placed within visible rows, not the DB capacity of 20. -
Span clamping -- Any item with
spanX > newColsis clamped tospanX = newColswithcellX = 0. Any item withspanY > visibleRowsis clamped similarly. No items are dropped -- a slightly undersized widget is better than a deleted one. -
Per-screen placement (Pass 1) -- Items that still fit at their original
(cellX, cellY)keep their position.GridOccupancytracks occupied cells. Smartspace row (row 0 on screen 0) is reserved when QSB is enabled. -
Per-screen overflow (Pass 2) -- Displaced items are placed on the same screen via
GridOccupancy.findVacantCell(). -
New screens (Pass 3) -- Items that can't fit on any existing screen are placed on newly created screens (
maxScreenId + 1, incrementing). Screens are implicitly created -- AOSP derives the screen set from items viaBgDataModel.collectWorkspaceScreens(). -
DB transaction -- All moved items are batch-updated in a single SQLite transaction:
UPDATE favorites SET cellX=?, cellY=?, spanX=?, spanY=?, screen=? WHERE _id=?.
User decreases columns in Settings
-> InvariantDeviceProfile.onConfigChanged()
-> Model reload -> LoaderTask.loadWorkspaceImpl()
-> attemptMigrateDb() -> needsToMigrate() returns false (same dbFile)
-> SquareGridReflow.reflowIfNeeded()
-> Detects column decrease via DeviceGridState comparison
-> Clamps oversized spans
-> Pass 1: keep items at original positions if they fit
-> Pass 2: findVacantCell for displaced items on same screen
-> Pass 3: overflow items to new screens
-> Batch UPDATE in single transaction
-> Saves new DeviceGridState
-> loadDefaultFavoritesIfNecessary()
-> Load items from DB
-> checkItemPlacement() -> all items now fit, no deletions
| Scenario | Handling |
|---|---|
| Columns increase | No reflow needed. Items already fit. State saved. |
Widget spanX > newCols
|
Clamped to newCols, cellX set to 0. User can manually resize after. |
Widget spanY > visibleRows
|
Clamped to visible rows. |
| First load (no persisted state) |
getColumns() <= 0, skip reflow, save initial state. |
| Spacing change only (same columns) | No reflow (columns unchanged). |
| Hotseat items beyond visible count | Preserved in DB via monotonic numDatabaseHotseatIcons. Invisible but not deleted. Reappear when columns increase. |
| Folders/app-pairs | 1x1 items, reflow like shortcuts. Children reference folder by ID, unaffected. |
| Screen 0 smartspace | Row 0 reserved for QSB (follows AOSP pattern). |
| Items on overflow screens | Visible because AOSP derives screens from items (collectWorkspaceScreens()). |
| Idempotency | Second load sees oldCols == newCols, skips reflow. |
Three bugs were found and fixed in the initial implementation:
-
Widget drop logic was fatally flawed --
getWidgetMinSpanX()fell back to the originalspanX(the oversized value) whenLauncherAppWidgetProviderInfo.minSpanXwas 0 (uninitialized --initSpans()hasn't run at reflow time). This caused widgets to be dropped from the reflow, left untouched in the DB, and then permanently deleted bycheckItemPlacement(). Fixed by removing the widget-specific drop logic entirely and always clamping. -
Used DB row capacity (20) instead of visible rows --
GridOccupancyusedidp.numRows(20) instead of the actual visible row count fromDeviceProfile.deriveSquareGridRows(). Overflow items were placed at rows 7-19, invisible on a ~6-row screen. Fixed by readingDeviceProfile.numRowsfromidp.supportedProfiles. -
Widget
spanYnot checked -- Widgets taller than the visible row count were not clamped, causing vertical overflow. Fixed by clampingspanYtovisibleRows.
Architecture
Guides
Changes
- 031 App Icon & Clipping Fix
- 030 Folder Refactoring
- 029 Folder Features
- 028 Settings Polish
- 027 M3 Expressive
- 026 Code Quality
- 025 Wallpaper Scroll
- 024 Dead Code Cleanup
- 023 Search Perf
- 022 Settings Colors
- 021 Render Overrides
- 020 Per-App Icons
- 019 Toolbar & Preview
- 018 Colors Reorg
- 017 Search Animation
- 016 Universal Search
- 015 App Visibility
- 014 Settings Polish
- 013 Drawer Colors Fix
- 012 Drawer Colors
- 011 Notification Shade
- 010 Drawer Labels
- 009 M3 Drawer
- 008 M3 Settings
- 007 Icon Size
- 006 Icon Packs
- 005 Drawer Cleanup
- 004 Defaults & Gaps
- 003 Grid Reflow
- 002 Hotseat Row
- 001 Square Grid