diff --git a/lawnchair/res/layout/hotseat_page.xml b/lawnchair/res/layout/hotseat_page.xml new file mode 100644 index 0000000000..38eebe453f --- /dev/null +++ b/lawnchair/res/layout/hotseat_page.xml @@ -0,0 +1,26 @@ + + + diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 5a1efc69d0..f77f26a012 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -782,6 +782,7 @@ Dock icons Dock rows + Dock pages %1$s (folded) %1$s (unfolded) Folded diff --git a/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt b/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt index 9caecbb91d..01b9ad6d36 100644 --- a/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt +++ b/lawnchair/src/app/lawnchair/DeviceProfileOverrides.kt @@ -103,6 +103,7 @@ class DeviceProfileOverrides @Inject constructor( val enableTaskbarOnPhone: Boolean, val numHotseatRows: Int = 1, + val numDockPages: Int = 1, // Foldable overrides (-1 means don't override) val foldableShownHotseatIcons: Int = -1, @@ -132,6 +133,7 @@ class DeviceProfileOverrides @Inject constructor( enableTaskbarOnPhone = prefs2.enableTaskbarOnPhone.firstCached(), numHotseatRows = prefs.hotseatRows.get().coerceIn(1, 2), + numDockPages = prefs.dockPages.get().coerceIn(1, 5), foldableShownHotseatIcons = if (deviceType == InvariantDeviceProfile.TYPE_MULTI_DISPLAY) { val folded = prefs.hotseatColumns.get() @@ -172,8 +174,8 @@ class DeviceProfileOverrides @Inject constructor( idp.numDatabaseAllAppsColumns = foldableDatabaseAllAppsColumns } - // Ensure database can hold enough icons for multi-row dock - val requiredSlots = idp.numShownHotseatIcons * numHotseatRows + // Ensure database can hold enough icons for multi-row / multi-page dock + val requiredSlots = idp.numShownHotseatIcons * numHotseatRows * numDockPages if (idp.numDatabaseHotseatIcons < requiredSlots) { idp.numDatabaseHotseatIcons = requiredSlots } diff --git a/lawnchair/src/app/lawnchair/hotseat/HotseatPagedView.kt b/lawnchair/src/app/lawnchair/hotseat/HotseatPagedView.kt new file mode 100644 index 0000000000..b8ea654afd --- /dev/null +++ b/lawnchair/src/app/lawnchair/hotseat/HotseatPagedView.kt @@ -0,0 +1,136 @@ +package app.lawnchair.hotseat + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import com.android.launcher3.CellLayout +import com.android.launcher3.DeviceProfile +import com.android.launcher3.PagedView +import com.android.launcher3.R +import com.android.launcher3.Workspace +import com.android.launcher3.pageindicators.PageIndicatorDots + +/** + * Horizontally pageable container for dock [CellLayout] pages. + */ +class HotseatPagedView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : PagedView(context, attrs, defStyle) { + + fun interface OnDockPageChangeListener { + fun onDockPageChanged(page: Int) + } + + var isPagingEnabled: Boolean = false + private set(value) { + field = value + applyPageIndicatorVisibility() + } + + private var onDockPageChangeListener: OnDockPageChangeListener? = null + + init { + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + } + + fun setPageIndicator(indicator: PageIndicatorDots?) { + mPageIndicator = indicator + mPageIndicator?.let { + it.setMarkersCount(childCount) + it.setActiveMarker(nextPage) + } + applyPageIndicatorVisibility() + } + + private fun applyPageIndicatorVisibility() { + val indicator = mPageIndicator ?: return + if (isPagingEnabled) { + indicator.visibility = VISIBLE + // Same as workspace: show while paging, fade out when idle. + indicator.setShouldAutoHide(true) + } else { + indicator.setShouldAutoHide(false) + indicator.visibility = GONE + } + } + + override fun getPageAt(index: Int): CellLayout? = getChildAt(index) as? CellLayout + + fun getCurrentCellLayout(): CellLayout? = getPageAt(nextPage) + + fun resetPages(hasVerticalHotseat: Boolean, workspace: Workspace<*>?, dp: DeviceProfile) { + removeAllViews() + val pageCount = if (hasVerticalHotseat) 1 else maxOf(1, dp.numHotseatPages) + isPagingEnabled = pageCount > 1 + + val inflater = LayoutInflater.from(context) + for (i in 0 until pageCount) { + val page = inflater.inflate(R.layout.hotseat_page, this, false) as CellLayout + page.setHotseatPageIndex(i) + if (workspace != null) { + page.setCellLayoutContainer(workspace) + } + page.resetCellSize(dp) + page.isLongClickable = false + page.isHapticFeedbackEnabled = false + if (hasVerticalHotseat) { + page.setGridSize(1, dp.numShownHotseatIcons) + } else { + page.setGridSize(dp.numShownHotseatIcons, dp.numHotseatRows) + } + addView(page, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) + } + + mPageIndicator?.let { + it.setMarkersCount(pageCount) + it.setActiveMarker(0) + } + applyPageIndicatorVisibility() + setCurrentPage(0) + requestLayout() + } + + fun setOnDockPageChangeListener(listener: OnDockPageChangeListener?) { + onDockPageChangeListener = listener + } + + override fun onPageEndTransition() { + super.onPageEndTransition() + onDockPageChangeListener?.onDockPageChanged(nextPage) + } + + override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) { + super.onScrollChanged(l, t, oldl, oldt) + if (mMaxScroll > 0) { + mPageIndicator?.setScroll(l, mMaxScroll) + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (!isPagingEnabled) return false + return super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + if (!isPagingEnabled) return false + return super.onTouchEvent(ev) + } + + /** Returns the page CellLayout under the given x in this view's coordinates. */ + fun findPageAtLocalX(x: Float): CellLayout? { + val width = measuredWidth + if (width <= 0 || childCount == 0) { + return getCurrentCellLayout() + } + var raw = (scrollX + x).toInt() / width + if (mIsRtl) { + raw = childCount - 1 - raw + } + val page = raw.coerceIn(0, childCount - 1) + return getPageAt(page) + } +} diff --git a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt index ef5f585e08..31b772ea25 100644 --- a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt +++ b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt @@ -101,6 +101,7 @@ class PreferenceManager @Inject constructor( val hotseatColumns = IntPref("pref_hotseatColumns", calculatedGridSpec.hotseatColumns, reloadGrid) val hotseatColumnsUnfolded = IntPref("pref_hotseatColumnsUnfolded", calculatedGridSpec.hotseatColumnsUnfolded, reloadGrid) val hotseatRows = IntPref("pref_hotseatRows", 1, reloadGrid) + val dockPages = IntPref("pref_dockPages", 1, reloadGrid) val workspaceColumns = IntPref("pref_workspaceColumns", calculatedGridSpec.workspaceColumns) val workspaceRows = IntPref("pref_workspaceRows", calculatedGridSpec.workspaceRows) val workspaceIncreaseMaxGridSize = BoolPref("pref_workspace_increase_max_grid_size", false) diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt index 3eccfd5c73..acc580686b 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/DockPreferences.kt @@ -139,6 +139,7 @@ fun GridSettings(prefs: PreferenceManager, prefs2: PreferenceManager2) { val hotseatColumnsAdapter = prefs.hotseatColumns.getAdapter() val hotseatColumnsUnfoldedAdapter = prefs.hotseatColumnsUnfolded.getAdapter() val hotseatRowsAdapter = prefs.hotseatRows.getAdapter() + val dockPagesAdapter = prefs.dockPages.getAdapter() PreferenceGroup(heading = stringResource(id = R.string.grid)) { if (isFoldable) { @@ -175,6 +176,12 @@ fun GridSettings(prefs: PreferenceManager, prefs2: PreferenceManager2) { step = 1, valueRange = 1..2, ) + SliderPreference( + label = stringResource(id = R.string.dock_pages), + adapter = dockPagesAdapter, + step = 1, + valueRange = 1..5, + ) SliderPreference( adapter = prefs2.hotseatBottomFactor.getAdapter(), label = stringResource(id = R.string.hotseat_bottom_space_label), @@ -199,12 +206,14 @@ fun ColumnScope.DockPreferencesPreview(modifier: Modifier = Modifier) { val prefs2 = preferenceManager2() val hotseatRows = prefs.hotseatRows + val dockPages = prefs.dockPages val adapters = listOf( prefs2.hotseatMode.getAdapter(), prefs.hotseatColumns.getAdapter(), prefs.hotseatColumnsUnfolded.getAdapter(), hotseatRows.getAdapter(), + dockPages.getAdapter(), prefs2.themedHotseatQsb.getAdapter(), prefs.hotseatQsbCornerRadius.getAdapter(), prefs.hotseatQsbAlpha.getAdapter(), diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java index 7c53360ea4..740997e289 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduController.java @@ -96,23 +96,53 @@ public class HotseatEduController { pageId = mLauncher.getModel().getModelDbController().getNewScreenId(); } boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout(); - int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons; - for (int i = 0; i < hotseatItemsNum; i++) { - int x = isPortrait ? i : 0; - int y = isPortrait ? 0 : hotseatItemsNum - i - 1; - View child = mHotseat.getChildAt(x, y); - if (child == null || child.getTag() == null) continue; - ItemInfo tag = (ItemInfo) child.getTag(); - if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue; - mLauncher.getModelWriter().moveItemInDatabase(tag, - LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow); - mNewItems.add(tag); + int numColumns = mLauncher.getDeviceProfile().inv.numColumns; + int destIndex = 0; + for (CellLayout page : mHotseat.getPageLayouts()) { + int countX = page.getCountX(); + int countY = page.getCountY(); + if (isPortrait) { + for (int y = 0; y < countY; y++) { + for (int x = 0; x < countX; x++) { + destIndex += migrateHotseatChild(page.getChildAt(x, y), pageId, toRow, + destIndex, numColumns); + } + } + } else { + for (int i = 0; i < countY; i++) { + destIndex += migrateHotseatChild(page.getChildAt(0, countY - i - 1), pageId, + toRow, destIndex, numColumns); + } + } } return pageId; } + /** @return 1 if the item was migrated, otherwise 0 */ + private int migrateHotseatChild(View child, int pageId, int toRow, int destIndex, + int numColumns) { + if (child == null || child.getTag() == null) { + return 0; + } + ItemInfo tag = (ItemInfo) child.getTag(); + if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { + return 0; + } + int destX = numColumns > 0 ? destIndex % numColumns : destIndex; + int destY = numColumns > 0 ? toRow - destIndex / numColumns : toRow; + if (destY < 0) { + destY = 0; + } + mLauncher.getModelWriter().moveItemInDatabase(tag, + LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, destX, destY); + mNewItems.add(tag); + return 1; + } + void moveHotseatItems() { - mHotseat.removeAllViewsInLayout(); + for (CellLayout page : mHotseat.getPageLayouts()) { + page.removeAllViewsInLayout(); + } if (!mNewItems.isEmpty()) { mLauncher.bindItemsAdded(mNewItems); } @@ -123,8 +153,9 @@ public class HotseatEduController { } void showDimissTip() { - if (mHotseat.getShortcutsAndWidgets().getChildCount() - < mLauncher.getDeviceProfile().numShownHotseatIcons) { + CellLayout page = mHotseat.getCurrentPageLayout(); + int childCount = page != null ? page.getShortcutsAndWidgets().getChildCount() : 0; + if (childCount < mLauncher.getDeviceProfile().numShownHotseatIcons) { Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, R.string.hotseat_prediction_settings, null, () -> mLauncher.startActivity(getSettingsIntent())); @@ -138,11 +169,12 @@ public class HotseatEduController { } void showEdu() { - int childCount = mHotseat.getShortcutsAndWidgets().getChildCount(); + CellLayout page = mHotseat.getCurrentPageLayout(); + int childCount = page != null ? page.getShortcutsAndWidgets().getChildCount() : 0; CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID); // hotseat is already empty and does not require migration. show edu tip boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> { - View v = mHotseat.getShortcutsAndWidgets().getChildAt(i); + View v = page.getShortcutsAndWidgets().getChildAt(i); return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; }); @@ -168,14 +200,19 @@ public class HotseatEduController { * @return whether suitable child was found and tip was shown */ private boolean showHotseatArrowTip(boolean usePinned, String message) { - int childCount = mHotseat.getShortcutsAndWidgets().getChildCount(); + CellLayout page = mHotseat.getCurrentPageLayout(); + if (page == null) { + Log.e(TAG, "Unable to find suitable view for ArrowTip"); + return false; + } + int childCount = page.getShortcutsAndWidgets().getChildCount(); boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout(); BubbleTextView tipTargetView = null; for (int i = childCount - 1; i > -1; i--) { int x = isPortrait ? i : 0; int y = isPortrait ? 0 : i; - View v = mHotseat.getShortcutsAndWidgets().getChildAt(x, y); + View v = page.getShortcutsAndWidgets().getChildAt(x, y); if (v instanceof BubbleTextView && v.getTag() instanceof WorkspaceItemInfo) { ItemInfo info = (ItemInfo) v.getTag(); boolean isPinned = info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index 621a5c9a24..f4f68f6a57 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -42,6 +42,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.Flags; +import com.android.launcher3.CellLayout; import com.android.launcher3.Hotseat; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; @@ -98,7 +99,7 @@ public class HotseatPredictionController implements DragController.DragListener, private AnimatorSet mIconRemoveAnimators; private int mPauseFlags = 0; - private List mOutlineDrawings = new ArrayList<>(); + private final List mOutlineDrawings = new ArrayList<>(); private boolean mEnableHotseatLongPressTipForTesting = true; @@ -133,7 +134,40 @@ public class HotseatPredictionController implements DragController.DragListener, mLauncher.getDragController().addDragListener(this); launcher.addOnDeviceProfileChangeListener(this); - mHotseat.getShortcutsAndWidgets().setOnHierarchyChangeListener(this); + attachPageListeners(); + mHotseat.getPagedView().setOnDockPageChangeListener(page -> { + if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) { + fillGapsWithPrediction(true); + } + }); + } + + private void attachPageListeners() { + for (CellLayout page : mHotseat.getPageLayouts()) { + page.getShortcutsAndWidgets().setOnHierarchyChangeListener(this); + } + } + + @Nullable + private CellLayout getPageForRank(int rank) { + return mHotseat.getPageAt(mHotseat.getPageFromOrder(rank)); + } + + @Nullable + private View getChildAtRank(int rank) { + CellLayout page = getPageForRank(rank); + if (page == null) { + return null; + } + return page.getChildAt( + mHotseat.getCellXFromOrder(rank), + mHotseat.getCellYFromOrder(rank)); + } + + /** Visible-page slot count (columns × rows). Predictions only fill the current page. */ + private int getVisiblePageStartRank() { + int page = mHotseat.getPagedView().getNextPage(); + return Math.max(0, page) * mHotSeatItemsCount; } @Override @@ -211,10 +245,10 @@ public class HotseatPredictionController implements DragController.DragListener, } mPauseFlags |= FLAG_FILL_IN_PROGRESS; - for (int rank = 0; rank < mHotSeatItemsCount; rank++) { - View child = mHotseat.getChildAt( - mHotseat.getCellXFromOrder(rank), - mHotseat.getCellYFromOrder(rank)); + int startRank = getVisiblePageStartRank(); + for (int localRank = 0; localRank < mHotSeatItemsCount; localRank++) { + int rank = startRank + localRank; + View child = getChildAtRank(rank); if (child != null && !isPredictedIcon(child)) { continue; @@ -222,7 +256,10 @@ public class HotseatPredictionController implements DragController.DragListener, if (mPredictedItems.size() <= predictionIndex) { // Remove predicted apps from the past if (isPredictedIcon(child)) { - mHotseat.removeView(child); + CellLayout page = getPageForRank(rank); + if (page != null) { + page.removeView(child); + } } continue; } @@ -270,10 +307,10 @@ public class HotseatPredictionController implements DragController.DragListener, private void removeOutlineDrawings() { if (mOutlineDrawings.isEmpty()) return; - for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) { - mHotseat.removeDelegatedCellDrawing(outlineDrawing); + for (OutlineOnPage outline : mOutlineDrawings) { + outline.page.removeDelegatedCellDrawing(outline.drawing); + outline.page.invalidate(); } - mHotseat.invalidate(); mOutlineDrawings.clear(); } @@ -300,9 +337,7 @@ public class HotseatPredictionController implements DragController.DragListener, * Pins a predicted app icon into place. */ public void pinPrediction(ItemInfo info) { - PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt( - mHotseat.getCellXFromOrder(info.rank), - mHotseat.getCellYFromOrder(info.rank)); + PredictedAppIcon icon = (PredictedAppIcon) getChildAtRank(info.rank); if (icon == null) { return; } @@ -317,26 +352,28 @@ public class HotseatPredictionController implements DragController.DragListener, .log(LAUNCHER_HOTSEAT_PREDICTION_PINNED); } - private List getPredictedIcons() { - List icons = new ArrayList<>(); - ViewGroup vg = mHotseat.getShortcutsAndWidgets(); - for (int i = 0; i < vg.getChildCount(); i++) { - View child = vg.getChildAt(i); - if (isPredictedIcon(child)) { - icons.add((PredictedAppIcon) child); + private List getPredictedIcons() { + List icons = new ArrayList<>(); + for (CellLayout page : mHotseat.getPageLayouts()) { + ViewGroup vg = page.getShortcutsAndWidgets(); + for (int i = 0; i < vg.getChildCount(); i++) { + View child = vg.getChildAt(i); + if (isPredictedIcon(child)) { + icons.add(new PredictedIconOnPage(page, (PredictedAppIcon) child)); + } } } return icons; } - private void removePredictedApps(List outlines, - DropTarget.DragObject dragObject) { + private void removePredictedApps(DropTarget.DragObject dragObject) { if (mIconRemoveAnimators != null) { mIconRemoveAnimators.end(); } mIconRemoveAnimators = new AnimatorSet(); removeOutlineDrawings(); - for (PredictedAppIcon icon : getPredictedIcons()) { + for (PredictedIconOnPage entry : getPredictedIcons()) { + PredictedAppIcon icon = entry.icon; if (!icon.isEnabled()) { continue; } @@ -345,8 +382,10 @@ public class HotseatPredictionController implements DragController.DragListener, continue; } int rank = ((WorkspaceItemInfo) icon.getTag()).rank; - outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing( - mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon)); + mOutlineDrawings.add(new OutlineOnPage(entry.page, + new PredictedAppIcon.PredictedIconOutlineDrawing( + mHotseat.getCellXFromOrder(rank), + mHotseat.getCellYFromOrder(rank), icon))); icon.setEnabled(false); ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0); animator.addListener(new AnimationSuccessListener() { @@ -369,19 +408,28 @@ public class HotseatPredictionController implements DragController.DragListener, */ private void removeIconWithoutNotify(PredictedAppIcon icon) { mPauseFlags |= FLAG_REMOVING_PREDICTED_ICON; - mHotseat.removeView(icon); + ViewGroup parent = (ViewGroup) icon.getParent(); + if (parent != null) { + // Parent is ShortcutAndWidgetContainer; remove via CellLayout. + ViewGroup cellLayout = (ViewGroup) parent.getParent(); + if (cellLayout instanceof CellLayout) { + cellLayout.removeView(icon); + } else { + parent.removeView(icon); + } + } mPauseFlags &= ~FLAG_REMOVING_PREDICTED_ICON; } @Override public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { - removePredictedApps(mOutlineDrawings, dragObject); + removePredictedApps(dragObject); if (mOutlineDrawings.isEmpty()) return; - for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) { - mHotseat.addDelegatedCellDrawing(outlineDrawing); + for (OutlineOnPage outline : mOutlineDrawings) { + outline.page.addDelegatedCellDrawing(outline.drawing); + outline.page.invalidate(); } mPauseFlags |= FLAG_DRAG_IN_PROGRESS; - mHotseat.invalidate(); } @Override @@ -419,6 +467,7 @@ public class HotseatPredictionController implements DragController.DragListener, @Override public void onDeviceProfileChanged(DeviceProfile profile) { this.mHotSeatItemsCount = profile.numShownHotseatIcons * profile.numHotseatRows; + attachPageListeners(); } @Override @@ -447,8 +496,8 @@ public class HotseatPredictionController implements DragController.DragListener, } int cardinality = 0; - for (PredictedAppIcon icon : getPredictedIcons()) { - ItemInfo info = (ItemInfo) icon.getTag(); + for (PredictedIconOnPage entry : getPredictedIcons()) { + ItemInfo info = (ItemInfo) entry.icon.getTag(); cardinality |= 1 << info.screenId; } @@ -482,7 +531,11 @@ public class HotseatPredictionController implements DragController.DragListener, * Called when user completes adding item requiring a config activity to the hotseat */ public void onDeferredDrop(int cellX, int cellY) { - View child = mHotseat.getChildAt(cellX, cellY); + CellLayout page = mHotseat.getCurrentPageLayout(); + if (page == null) { + return; + } + View child = page.getChildAt(cellX, cellY); if (child instanceof PredictedAppIcon) { removeIconWithoutNotify((PredictedAppIcon) child); } @@ -534,4 +587,24 @@ public class HotseatPredictionController implements DragController.DragListener, writer.println(prefix + "\t\t" + info); } } + + private static final class PredictedIconOnPage { + final CellLayout page; + final PredictedAppIcon icon; + + PredictedIconOnPage(CellLayout page, PredictedAppIcon icon) { + this.page = page; + this.icon = icon; + } + } + + private static final class OutlineOnPage { + final CellLayout page; + final PredictedAppIcon.PredictedIconOutlineDrawing drawing; + + OutlineOnPage(CellLayout page, PredictedAppIcon.PredictedIconOutlineDrawing drawing) { + this.page = page; + this.drawing = drawing; + } + } } diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt index 2f90ee760e..7e79806c05 100644 --- a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt +++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt @@ -160,7 +160,11 @@ object UnfoldAnimationBuilder { LINEAR ) } - registerViews(hotseat) + setClipChildren(hotseat, false, restoreList) + setClipToPadding(hotseat, false, restoreList) + for (page in hotseat.pageLayouts) { + registerViews(page) + } } anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } } } diff --git a/res/layout/hotseat.xml b/res/layout/hotseat.xml index d14dcd54af..461b4a910b 100644 --- a/res/layout/hotseat.xml +++ b/res/layout/hotseat.xml @@ -12,10 +12,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + Modifications copyright 2026 Lawnchair --> \ No newline at end of file + android:clipChildren="false" /> diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 1b4c319353..eef2c43797 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -191,6 +191,8 @@ public class CellLayout extends ViewGroup { public static final int FOLDER = 2; @ContainerType private final int mContainerType; + /** Page index within a multi-page hotseat; unused for non-hotseat layouts. */ + private int mHotseatPageIndex = 0; public static final float DEFAULT_SCALE = 1f; @@ -336,6 +338,20 @@ public class CellLayout extends ViewGroup { mCellLayoutContainer = cellLayoutContainer; } + /** Sets which dock page this layout represents when used as a hotseat page. */ + public void setHotseatPageIndex(int pageIndex) { + mHotseatPageIndex = pageIndex; + } + + /** Returns the dock page index for multi-page hotseat layouts. */ + public int getHotseatPageIndex() { + return mHotseatPageIndex; + } + + public boolean isHotseat() { + return mContainerType == HOTSEAT; + } + /** * Sets or clears a delegate used for accessible drag and drop */ @@ -1473,7 +1489,7 @@ public class CellLayout extends ViewGroup { int container = Favorites.CONTAINER_DESKTOP; if (mContainerType == HOTSEAT) { - screenId = -1; + screenId = mHotseatPageIndex; container = Favorites.CONTAINER_HOTSEAT; } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 8459796da3..87ae24be8a 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -221,6 +221,8 @@ public class DeviceProfile { private final HotseatProfile hotseatProfile; public int numShownHotseatIcons; public int numHotseatRows; + /** Number of horizontally scrollable dock pages (1 = classic single dock). */ + public int numHotseatPages; public int hotseatCellHeightPx; private int mHotseatColumnSpan; private int mHotseatWidthPx; // not used in vertical bar layout @@ -331,6 +333,7 @@ public class DeviceProfile { hotseatQsbWidth = 0; hotseatBorderSpace = 0; numHotseatRows = 1; + numHotseatPages = 1; mBubbleBarSpaceThresholdPx = 0; numShownAllAppsColumns = 0; mViewScaleProvider = null; @@ -517,6 +520,7 @@ public class DeviceProfile { numShownHotseatIcons = displayOptionSpec.numShownHotseatIcons; numHotseatRows = Math.max(1, Math.min(2, PreferenceManager.getInstance(context).getHotseatRows().get())); + numHotseatPages = Math.max(1, Math.min(5, PreferenceManager.getInstance(context).getDockPages().get())); mHotseatColumnSpan = inv.numColumns; numShownAllAppsColumns = displayOptionSpec.numAllAppsColumns; diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index 5c490ff3e3..a1e45f9866 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -27,11 +27,8 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.InsetDrawable; import android.util.AttributeSet; import android.view.Gravity; @@ -45,35 +42,34 @@ import android.widget.FrameLayout; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.android.launcher3.ShortcutAndWidgetContainer.TranslationProvider; +import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; import com.android.launcher3.celllayout.CellLayoutLayoutParams; -import com.android.launcher3.taskbar.BlurredBitmapDrawable; - +import com.android.launcher3.pageindicators.PageIndicatorDots; import com.android.launcher3.util.HorizontalInsettableView; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.views.ActivityContext; -import android.graphics.drawable.Drawable; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import com.hoko.blur.HokoBlur; -import app.lawnchair.preferences2.PreferenceCacheExtensionsKt; import app.lawnchair.hotseat.DisabledHotseat; import app.lawnchair.hotseat.HotseatMode; +import app.lawnchair.hotseat.HotseatPagedView; import app.lawnchair.hotseat.LawnchairHotseat; import app.lawnchair.preferences.PreferenceManager; +import app.lawnchair.preferences2.PreferenceCacheExtensionsKt; import app.lawnchair.preferences2.PreferenceManager2; import app.lawnchair.theme.drawable.DrawableTokens; /** - * View class that represents the bottom row of the home screen. + * View class that represents the bottom dock of the home screen. + * Hosts a {@link app.lawnchair.hotseat.HotseatPagedView} of icon grids plus an optional QSB. */ -public class Hotseat extends CellLayout implements Insettable { +public class Hotseat extends FrameLayout implements Insettable { public static final int ALPHA_CHANNEL_TASKBAR_ALIGNMENT = 0; public static final int ALPHA_CHANNEL_PREVIEW_RENDERER = 1; @@ -98,11 +94,13 @@ public class Hotseat extends CellLayout implements Insettable { // Ratio of empty space, qsb should take up to appear visually centered. public static final float QSB_CENTER_FACTOR = .325f; private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250; + private static final int DOCK_PAGE_INDICATOR_HEIGHT_DP = 8; @ViewDebug.ExportedProperty(category = "launcher") private boolean mHasVerticalHotseat; private Workspace mWorkspace; private boolean mSendTouchToWorkspace; + private boolean mSendTouchToPager; private final MultiValueAlpha mIconsAlphaChannels; private final MultiValueAlpha mQsbAlphaChannels; @@ -111,6 +109,12 @@ public class Hotseat extends CellLayout implements Insettable { private final MultiPropertyFactory mIconsTranslationXFactory; private final View mQsb; + private final FrameLayout mIconsContainer; + private final HotseatPagedView mPagedView; + private final PageIndicatorDots mPageIndicator; + private final int mPageIndicatorHeight; + + private final ActivityContext mActivity; PreferenceManager2 preferenceManager2; PreferenceManager preferenceManager; @@ -125,6 +129,7 @@ public class Hotseat extends CellLayout implements Insettable { public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mActivity = ActivityContext.lookupContext(context); preferenceManager2 = PreferenceManager2.getInstance(context); preferenceManager = PreferenceManager.getInstance(context); @@ -143,6 +148,29 @@ public class Hotseat extends CellLayout implements Insettable { } int layoutId = hotseatMode.getLayoutResourceId(); + mIconsContainer = new FrameLayout(context); + mIconsContainer.setLayoutParams(new LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mIconsContainer.setClipChildren(false); + mIconsContainer.setClipToPadding(false); + addView(mIconsContainer); + + mPagedView = new HotseatPagedView(context); + mPagedView.setLayoutParams(new LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mIconsContainer.addView(mPagedView); + + mPageIndicatorHeight = Math.round( + DOCK_PAGE_INDICATOR_HEIGHT_DP * getResources().getDisplayMetrics().density); + mPageIndicator = new PageIndicatorDots(context); + LayoutParams indicatorLp = new LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, mPageIndicatorHeight); + indicatorLp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + mPageIndicator.setLayoutParams(indicatorLp); + mPageIndicator.setVisibility(GONE); + mIconsContainer.addView(mPageIndicator); + mPagedView.setPageIndicator(mPageIndicator); + if (Flags.enableQsbOnHotseat()) { mQsb = LayoutInflater.from(context).inflate(R.layout.qsb_container_hotseat, this, false); @@ -152,19 +180,24 @@ public class Hotseat extends CellLayout implements Insettable { } addView(mQsb); - mIconsAlphaChannels = new MultiValueAlpha(getShortcutsAndWidgets(), - ALPHA_CHANNEL_CHANNELS_COUNT); + + // Create an initial page so alpha/translation channels have a target during construction. + mPagedView.resetPages(false, null, mActivity.getDeviceProfile()); + + mIconsAlphaChannels = new MultiValueAlpha(mIconsContainer, ALPHA_CHANNEL_CHANNELS_COUNT); mIconsAlphaChannels.setUpdateVisibility(true); if (mQsb instanceof Reorderable qsbReorderable) { mQsbTranslationX = qsbReorderable.getTranslateDelegate() .getTranslationX(MultiTranslateDelegate.INDEX_NAV_BAR_ANIM); } - mIconsTranslationXFactory = new MultiPropertyFactory<>(getShortcutsAndWidgets(), + mIconsTranslationXFactory = new MultiPropertyFactory<>(mIconsContainer, VIEW_TRANSLATE_X, ICONS_TRANSLATION_X_CHANNELS_COUNT, Float::sum); mQsbAlphaChannels = new MultiValueAlpha(mQsb, ALPHA_CHANNEL_CHANNELS_COUNT); mQsbAlphaChannels.setUpdateVisibility(true); setUpBackground(); + setClipChildren(false); + setClipToPadding(false); } private void setUpBackground() { @@ -196,6 +229,40 @@ public class Hotseat extends CellLayout implements Insettable { return mQsbTranslationX; } + public HotseatPagedView getPagedView() { + return mPagedView; + } + + /** Returns the CellLayout for the given dock page. */ + @Nullable + public CellLayout getPageAt(int page) { + return mPagedView.getPageAt(page); + } + + /** Returns the currently visible dock page layout. */ + @Nullable + public CellLayout getCurrentPageLayout() { + return mPagedView.getCurrentCellLayout(); + } + + /** Returns all dock page layouts. */ + public CellLayout[] getPageLayouts() { + int count = mPagedView.getPageCount(); + CellLayout[] pages = new CellLayout[count]; + for (int i = 0; i < count; i++) { + pages[i] = mPagedView.getPageAt(i); + } + return pages; + } + + /** Whether {@code layout} is one of this hotseat's page CellLayouts. */ + public boolean isHotseatPage(View layout) { + if (!(layout instanceof CellLayout)) { + return false; + } + return layout.getParent() == mPagedView; + } + /** * Returns orientation specific cell X given invariant order in the hotseat */ @@ -205,7 +272,8 @@ public class Hotseat extends CellLayout implements Insettable { } DeviceProfile dp = mActivity.getDeviceProfile(); int numColumns = dp.numShownHotseatIcons; - return rank % numColumns; + int localRank = getLocalRank(rank, dp); + return localRank % numColumns; } /** @@ -213,11 +281,29 @@ public class Hotseat extends CellLayout implements Insettable { */ public int getCellYFromOrder(int rank) { if (mHasVerticalHotseat) { - return getCountY() - (rank + 1); + CellLayout page = getCurrentPageLayout(); + int countY = page != null ? page.getCountY() : mActivity.getDeviceProfile().numShownHotseatIcons; + return countY - (rank + 1); } DeviceProfile dp = mActivity.getDeviceProfile(); int numColumns = dp.numShownHotseatIcons; - return rank / numColumns; + int localRank = getLocalRank(rank, dp); + return localRank / numColumns; + } + + /** Returns the dock page index for a global hotseat rank. */ + public int getPageFromOrder(int rank) { + if (mHasVerticalHotseat) { + return 0; + } + DeviceProfile dp = mActivity.getDeviceProfile(); + int slotsPerPage = Math.max(1, dp.numShownHotseatIcons * dp.numHotseatRows); + return rank / slotsPerPage; + } + + private static int getLocalRank(int rank, DeviceProfile dp) { + int slotsPerPage = Math.max(1, dp.numShownHotseatIcons * dp.numHotseatRows); + return rank % slotsPerPage; } boolean isHasVerticalHotseat() { @@ -228,45 +314,34 @@ public class Hotseat extends CellLayout implements Insettable { ActivityContext activityContext = ActivityContext.lookupContext(getContext()); boolean bubbleBarEnabled = activityContext.isBubbleBarEnabled(); boolean hasBubbles = activityContext.hasBubbles(); - removeAllViewsInLayout(); mHasVerticalHotseat = hasVerticalHotseat; DeviceProfile dp = mActivity.getDeviceProfile(); + mPagedView.resetPages(hasVerticalHotseat, mWorkspace, dp); + if (bubbleBarEnabled) { - if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) { - getShortcutsAndWidgets().setTranslationProvider( - cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); - if (mQsb instanceof HorizontalInsettableView) { - HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; + for (CellLayout page : getPageLayouts()) { + if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) { + page.getShortcutsAndWidgets().setTranslationProvider( + cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); + } else { + page.getShortcutsAndWidgets().setTranslationProvider(null); + } + } + if (mQsb instanceof HorizontalInsettableView) { + HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb; + if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) { final float insetFraction = (float) dp.iconSizePx / dp.hotseatQsbWidth; - // post this to the looper so that QSB has a chance to redraw itself, e.g. - // after device rotation mQsb.post(() -> insettableQsb.setHorizontalInsets(insetFraction)); - } - } else { - getShortcutsAndWidgets().setTranslationProvider(null); - if (mQsb instanceof HorizontalInsettableView) { - ((HorizontalInsettableView) mQsb).setHorizontalInsets(0); + } else { + insettableQsb.setHorizontalInsets(0); } } - } - - resetCellSize(dp); - if (hasVerticalHotseat) { - setGridSize(1, dp.numShownHotseatIcons); - } else { - setGridSize(dp.numShownHotseatIcons, dp.numHotseatRows); } } /** * Adjust the hotseat icons for the bubble bar. - * - *

When the bubble bar becomes visible, if needed, this method animates the hotseat icons - * to reduce the spacing between them and make room for the bubble bar. The QSB width is - * animated as well to align with the hotseat icons. - * - *

When the bubble bar goes away, any adjustments that were previously made are reversed. */ public void adjustForBubbleBar(boolean isBubbleBarVisible) { DeviceProfile dp = mActivity.getDeviceProfile(); @@ -274,30 +349,30 @@ public class Hotseat extends CellLayout implements Insettable { && dp.shouldAdjustHotseatOrQsbForBubbleBar(getContext()); boolean shouldAdjustHotseat = shouldAdjust && dp.shouldAlignBubbleBarWithHotseat(); - ShortcutAndWidgetContainer icons = getShortcutsAndWidgets(); - // update the translation provider for future layout passes of hotseat icons. - if (shouldAdjustHotseat) { - icons.setTranslationProvider( - cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); - } else { - icons.setTranslationProvider(null); - } AnimatorSet animatorSet = new AnimatorSet(); - for (int i = 0; i < icons.getChildCount(); i++) { - View child = icons.getChildAt(i); - if (child.getLayoutParams() instanceof CellLayoutLayoutParams lp) { - float tx = shouldAdjustHotseat - ? dp.getHotseatAdjustedTranslation(getContext(), lp.getCellX()) : 0; - if (child instanceof Reorderable) { - MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); - animatorSet.play( - mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx)); - } else { - animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); + for (CellLayout page : getPageLayouts()) { + ShortcutAndWidgetContainer icons = page.getShortcutsAndWidgets(); + if (shouldAdjustHotseat) { + icons.setTranslationProvider( + cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX)); + } else { + icons.setTranslationProvider(null); + } + for (int i = 0; i < icons.getChildCount(); i++) { + View child = icons.getChildAt(i); + if (child.getLayoutParams() instanceof CellLayoutLayoutParams lp) { + float tx = shouldAdjustHotseat + ? dp.getHotseatAdjustedTranslation(getContext(), lp.getCellX()) : 0; + if (child instanceof Reorderable) { + MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); + animatorSet.play( + mtd.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM).animateToValue(tx)); + } else { + animatorSet.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, tx)); + } } } } - //TODO(b/381109832) refactor & simplify adjustment logic boolean shouldAdjustQsb = shouldAdjustHotseat || (shouldAdjust && dp.shouldAlignBubbleBarWithQSB()); if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) { @@ -315,13 +390,6 @@ public class Hotseat extends CellLayout implements Insettable { animatorSet.setDuration(BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS).start(); } - @Override - protected int getTranslationXForCell(int cellX, int cellY) { - TranslationProvider translationProvider = getShortcutsAndWidgets().getTranslationProvider(); - if (translationProvider == null) return 0; - return (int) translationProvider.getTranslationX(cellX); - } - @Override public void setInsets(Rect insets) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); @@ -329,6 +397,7 @@ public class Hotseat extends CellLayout implements Insettable { if (grid.isVerticalBarLayout()) { mQsb.setVisibility(View.GONE); + mPageIndicator.setVisibility(GONE); lp.height = ViewGroup.LayoutParams.MATCH_PARENT; if (grid.isSeascape()) { lp.gravity = Gravity.LEFT; @@ -352,16 +421,48 @@ public class Hotseat extends CellLayout implements Insettable { public void setWorkspace(Workspace w) { mWorkspace = w; - setCellLayoutContainer(w); + for (CellLayout page : getPageLayouts()) { + page.setCellLayoutContainer(w); + } + } + + private boolean isTouchOnQsb(MotionEvent ev) { + if (mQsb == null || mQsb.getVisibility() != VISIBLE) { + return false; + } + float x = ev.getX(); + float y = ev.getY(); + return x >= mQsb.getLeft() && x < mQsb.getRight() + && y >= mQsb.getTop() && y < mQsb.getBottom(); + } + + private MotionEvent obtainPagerEvent(MotionEvent ev) { + MotionEvent pagerEv = MotionEvent.obtain(ev); + pagerEv.offsetLocation(-mIconsContainer.getLeft() - mPagedView.getLeft(), + -mIconsContainer.getTop() - mPagedView.getTop()); + return pagerEv; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating - // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace - // for the remainder of the this input stream. int yThreshold = getMeasuredHeight() - getPaddingBottom(); - if (mWorkspace != null && ev.getY() <= yThreshold) { + if (ev.getY() > yThreshold || isTouchOnQsb(ev)) { + return false; + } + + // Multi-page dock: own the icon-band stream and dispatch it to the pager so empty + // pages can still scroll (same pattern as single-page workspace forwarding). + if (mPagedView.isPagingEnabled()) { + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action == MotionEvent.ACTION_DOWN) { + mSendTouchToPager = true; + mSendTouchToWorkspace = false; + } + return mSendTouchToPager; + } + + // Single-page dock: forward horizontal swipes to workspace. + if (mWorkspace != null) { mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev); return mSendTouchToWorkspace; } @@ -370,7 +471,16 @@ public class Hotseat extends CellLayout implements Insettable { @Override public boolean onTouchEvent(MotionEvent event) { - // See comment in #onInterceptTouchEvent + if (mSendTouchToPager) { + MotionEvent pagerEv = obtainPagerEvent(event); + boolean handled = mPagedView.dispatchTouchEvent(pagerEv); + pagerEv.recycle(); + final int action = event.getAction() & MotionEvent.ACTION_MASK; + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mSendTouchToPager = false; + } + return handled; + } if (mSendTouchToWorkspace) { final int action = event.getAction(); switch (action & MotionEvent.ACTION_MASK) { @@ -380,45 +490,93 @@ public class Hotseat extends CellLayout implements Insettable { } return mWorkspace.onTouchEvent(event); } - // Always let touch follow through to Workspace. return false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(widthSize, heightSize); + + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); DeviceProfile dp = mActivity.getDeviceProfile(); + boolean showIndicator = mPagedView.isPagingEnabled() && !dp.isVerticalBarLayout(); + int indicatorSpace = showIndicator ? mPageIndicatorHeight : 0; + + int pagerWidth = widthSize - paddingLeft - paddingRight; + int pagerHeight = heightSize - paddingTop - paddingBottom; + mIconsContainer.measure( + makeMeasureSpec(Math.max(0, pagerWidth), MeasureSpec.EXACTLY), + makeMeasureSpec(Math.max(0, pagerHeight), MeasureSpec.EXACTLY)); + + int pageHeight = Math.max(0, pagerHeight - indicatorSpace); + mPagedView.measure( + makeMeasureSpec(Math.max(0, pagerWidth), MeasureSpec.EXACTLY), + makeMeasureSpec(pageHeight, MeasureSpec.EXACTLY)); + + if (showIndicator) { + mPageIndicator.measure( + makeMeasureSpec(Math.max(0, pagerWidth), MeasureSpec.EXACTLY), + makeMeasureSpec(mPageIndicatorHeight, MeasureSpec.EXACTLY)); + } - // LC: Fix weird sizing with hotseatQsbWidth being 0 on phone int width; if (dp.isQsbInline) { width = dp.hotseatQsbWidth; } else { - width = getShortcutsAndWidgets().getMeasuredWidth(); + width = mPagedView.getMeasuredWidth(); } - - mQsb.measure(makeMeasureSpec(width, MeasureSpec.EXACTLY), + + mQsb.measure(makeMeasureSpec(Math.max(0, width), MeasureSpec.EXACTLY), makeMeasureSpec(dp.getHotseatProfile().getQsbHeight(), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + + DeviceProfile dp = mActivity.getDeviceProfile(); + mIconsContainer.layout(paddingLeft, paddingTop, width - paddingRight, + height - paddingBottom); + + int containerWidth = mIconsContainer.getWidth(); + int containerHeight = mIconsContainer.getHeight(); + boolean showIndicator = mPagedView.isPagingEnabled() && !dp.isVerticalBarLayout(); + int indicatorSpace = showIndicator ? mPageIndicatorHeight : 0; + + mPagedView.layout(0, 0, containerWidth, Math.max(0, containerHeight - indicatorSpace)); + + if (showIndicator) { + int indicatorTop = containerHeight - mPageIndicatorHeight; + mPageIndicator.layout(0, indicatorTop, containerWidth, + indicatorTop + mPageIndicatorHeight); + mPageIndicator.setVisibility(VISIBLE); + } else { + mPageIndicator.setVisibility(GONE); + } int qsbMeasuredWidth = mQsb.getMeasuredWidth(); int left; - DeviceProfile dp = mActivity.getDeviceProfile(); if (dp.isQsbInline) { int qsbSpace = dp.hotseatBorderSpace; left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace; } else { - left = (r - l - qsbMeasuredWidth) / 2; + left = (width - qsbMeasuredWidth) / 2; } int right = left + qsbMeasuredWidth; - int bottom = b - t - dp.getQsbOffsetY(); + int bottom = height - dp.getQsbOffsetY(); int top = bottom - dp.getHotseatProfile().getQsbHeight(); mQsb.layout(left, top, right, bottom); } @@ -454,9 +612,25 @@ public class Hotseat extends CellLayout implements Insettable { return mQsb; } + /** Delegates to the current page's shortcuts container. */ + @Nullable + public ShortcutAndWidgetContainer getShortcutsAndWidgets() { + CellLayout page = getCurrentPageLayout(); + return page != null ? page.getShortcutsAndWidgets() : null; + } + + /** Delegates accessibility drag helper to the current page. */ + @Nullable + public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() { + CellLayout page = getCurrentPageLayout(); + return page != null ? page.getDragAndDropAccessibilityDelegate() : null; + } + /** Dumps the Hotseat internal state */ public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "Hotseat:"); + writer.println(prefix + "\tpages: " + mPagedView.getPageCount() + + " pagingEnabled=" + mPagedView.isPagingEnabled()); mIconsAlphaChannels.dump( prefix + "\t", writer, diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 02a6572575..e7340488eb 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -748,7 +748,8 @@ public class Launcher extends StatefulActivity mCellPosMapper = new TwoPanelCellPosMapper(mDeviceProfile.inv.numColumns); } else { mCellPosMapper = new CellPosMapper(mDeviceProfile.isVerticalBarLayout(), - mDeviceProfile.numShownHotseatIcons); + mDeviceProfile.numShownHotseatIcons, mDeviceProfile.numHotseatRows, + mDeviceProfile.numHotseatPages); } mModelWriter = mModel.getWriter(true, mCellPosMapper, this); updateFixedLandscape(); @@ -2151,7 +2152,7 @@ public class Launcher extends StatefulActivity boolean isHotseatLayout(View layout) { // TODO: Remove this method - return mHotseat != null && (layout == mHotseat); + return mHotseat != null && (layout == mHotseat || mHotseat.isHotseatPage(layout)); } @Override @@ -2371,7 +2372,13 @@ public class Launcher extends StatefulActivity } List containers = new ArrayList<>(mWorkspace.getPanelCount() + 1); - containers.add(mWorkspace.getHotseat()); + Hotseat hotseat = mWorkspace.getHotseat(); + if (hotseat != null) { + CellLayout currentPage = hotseat.getCurrentPageLayout(); + if (currentPage != null) { + containers.add(currentPage); + } + } mWorkspace.forEachVisiblePage(page -> containers.add((CellLayout) page)); CellLayout[] containerArray = containers.toArray(new CellLayout[0]); LauncherBindableItemsContainer visibleContainer = @@ -2501,11 +2508,18 @@ public class Launcher extends StatefulActivity writer.println(prefix + " Hotseat"); mHotseat.dump(prefix, writer); - ViewGroup layout = mHotseat.getShortcutsAndWidgets(); - for (int j = 0; j < layout.getChildCount(); j++) { - Object tag = layout.getChildAt(j).getTag(); - if (tag != null) { - writer.println(prefix + " " + tag); + CellLayout[] hotseatPages = mHotseat.getPageLayouts(); + for (int p = 0; p < hotseatPages.length; p++) { + ViewGroup layout = hotseatPages[p].getShortcutsAndWidgets(); + if (layout == null) { + continue; + } + writer.println(prefix + " Dock page " + p); + for (int j = 0; j < layout.getChildCount(); j++) { + Object tag = layout.getChildAt(j).getTag(); + if (tag != null) { + writer.println(prefix + " " + tag); + } } } } @@ -2928,8 +2942,15 @@ public class Launcher extends StatefulActivity * @param screenId must be presenterPos and not modelPos. */ public CellLayout getCellLayout(int container, int screenId) { - return (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) - ? mHotseat : mWorkspace.getScreenWithId(screenId); + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT + || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { + if (mHotseat == null) { + return null; + } + CellLayout page = mHotseat.getPageAt(screenId); + return page != null ? page : mHotseat.getCurrentPageLayout(); + } + return mWorkspace.getScreenWithId(screenId); } @Override diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index b587d4d57e..6620f052eb 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -145,6 +145,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; +import app.lawnchair.hotseat.HotseatPagedView; import app.lawnchair.preferences2.PreferenceCacheExtensionsKt; import static app.lawnchair.util.LawnchairUtilsKt.toBitmap; import app.lawnchair.LawnchairApp; @@ -765,7 +766,8 @@ public class Workspace extends PagedView int dragSourceChildCount = mDragSourceInternal.getChildCount(); // If the icon was dragged from Hotseat, there is no page pair - if (isTwoPanelEnabled() && !(mDragSourceInternal.getParent() instanceof Hotseat)) { + if (isTwoPanelEnabled() && !mLauncher.isHotseatLayout( + (View) mDragSourceInternal.getParent())) { int pagePairScreenId = getScreenPair(getCellPosMapper().mapModelToPresenter( dragObject.dragInfo).screenId); CellLayout pagePair = mWorkspaceScreens.get(pagePairScreenId); @@ -1017,6 +1019,10 @@ public class Workspace extends PagedView if (index != -1) { return mWorkspaceScreens.keyAt(index); } + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat != null && hotseat.isHotseatPage(layout)) { + return layout.getHotseatPageIndex(); + } return -1; } @@ -1859,7 +1865,12 @@ public class Workspace extends PagedView protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) { super.enableAccessibleDrag(enable, dragObject); - setEnableForLayout(mLauncher.getHotseat(), enable); + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat != null) { + for (CellLayout page : hotseat.getPageLayouts()) { + setEnableForLayout(page, enable); + } + } if (enable && dragObject != null && dragObject.dragInfo instanceof LauncherAppWidgetInfo) { mLauncher.getHotseat().setImportantForAccessibility( @@ -2787,7 +2798,7 @@ public class Workspace extends PagedView private boolean setDropLayoutForDragObject(DragObject d, float centerX, float centerY) { CellLayout layout = null; if (shouldUseHotseatAsDropLayout(d)) { - layout = mLauncher.getHotseat(); + layout = resolveHotseatDropLayout(d); } else if (!isDragObjectOverSmartSpace(d)) { // If the object is over qsb/smartspace, we don't want to highlight anything. @@ -2813,14 +2824,35 @@ public class Workspace extends PagedView return false; } + @Nullable + private CellLayout resolveHotseatDropLayout(DragObject dragObject) { + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat == null) { + return null; + } + // Prefer the visible dock page so drops land where the user is looking. + CellLayout current = hotseat.getCurrentPageLayout(); + HotseatPagedView pager = hotseat.getPagedView(); + if (pager != null && pager.isPageInTransition()) { + float[] xy = new float[] { dragObject.x, dragObject.y }; + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true); + mLauncher.getDragLayer().mapCoordInSelfToDescendant(pager, xy); + CellLayout underFinger = pager.findPageAtLocalX(xy[0]); + if (underFinger != null) { + return underFinger; + } + } + return current; + } + private boolean shouldUseHotseatAsDropLayout(DragObject dragObject) { if (mLauncher.getHotseat() == null || mLauncher.getHotseat().getShortcutsAndWidgets() == null || isDragWidget(dragObject)) { return false; } - View hotseatShortcuts = mLauncher.getHotseat().getShortcutsAndWidgets(); - getViewBoundsRelativeToWorkspace(hotseatShortcuts, mTempRect); + View hotseatIcons = mLauncher.getHotseat().getPagedView(); + getViewBoundsRelativeToWorkspace(hotseatIcons, mTempRect); return mTempRect.contains(dragObject.x, dragObject.y); } @@ -3710,16 +3742,14 @@ public class Workspace extends PagedView */ private CellLayout[] getWorkspaceAndHotseatCellLayouts() { int screenCount = getChildCount(); - final CellLayout[] layouts; - if (mLauncher.getHotseat() != null) { - layouts = new CellLayout[screenCount + 1]; - layouts[screenCount] = mLauncher.getHotseat(); - } else { - layouts = new CellLayout[screenCount]; - } + CellLayout[] hotseatPages = mLauncher.getHotseat() != null + ? mLauncher.getHotseat().getPageLayouts() + : new CellLayout[0]; + final CellLayout[] layouts = new CellLayout[screenCount + hotseatPages.length]; for (int screen = 0; screen < screenCount; screen++) { layouts[screen] = (CellLayout) getChildAt(screen); } + System.arraycopy(hotseatPages, 0, layouts, screenCount, hotseatPages.length); return layouts; } diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java index 226aeceddf..7a40eee50f 100644 --- a/src/com/android/launcher3/WorkspaceLayoutManager.java +++ b/src/com/android/launcher3/WorkspaceLayoutManager.java @@ -54,16 +54,18 @@ public interface WorkspaceLayoutManager { CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info); int x = presenterPos.cellX; int y = presenterPos.cellY; + int screenId = presenterPos.screenId; if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { - int screenId = presenterPos.screenId; - x = getHotseat().getCellXFromOrder(screenId); - y = getHotseat().getCellYFromOrder(screenId); - // TODO(b/335141365): Remove this log after the bug is fixed. + // Model screenId is a flat rank; presenter screenId is the dock page. + int rank = info.screenId; + x = getHotseat().getCellXFromOrder(rank); + y = getHotseat().getCellYFromOrder(rank); + screenId = getHotseat().getPageFromOrder(rank); Log.d(TAG, "addInScreenFromBind: hotseat inflation with x = " + x - + " and y = " + y); + + " and y = " + y + " page = " + screenId); } - addInScreen(child, info.container, presenterPos.screenId, x, y, info.spanX, info.spanY); + addInScreen(child, info.container, screenId, x, y, info.spanX, info.spanY); } /** @@ -103,10 +105,18 @@ public interface WorkspaceLayoutManager { throw new RuntimeException("Screen id should not be extra empty screen: " + screenId); } - final CellLayout layout; + CellLayout layout; if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { - layout = getHotseat(); + Hotseat hotseat = getHotseat(); + layout = hotseat.getPageAt(screenId); + if (layout == null) { + layout = hotseat.getCurrentPageLayout(); + } + if (layout == null) { + Log.e(TAG, "Skipping child, hotseat page " + screenId + " not found"); + return; + } // Hide folder title in the hotseat if (child instanceof FolderIcon) { diff --git a/src/com/android/launcher3/celllayout/CellPosMapper.java b/src/com/android/launcher3/celllayout/CellPosMapper.java index 2d9ac4da04..9441454d1d 100644 --- a/src/com/android/launcher3/celllayout/CellPosMapper.java +++ b/src/com/android/launcher3/celllayout/CellPosMapper.java @@ -12,12 +12,15 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * Modifications copyright 2026 Lawnchair */ package com.android.launcher3.celllayout; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; -import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.model.data.ItemInfo; import java.util.Objects; @@ -27,19 +30,42 @@ import java.util.Objects; */ public class CellPosMapper { - public static final CellPosMapper DEFAULT = new CellPosMapper(false, -1); + public static final CellPosMapper DEFAULT = new CellPosMapper(false, -1, 1, 1); private final boolean mHasVerticalHotseat; private final int mNumOfHotseat; + private final int mNumHotseatRows; public CellPosMapper(boolean hasVerticalHotseat, int numOfHotseat) { + this(hasVerticalHotseat, numOfHotseat, 1, 1); + } + + public CellPosMapper(boolean hasVerticalHotseat, int numOfHotseat, int numHotseatRows, + int numHotseatPages) { mHasVerticalHotseat = hasVerticalHotseat; mNumOfHotseat = numOfHotseat; + mNumHotseatRows = Math.max(1, numHotseatRows); + } + + private static boolean isHotseatContainer(int container) { + return container == CONTAINER_HOTSEAT || container == CONTAINER_HOTSEAT_PREDICTION; } /** - * Maps the position in model to the position in view + * Maps the position in model to the position in view. + * Hotseat model screenId is a flat rank; presenter screenId is the dock page index. */ public CellPos mapModelToPresenter(ItemInfo info) { + if (isHotseatContainer(info.container) && mNumOfHotseat > 0) { + if (mHasVerticalHotseat) { + return new CellPos(0, mNumOfHotseat - info.screenId - 1, 0); + } + int slotsPerPage = Math.max(1, mNumOfHotseat * mNumHotseatRows); + int page = info.screenId / slotsPerPage; + int local = info.screenId % slotsPerPage; + int cellX = local % mNumOfHotseat; + int cellY = local / mNumOfHotseat; + return new CellPos(cellX, cellY, page); + } return new CellPos(info.cellX, info.cellY, info.screenId); } @@ -48,12 +74,17 @@ public class CellPosMapper { */ public CellPos mapPresenterToModel(int presenterX, int presenterY, int presenterScreen, int container) { - if (container == Favorites.CONTAINER_HOTSEAT) { - if (mHasVerticalHotseat) { + if (isHotseatContainer(container)) { + if (mHasVerticalHotseat && mNumOfHotseat > 0) { presenterScreen = mNumOfHotseat - presenterY - 1; - } else { - // For multi-row hotseat, screenId (rank) = row * numColumns + column - presenterScreen = presenterY * mNumOfHotseat + presenterX; + } else if (!mHasVerticalHotseat && mNumOfHotseat > 0) { + // presenterScreen is the dock page index (0-based) + int page = Math.max(0, presenterScreen); + int slotsPerPage = Math.max(1, mNumOfHotseat * mNumHotseatRows); + int localRank = presenterY * mNumOfHotseat + presenterX; + presenterScreen = page * slotsPerPage + localRank; + } else if (!mHasVerticalHotseat) { + presenterScreen = presenterX; } } return new CellPos(presenterX, presenterY, presenterScreen); @@ -67,7 +98,7 @@ public class CellPosMapper { private final int mColumnCount; public TwoPanelCellPosMapper(int columnCount) { - super(false, -1); + super(false, -1, 1, 1); mColumnCount = columnCount; } diff --git a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java index 408a5a04a7..5233dd3df1 100644 --- a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java +++ b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java @@ -183,10 +183,16 @@ public class KeyboardDragAndDropView extends AbstractFloatingView mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate()); } if (openFolder == null) { - mDelegates.add(pv.getNextPage() + 1, - mLauncher.getHotseat().getDragAndDropAccessibilityDelegate()); + DragAndDropAccessibilityDelegate hotseatDelegate = + mLauncher.getHotseat().getDragAndDropAccessibilityDelegate(); + if (hotseatDelegate != null) { + mDelegates.add(pv.getNextPage() + 1, hotseatDelegate); + } } mDelegates.forEach(delegate -> { + if (delegate == null) { + return; + } mIntList.clear(); delegate.getVisibleVirtualViews(mIntList); mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id))); diff --git a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java index 2839147e89..dc6d7f8f7d 100644 --- a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java @@ -81,6 +81,7 @@ import com.android.launcher3.widget.LauncherWidgetHolder; import app.lawnchair.preferences2.PreferenceCacheExtensionsKt; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -307,7 +308,10 @@ public class LauncherPreviewRenderer extends BaseContext private List getAllLayouts() { List screens = new ArrayList<>(mWorkspaceScreens.values()); - screens.add(getHotseat()); + Hotseat hotseat = getHotseat(); + if (hotseat != null) { + Collections.addAll(screens, hotseat.getPageLayouts()); + } return screens; } @@ -379,7 +383,8 @@ public class LauncherPreviewRenderer extends BaseContext int cellX = mHotseat.getCellXFromOrder(rank); int cellY = mHotseat.getCellYFromOrder(rank); - if (mHotseat.isOccupied(cellX, cellY)) continue; + CellLayout page = mHotseat.getPageAt(mHotseat.getPageFromOrder(rank)); + if (page == null || page.isOccupied(cellX, cellY)) continue; WorkspaceItemInfo itemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(predictionIndex)); diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index f6590464bd..494923c68b 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -297,8 +297,12 @@ public class TestInformationHandler implements ResourceBasedOverride { int cellIndex = extra.getInt(TestProtocol.TEST_INFO_PARAM_INDEX); return getLauncherUIProperty(Bundle::putParcelable, launcher -> { final Hotseat hotseat = launcher.getHotseat(); + final CellLayout page = hotseat.getCurrentPageLayout(); + if (page == null) { + return new Point(0, 0); + } final Rect cellRect = getDescendantRectRelativeToDragLayerForCell(launcher, - hotseat, cellIndex, /* cellY= */ 0, + page, cellIndex, /* cellY= */ 0, /* spanX= */ 1, /* spanY= */ 1); // TODO(b/234322284): return the real center point. return new Point(cellRect.left + (cellRect.right - cellRect.left) / 3, @@ -428,6 +432,9 @@ public class TestInformationHandler implements ResourceBasedOverride { ShortcutAndWidgetContainer hotseatIconsContainer = l.getHotseat().getShortcutsAndWidgets(); ArrayList hotseatIconNames = new ArrayList<>(); + if (hotseatIconsContainer == null) { + return hotseatIconNames; + } for (int i = 0; i < hotseatIconsContainer.getChildCount(); i++) { // Use unchecked cast to catch changes in hotseat layout diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index 0c744962f2..26cd7829ef 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -554,7 +554,8 @@ public interface ActivityContext extends SavedStateRegistryOwner { default CellPosMapper getCellPosMapper() { DeviceProfile dp = getDeviceProfile(); - return new CellPosMapper(dp.isVerticalBarLayout(), dp.numShownHotseatIcons); + return new CellPosMapper(dp.isVerticalBarLayout(), dp.numShownHotseatIcons, + dp.numHotseatRows, dp.numHotseatPages); } /** Returns a writer for updating model properties */