-
Notifications
You must be signed in to change notification settings - Fork 0
widget stack
How multiple widgets can be stacked in a single grid cell group, allowing users to swipe between them horizontally.
The widget stack system has four layers:
WidgetStackInfo (data model, extends CollectionInfo)
|
v
WidgetStackView (FrameLayout with swipe gestures + dot indicators)
|
v
AppWidgetHostView[] (child widget views, one visible at a time)
|
v
WidgetDropHighlight (spring-animated drop feedback)
WidgetStackInfo is the data model, analogous to FolderInfo for folders. It stores an ordered list of LauncherAppWidgetInfo children and the currently active index. Persisted to the favorites DB as ITEM_TYPE_WIDGET_STACK (value 12).
WidgetStackView is the view layer. It extends FrameLayout, implements DraggableView and Reorderable, and manages the swipe gesture, animations, and page indicator dots. Only one child widget is visible at a time.
WidgetDropHighlight is a shared spring-animated visual feedback helper used during drag-and-drop hover, both for creating new stacks and adding to existing ones.
Widget stacks reuse the existing favorites table with these conventions:
| Column | Stack Row | Child Widget Row |
|---|---|---|
itemType |
12 (WIDGET_STACK) |
4 (APPWIDGET) or 5 (CUSTOM_APPWIDGET) |
container |
CONTAINER_DESKTOP or CONTAINER_HOTSEAT
|
Stack's _id (positive integer) |
screen |
Screen ID | Same as parent stack |
cellX, cellY
|
Grid position |
-1 (no individual position) |
spanX, spanY
|
Stack's span | Individual widget's span |
options |
Active widget index | (unused) |
rank |
(unused) | Order within stack (0-based) |
appWidgetId |
-1 (not a widget) |
Widget's actual appWidgetId |
The container > 0 convention is how the loader knows a widget belongs to a collection (folder or stack) rather than being placed directly on the workspace. The utility method LauncherSettings.Favorites.isInsideCollection(int) encapsulates this check.
File: src/com/android/launcher3/model/data/WidgetStackInfo.java
Extends CollectionInfo (the shared base for folders, app pairs, and stacks).
| Field / Constant | Description |
|---|---|
MAX_STACK_SIZE |
Hard limit of 6 widgets per stack |
contents |
ArrayList<ItemInfo> of child widgets |
activeIndex |
Index of the currently visible widget |
Key methods:
-
add(ItemInfo)-- Validates widget type and enforcesMAX_STACK_SIZE. ThrowsRuntimeExceptionon violation. -
assignWidget(LauncherAppWidgetInfo, int rank)-- Setscontainer,screenId,cellX,cellY, andrankon the widget, then callsadd(). Does not persist to DB; callers handle persistence. -
removeMatching(Predicate<ItemInfo>)-- Removes widgets matching the predicate (e.g. on package uninstall) and clampsactiveIndex. Returns count removed. -
sortByRank()-- Sorts contents by rank for consistent ordering after DB load. -
getActiveWidget()-- Returns the activeLauncherAppWidgetInfowith safe cast, ornull. -
willAcceptItemType(int)-- Static. ReturnstrueforITEM_TYPE_APPWIDGETandITEM_TYPE_CUSTOM_APPWIDGET. -
onAddToDatabase(ContentWriter)-- PersistsactiveIndexto theOPTIONScolumn.
Two predicates control how widget-related items are filtered in model operations:
| Predicate | Matches | Used By |
|---|---|---|
APP_WIDGET_FILTER |
APPWIDGET, CUSTOM_APPWIDGET
|
CacheDataUpdatedTask, PackageInstallStateChangedTask, PackageUpdatedTask, LoaderTask (first screen broadcast) |
WIDGET_OR_STACK_FILTER |
Above + WIDGET_STACK
|
BaseLauncherBinder (categorizing items for binding) |
APP_WIDGET_FILTER is used when iterating over actual widget items that have appWidgetId, providerName, etc. Widget stacks don't have these fields, so they must be excluded.
WIDGET_OR_STACK_FILTER is used when categorizing items into "widgets" vs. "workspace items" for binding -- stacks should be treated like widgets for layout purposes.
Widget stacks are loaded from the database through the standard AOSP loader pipeline with targeted extensions at each stage.
File: src/com/android/launcher3/model/LoaderCursor.java
When child widgets are loaded before their parent stack row (lower _id), the loader needs a placeholder. findOrMakeWidgetStack(int id, BgDataModel) checks the data model and a pending collection map, creating a blank WidgetStackInfo if needed.
This uses the same generic findOrMakeCollection() helper as findOrMakeFolder():
private <T extends CollectionInfo> T findOrMakeCollection(int id, BgDataModel dataModel,
Class<T> type, Supplier<T> factory) {
ItemInfo info = dataModel.itemsIdMap.get(id);
if (type.isInstance(info)) return type.cast(info);
CollectionInfo pending = mPendingCollectionInfo.get(id);
if (type.isInstance(pending)) return type.cast(pending);
T created = factory.get();
created.id = id;
mPendingCollectionInfo.put(id, created);
return created;
}When a child widget has container > 0, the cursor calls findOrMakeWidgetStack(container) and adds the widget to the returned stack.
File: src/com/android/launcher3/model/WorkspaceItemProcessor.kt
The ITEM_TYPE_WIDGET_STACK case dispatches to processWidgetStack(), which:
- Calls
findOrMakeWidgetStack(c.id, bgDataModel)to get the (possibly pre-populated) stack - Applies common properties (id, container, screenId, cellX, cellY)
- Sets spanX, spanY
- Loads active index from the
OPTIONScolumn - Adds to
BgDataModelviacheckAndAddItem()
Child widgets use Favorites.isInsideCollection(c.container) to determine they belong to a collection rather than the workspace directly.
File: src/com/android/launcher3/model/LoaderTask.java
After all items are loaded, processWidgetStackItems() iterates over all WidgetStackInfo entries and calls sortByRank() on each to ensure consistent widget ordering.
sanitizeWidgetStacks() runs after sanitizeFolders() during data loading. It calls ModelDbController.deleteEmptyWidgetStacks(), which queries for ITEM_TYPE_WIDGET_STACK rows whose _id does not appear as a container for any item, then deletes them in a transaction. Deleted stack IDs are removed from BgDataModel.itemsIdMap.
File: src/com/android/launcher3/util/ItemInflater.kt
The ITEM_TYPE_WIDGET_STACK case calls inflateWidgetStack(info, writer):
- Sorts contents by rank
- Creates a new
WidgetStackViewand sets its tag to theWidgetStackInfo - Iterates through children, inflating each via
widgetInflater.inflateAppWidget() - Handles
TYPE_DELETE(removes corrupt widgets from DB) andTYPE_PENDING(pending widgets) - Calls
stackView.addWidgetView()for each successfully inflated child - Removes dead children (those that failed inflation) from the in-memory
WidgetStackInfo.contentslist to keep the model in sync with views - Sets the active index from
WidgetStackInfo
Safe casts (as?) protect against unexpected child types with diagnostic logging.
File: src/com/android/launcher3/widget/WidgetStackView.java
A FrameLayout that manages multiple AppWidgetHostView children. Implements:
-
DraggableView-- So the entire stack can be long-press dragged as a unit -
Reorderable-- So workspace reorder animations work correctly
Children are stored in mWidgetViews (ArrayList). Only the active widget is VISIBLE; all others are INVISIBLE. Individual widget long-press is disabled (setLongClickable(false)) so the stack handles drag as a unit.
addWidgetView(AppWidgetHostView, LauncherAppWidgetInfo) adds a child, resetting any stale transform state and updating visibility.
rebuildFromStackInfo() synchronizes the view list with the data model after mutations (e.g. package uninstall), removing stale child views and clamping the active index.
reInflateChildWidget(PendingAppWidgetHostView, LauncherAppWidgetInfo, Launcher) replaces a pending widget in-place when it completes restore. Creates a new view via WidgetInflater, handles TYPE_DELETE (removes from stack and DB), calls prepareAppWidget() for proper focusability, and swaps the view in the child list.
findChildByAppWidgetId(int) searches the stack's child views for a LauncherAppWidgetHostView matching the given app widget ID.
forEachWidgetHostView(Consumer<LauncherAppWidgetHostView>) iterates over child views that are LauncherAppWidgetHostView instances, used by Launcher for batch operations like deferring/resuming updates.
attachChildWidgetsToHost(LauncherWidgetHolder) reattaches children after async workspace inflation so they receive remote view updates.
The horizontal swipe system uses a three-phase approach:
-
Intercept (
onInterceptTouchEvent) -- Detects horizontal vs. vertical gesture usingmTouchSlop. Claims parent touch for horizontal swipes (requestDisallowInterceptTouchEvent). Releases for vertical gestures so Workspace page scrolling works. -
Track (
onTouchEvent) -- TracksmSwipeOffsetand applies live translation/scale to the current and peek widgets viaapplySwipeTranslation(). -
Resolve -- On
ACTION_UP, checks fling velocity (threshold: 500 dp/s) and drag fraction (threshold: 50% of width). CallssnapToIndex()for successful swipes orsnapBack()for failed ones.
During a live swipe, two widgets are visible simultaneously:
- Current widget: Scales from 1.0 down to 0.92, translates with finger
- Peek widget: Scales from 0.92 up to 1.0, slides in from the edge
Both snapToIndex() and snapBack() delegate to a shared animateSnap() helper:
private void animateSnap(Interpolator interpolator,
SnapUpdateCallback updateCallback, Runnable endAction)| Animation | Interpolator | Duration |
|---|---|---|
| Snap to next | Interpolators.EMPHASIZED |
M3Durations.LONG_2 (450ms) |
| Snap back | Interpolators.EMPHASIZED_DECELERATE |
M3Durations.LONG_2 (450ms) |
Dots are drawn in dispatchDraw() as a canvas overlay (not child views):
| Property | Value |
|---|---|
| Radius | 3dp |
| Spacing | 8dp |
| Bottom margin | 8dp |
| Active color |
materialColorPrimary at 100% alpha |
| Inactive color |
materialColorOutline at 40% alpha |
| Show animation |
M3Durations.SHORT_3 (100ms), Interpolators.STANDARD
|
| Hide delay | 1500ms after swipe ends |
| Hide animation |
M3Durations.SHORT_4 (150ms), Interpolators.STANDARD
|
Dots auto-show when a swipe begins and auto-hide after the swipe animation completes.
Five provider-info methods use a shared accumulateFromProviderInfo() helper to iterate over all child widgets:
| Method | Identity | Accumulator | Meaning |
|---|---|---|---|
getMinSpanX |
1 | Math.max |
Most restrictive minimum |
getMinSpanY |
1 | Math.max |
Most restrictive minimum |
getMaxSpanX |
MAX_VALUE |
Math.min |
Most restrictive maximum |
getMaxSpanY |
MAX_VALUE |
Math.min |
Most restrictive maximum |
getResizeMode |
BOTH |
& (AND) |
Intersection of capabilities |
expandSpanIfNeeded() grows the stack's span to accommodate a new widget, clamped to InvariantDeviceProfile.numColumns/numRows.
updateChildWidgetSizes() propagates span changes to all children with an optional ModelWriter for DB persistence.
- Content description: "Widget %d of %d" (
widget_stack_page_description) - Scroll actions:
ACTION_SCROLL_FORWARD/ACTION_SCROLL_BACKWARD - Both actions trigger
snapToIndex()for animated navigation
onDetachedFromWindow() cancels all pending operations: long-press timer, snap animator, velocity tracker, dot hide runnable, dot fade animator, drop highlight, and debounced active-index save.
The active widget index is persisted to the DB (via the OPTIONS column) so it survives launcher restarts. To avoid excessive I/O from rapid swipes, persistence is debounced with a 1.5s delay (scheduleActiveIndexSave()). Each new swipe cancels the pending save and schedules a fresh one.
File: src/com/android/launcher3/widget/WidgetDropHighlight.java
Spring-animated visual feedback for drag hover over a widget or stack.
| Property | Value |
|---|---|
| Child scale at full highlight | 0.85 (15% reduction) |
| Background color | materialColorPrimaryContainer |
| Background alpha at full highlight | 230/255 (~90%) |
| Background corner radius | RoundedCornerEnforcement.computeEnforcedRadius() |
| Show spring | M3 bounce gentle (damping/stiffness from R.dimen.m3_bounce_gentle_*) |
| Clear spring | M3 bounce smooth (damping/stiffness from R.dimen.m3_bounce_smooth_*) |
Uses SpringAnimationBuilder with a FloatProperty<WidgetDropHighlight> for progress (0.0 = rest, 1.0 = full highlight). The progress drives both child scale and background alpha.
Used by:
-
WidgetStackView.showDropHighlight()-- When hovering a widget over an existing stack -
Workspace.onDragOver()-- When hovering a widget over another widget to create a stack
Trigger: Dragging a widget over another standalone widget on the workspace.
Flow:
-
Validation (
Workspace.willCreateWidgetStack()):- Both items must be widget types (
willAcceptItemType()) - Can't stack a widget onto itself
- Computes union span (
max(source.spanX, dest.spanX), same for Y) - Both widgets must support the union span (checked via provider info min/max span)
- Both items must be widget types (
-
Hover feedback (
Workspace.onDragOver()):- Creates a
WidgetDropHighlighton the target widget'sViewGroup - Calls
show()for spring animation - Sets drag mode to
DRAG_MODE_CREATE_WIDGET_STACK
- Creates a
-
Drop (
Workspace.createWidgetStackIfNecessary()):- Detaches source widget from DragView, removes from workspace
- Removes target widget from workspace
- Calls
Launcher.addWidgetStack()to createWidgetStackView+WidgetStackInfoat target's cell position - Assigns target widget at rank 0, source widget at rank 1
- Persists via
ModelWriter.modifyItemInDatabase()(existing widget) andaddItemToDatabase()(stack itself) - Adds both host views to the stack
- Sets active index to the newly added widget
- Animates drag view into stack position
Trigger: Dragging a widget over an existing WidgetStackView.
Flow:
-
Validation (
Workspace.willAddToExistingWidgetStack()):- Target must be
WidgetStackView - Dragged item must be widget type
- Stack must not be full (
< MAX_STACK_SIZE)
- Target must be
-
Hover feedback (
Workspace.onDragOver()):- Calls
stackView.showDropHighlight()(delegates toWidgetDropHighlight) - Sets drag mode to
DRAG_MODE_ADD_TO_WIDGET_STACK
- Calls
-
Drop (
Workspace.addToExistingWidgetStackIfNecessary()):- Detaches source widget from DragView
- Assigns widget to stack at next rank
- Calls
expandSpanIfNeeded()to accommodate larger widget - Adds host view to stack
- Sets active index to newly added widget
When a widget is dragged from the widget picker onto the workspace over an existing widget or stack:
-
Drag exit (
Workspace.onDropExternal()):- Stores the target in
mPendingExternalStackTarget(for existing stacks) ormPendingExternalStackCreationTarget(for new stacks) - Widget binding proceeds normally
- Stores the target in
-
Bind completion (
Launcher.completeAddAppWidget()):- Checks
consumePendingExternalStackTarget()-- if set, callsaddWidgetToExistingStack() - Checks
consumePendingExternalStackCreationTarget()-- if set, callscreateStackFromExternalDrop() - Both methods use
assignWidget()for field mutation and explicitModelWritercalls for persistence
- Checks
File: src/com/android/launcher3/AppWidgetResizeFrame.java
showForWidgetStack(WidgetStackView, CellLayout) creates a resize frame for the stack:
- Uses
setupForWidgetStack()instead of the standard widget setup - Queries min/max spans and resize mode from the stack (composite of all children)
- On resize commit, calls
wsv.updateChildWidgetSizes()to propagate new spans to all children - Sets content description: "Widget stack with %d widgets"
When a package is uninstalled, Workspace.removeItemsByMatcher() handles widget stacks alongside folders and app pairs:
} else if (child instanceof WidgetStackView wsv) {
WidgetStackInfo stackInfo = wsv.getStackInfo();
if (stackInfo != null) {
int removed = stackInfo.removeMatching(matcher);
if (removed > 0) {
if (stackInfo.getContents().isEmpty()) {
layout.removeViewInLayout(child);
mLauncher.getModelWriter().deleteItemFromDatabase(
stackInfo, "empty widget stack after item removal");
} else {
wsv.rebuildFromStackInfo();
}
}
}
}WidgetStackInfo.removeMatching() iterates backwards through contents, removes matched items, and clamps the active index.
WidgetStackView.rebuildFromStackInfo() builds a set of valid IDs from the stack info, removes stale child views, and updates visibility.
File: src/com/android/launcher3/graphics/DragPreviewProvider.java
WidgetStackView is treated like LauncherAppWidgetHostView for drag preview generation:
-
createDrawable()returnsnull(uses content view instead) -
getContentView()returns the stack view itself -
prepareDrawDragView()hides non-active children and cancels animations for a clean snapshot
A bottom sheet for reordering, removing, and adding widgets within a stack.
The editor extends AbstractSlideInView<Launcher> (the same base class as WidgetsBottomSheet) and implements Insettable for system bar inset handling.
WidgetStackEditorView (AbstractSlideInView + Insettable)
|
v
WidgetStackEditorAdapter (RecyclerView.Adapter with 2 view types)
|
v
WidgetStackEditorItemDecoration (position-aware rounded card backgrounds)
Entry point: Long-press a widget stack on the workspace, then tap "Edit stack" in the popup menu. This calls WidgetStackEditorView.show(Launcher, WidgetStackView).
The editor is a standard M3 modal bottom sheet:
- Drag handle (32x4dp,
outlineVariant) - Centered title (
MaterialToolbarwithtitleCentered="true") -
RecyclerViewwith card-grouped items - Last item is an "Add widget" action row (not a separate button)
| Type | Layout | Description |
|---|---|---|
TYPE_WIDGET (0) |
widget_stack_editor_item |
Widget row: 88dp min height, 64dp preview, drag handle, remove button |
TYPE_ADD (1) |
Same layout, reconfigured | Action row: 56dp min height, 40dp + icon, no drag/remove |
The add row is hidden when the stack is full (MAX_STACK_SIZE).
WidgetStackEditorItemDecoration draws position-aware rounded-rect card backgrounds behind each item:
| Position | Top corners | Bottom corners |
|---|---|---|
| First (solo) | 28dp | 28dp |
| First | 28dp | 4dp |
| Middle | 4dp | 4dp |
| Last (add row) | 4dp | 28dp |
Items being dragged are skipped by the decoration -- the drag animation draws their background instead.
Ripple drawables with matching corner radii are applied per-item for touch feedback.
Uses ItemTouchHelper with guards to exclude the add row:
-
getMovementFlags()returns 0 for the add row -
canDropOver()returns false for the add row -
onMove()swaps items inWidgetStackInfo.contentsand callsnotifyItemMoved()
Animated transitions on drag pickup and drop using AnimatorSet:
| Property | Pickup | Drop |
|---|---|---|
| Corner radii | Position-aware -> all 28dp | All 28dp -> position-aware |
| Elevation (translationZ) | 0 -> 8dp | 8dp -> 0 |
| Scale | 1.0 -> 1.02 | 1.02 -> 1.0 |
| Background color | colorSurfaceContainerHighest |
(animated back, cleared on end) |
| Duration |
M3Durations.SHORT_2 (100ms) |
M3Durations.SHORT_2 (100ms) |
| Interpolator | EMPHASIZED_DECELERATE |
EMPHASIZED_ACCELERATE |
Both the Widgets bottom sheet and the Widget Stack Editor resolve colors from WidgetContainerTheme for consistency:
| Layer | Theme attr | Token |
|---|---|---|
| Sheet background | widgetPickerPrimarySurfaceColor |
colorSurfaceContainerLow |
| Item card backgrounds | widgetPickerSecondarySurfaceColor |
colorSurfaceContainer |
| Dragged item | colorSurfaceContainerHighest |
(M3 attr, resolved from inflate context) |
| Title text | -- | materialColorOnSurface |
| Supporting text / icons | -- | materialColorOnSurfaceVariant |
| Add row icon | -- | materialColorPrimary |
The WidgetContainerTheme tokens were updated from surfaceContainerHigh to surfaceContainerLow for the primary surface to match the M3 bottom sheet spec.
The editor is inflated with HomeSettings_Theme + DynamicColors (needed for MaterialToolbar and MaterialButton), but colors are resolved from a separate WidgetContainerTheme context via getWidgetThemeContext(). This ensures both sheets use the same widgetPicker* attrs regardless of their inflation context.
Matches BaseWidgetSheet pattern:
-
Insettableinterface receives insets fromDragLayer -
setupNavBarColor()usesmActivityContext(Launcher) to readisMainColorDark - Nav bar scrim drawn in
dispatchDraw()as a filled rect - RecyclerView bottom padding accounts for nav bar height
WidgetStackPopupHelper adds "Edit stack" and "Remove stack" shortcuts to the long-press popup via PopupContainerWithArrow. These are system shortcuts that appear alongside the standard widget actions.
| File | Purpose |
|---|---|
src/.../model/data/WidgetStackInfo.java |
Data model (extends CollectionInfo) |
src/.../widget/WidgetStackView.java |
View layer (FrameLayout + swipe + dots) |
src/.../widget/WidgetDropHighlight.java |
Spring-animated drop feedback |
src/.../widget/WidgetStackEditorView.java |
Bottom sheet editor (AbstractSlideInView) |
src/.../widget/WidgetStackEditorAdapter.java |
RecyclerView adapter (widget rows + add row) |
src/.../widget/WidgetStackEditorItemDecoration.java |
Position-aware card backgrounds |
src/.../widget/WidgetStackPopupHelper.java |
Popup menu shortcuts (edit/remove stack) |
res/layout/widget_stack_editor.xml |
Editor bottom sheet layout |
| File | Changes |
|---|---|
LauncherSettings.java |
ITEM_TYPE_WIDGET_STACK = 12, isInsideCollection(), itemTypeToString()
|
Launcher.java |
addWidgetStack(), addWidgetToExistingStack(), createStackFromExternalDrop(), picker completion |
Workspace.java |
Stack creation/addition drop logic, hover feedback, package uninstall, external picker targets |
AppWidgetResizeFrame.java |
showForWidgetStack(), setupForWidgetStack(), stack resize handling |
DragPreviewProvider.java |
WidgetStackView recognized for content-view drag preview |
ModelUtils.java |
APP_WIDGET_FILTER, WIDGET_OR_STACK_FILTER predicates |
LoaderCursor.java |
findOrMakeWidgetStack(), generic findOrMakeCollection()
|
WorkspaceItemProcessor.kt |
processWidgetStack(), isInsideCollection check for child widgets |
LoaderTask.java |
processWidgetStackItems() post-processing, APP_WIDGET_FILTER usage |
ItemInflater.kt |
inflateWidgetStack() with safe casts |
BaseLauncherBinder.java |
Uses WIDGET_OR_STACK_FILTER for item categorization |
| File | Change |
|---|---|
CacheDataUpdatedTask.java |
Uses APP_WIDGET_FILTER (no redundant instanceof) |
PackageInstallStateChangedTask.java |
Uses APP_WIDGET_FILTER (no redundant instanceof) |
PackageUpdatedTask.java |
Uses APP_WIDGET_FILTER (no redundant instanceof) |
| Resource | Purpose |
|---|---|
res/values/strings.xml |
widget_stack_description, widget_stack_page_description, widget_stack_editor_title, widget_stack_add_widget, widget_stack_remove_widget, widget_stack_drag_handle
|
res/layout/widget_stack_editor.xml |
Editor bottom sheet layout |
res/layout/widget_stack_editor_item.xml |
Editor list item (shared by widget rows and add row) |
res/drawable/ic_add_widget.xml |
24dp plus icon for add row |
res/drawable/ic_drag_handle.xml |
Drag handle icon |
res/drawable/ic_edit.xml |
Edit icon for popup shortcut |
res/drawable/bg_preview_rounded.xml |
Rounded preview thumbnail background |
res/drawable/bg_icon_badge.xml |
App icon badge background |
res/values/ids.xml |
editor_drag_state_tag, editor_ripple_tag
|
Widget stacks follow the same "parent container with child items" pattern as folders. By extending CollectionInfo, stacks inherit the _id-based containment model, BgDataModel integration, and the mPendingCollectionInfo placeholder system from the loader.
Unlike CONTAINER_DESKTOP (-100) or CONTAINER_HOTSEAT (-101), widget stacks use their row ID as the container value for children. This matches the existing folder convention: container > 0 means "inside the item with _id = container". The isInsideCollection() utility method documents this convention.
assignWidget() only mutates fields. The three callers (Workspace.addWidgetToStack, Launcher.addWidgetToExistingStack, Launcher.createStackFromExternalDrop) each need different persistence patterns -- INSERT for new widgets from the picker, UPDATE for existing widgets being moved. Separating mutation from persistence makes this explicit at each call site.
Six widgets is enough to be useful without making the dot indicator illegible or the swipe navigation tedious. The limit is enforced at both the data level (WidgetStackInfo.add()) and the UI level (willAddToExistingWidgetStack() rejects drops when full).
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