Make hotseat fit one line on tablets

When running on tablets in landscape, the QSB should inline with the hotseat icons. This does that and fixes small bugs that didn't account for the new hotseatBorderSpace.

Bug: 210118169
Test: atest Launcher3Tests:DeviceProfileTest
Test: visual, using HSV and Window
Change-Id: I5a89c5449bf59fde736618151eceaacca443ef67
This commit is contained in:
Thales Lima
2022-01-13 17:58:42 +00:00
parent 1b077203d6
commit a1012904bb
6 changed files with 264 additions and 14 deletions

View File

@@ -33,6 +33,7 @@
<dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
<dimen name="dynamic_grid_hotseat_bottom_padding">2dp</dimen>
<dimen name="dynamic_grid_hotseat_bottom_tall_padding">0dp</dimen>
<dimen name="inline_qsb_bottom_margin">0dp</dimen>
<!-- Qsb -->
<!-- Used for adjusting the position of QSB when placed in hotseat. This is a ratio and a higher

View File

@@ -61,6 +61,7 @@ public class DeviceProfile {
public final boolean isPhone;
public final boolean transposeLayoutWithOrientation;
public final boolean isTwoPanels;
public final boolean isQsbInline;
// Device properties in current orientation
public final boolean isLandscape;
@@ -165,6 +166,7 @@ public class DeviceProfile {
public final float qsbBottomMarginOriginalPx;
public int qsbBottomMarginPx;
public int qsbWidth; // only used when isQsbInline
// All apps
public Point allAppsBorderSpacePx;
@@ -246,6 +248,7 @@ public class DeviceProfile {
isPhone = !isTablet;
isTwoPanels = isTablet && useTwoPanels;
aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
mQsbCenterFactor = context.getResources().getFloat(R.dimen.qsb_center_factor);
@@ -270,7 +273,6 @@ public class DeviceProfile {
}
}
hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height);
isTaskbarPresent = isTablet && ApiWrapper.TASKBAR_DRAWN_IN_PROCESS
&& FeatureFlags.ENABLE_TASKBAR.get();
if (isTaskbarPresent) {
@@ -333,6 +335,8 @@ public class DeviceProfile {
workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x);
hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height);
isQsbInline = isTablet && isLandscape && !isTwoPanels && hotseatQsbHeight > 0;
numShownHotseatIcons =
isTwoPanels ? inv.numDatabaseHotseatIcons : inv.numShownHotseatIcons;
numShownAllAppsColumns =
@@ -340,10 +344,17 @@ public class DeviceProfile {
hotseatBarSizeExtraSpacePx = 0;
hotseatBarTopPaddingPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding);
hotseatBarBottomPaddingPx = (isTallDevice ? res.getDimensionPixelSize(
R.dimen.dynamic_grid_hotseat_bottom_tall_padding)
: res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding))
+ res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
if (isQsbInline) {
hotseatBarBottomPaddingPx = res.getDimensionPixelSize(R.dimen.inline_qsb_bottom_margin);
qsbWidth = calculateQsbWidth();
} else {
hotseatBarBottomPaddingPx = (isTallDevice ? res.getDimensionPixelSize(
R.dimen.dynamic_grid_hotseat_bottom_tall_padding)
: res.getDimensionPixelSize(
R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding))
+ res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
qsbWidth = 0;
}
hotseatBarSidePaddingEndPx =
res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
// Add a bit of space between nav bar and hotseat in vertical bar layout.
@@ -472,6 +483,14 @@ public class DeviceProfile {
new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE);
}
private int calculateQsbWidth() {
return cellWidthPx * inv.numColumns
+ cellLayoutBorderSpacePx.x * (inv.numColumns - 1)
- (cellWidthPx - iconSizePx) // left and right cell space
- iconSizePx * numShownHotseatIcons
- hotseatBorderSpace * numShownHotseatIcons;
}
private int getHorizontalMarginPx(InvariantDeviceProfile idp, Resources res) {
if (isVerticalBarLayout()) {
return 0;
@@ -717,6 +736,11 @@ public class DeviceProfile {
// Hotseat
hotseatBorderSpace = pxFromDp(inv.hotseatBorderSpaces[mTypeIndex], mMetrics, scale);
if (isQsbInline) {
qsbWidth = calculateQsbWidth();
} else {
qsbWidth = 0;
}
updateHotseatIconSize(iconSizePx);
if (!isVerticalLayout) {
@@ -895,15 +919,24 @@ public class DeviceProfile {
} else if (isTaskbarPresent) {
int hotseatHeight = workspacePadding.bottom;
int taskbarOffset = getTaskbarOffsetY();
int additionalLeftSpace = 0;
// Center the QSB with hotseat and push icons to the right
if (isQsbInline) {
additionalLeftSpace = qsbWidth + hotseatBorderSpace;
}
int hotseatTopDiff = hotseatHeight - taskbarOffset;
int endOffset = ApiWrapper.getHotseatEndOffset(context);
int requiredWidth = iconSizePx * numShownHotseatIcons
+ hotseatBorderSpace * (numShownHotseatIcons - 1);
+ hotseatBorderSpace * (numShownHotseatIcons - 1)
+ additionalLeftSpace;
int hotseatSize = Math.min(requiredWidth, availableWidthPx - endOffset);
int sideSpacing = (availableWidthPx - hotseatSize) / 2;
mHotseatPadding.set(sideSpacing, hotseatTopDiff, sideSpacing, taskbarOffset);
mHotseatPadding.set(sideSpacing + additionalLeftSpace, hotseatTopDiff, sideSpacing,
taskbarOffset);
if (endOffset > sideSpacing) {
int diff = Utilities.isRtl(context.getResources())
@@ -936,6 +969,10 @@ public class DeviceProfile {
* Returns the number of pixels the QSB is translated from the bottom of the screen.
*/
public int getQsbOffsetY() {
if (isQsbInline) {
return hotseatBarBottomPaddingPx;
}
int freeSpace = isTaskbarPresent
? workspacePadding.bottom
: hotseatBarSizePx - hotseatCellHeightPx - hotseatQsbHeight;
@@ -953,7 +990,11 @@ public class DeviceProfile {
* Returns the number of pixels the taskbar is translated from the bottom of the screen.
*/
public int getTaskbarOffsetY() {
return (getQsbOffsetY() - taskbarSize) / 2;
if (isQsbInline) {
return getQsbOffsetY() + (Math.abs(hotseatQsbHeight - iconSizePx) / 2);
} else {
return (getQsbOffsetY() - taskbarSize) / 2;
}
}
/**
@@ -1113,6 +1154,8 @@ public class DeviceProfile {
writer.println(prefix + pxToDpStr("hotseatBarSidePaddingEndPx",
hotseatBarSidePaddingEndPx));
writer.println(prefix + "\tnumShownHotseatIcons: " + numShownHotseatIcons);
writer.println(prefix + "\tisQsbInline: " + isQsbInline);
writer.println(prefix + pxToDpStr("qsbWidth", qsbWidth));
writer.println(prefix + "\tisTaskbarPresent:" + isTaskbarPresent);
writer.println(prefix + "\tisTaskbarPresentInApps:" + isTaskbarPresentInApps);

View File

@@ -173,7 +173,10 @@ public class Hotseat extends CellLayout implements Insettable {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getShortcutsAndWidgets().getMeasuredWidth();
int width = mActivity.getDeviceProfile().isQsbInline
? mActivity.getDeviceProfile().qsbWidth
: getShortcutsAndWidgets().getMeasuredWidth();
mQsb.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mQsbHeight, MeasureSpec.EXACTLY));
}
@@ -183,7 +186,13 @@ public class Hotseat extends CellLayout implements Insettable {
super.onLayout(changed, l, t, r, b);
int qsbWidth = mQsb.getMeasuredWidth();
int left = (r - l - qsbWidth) / 2;
int left;
if (mActivity.getDeviceProfile().isQsbInline) {
int qsbSpace = mActivity.getDeviceProfile().hotseatBorderSpace;
left = l + getPaddingLeft() - qsbWidth - qsbSpace;
} else {
left = (r - l - qsbWidth) / 2;
}
int right = left + qsbWidth;
int bottom = b - t - mActivity.getDeviceProfile().getQsbOffsetY();

View File

@@ -19,6 +19,7 @@ package com.android.launcher3;
import static android.view.MotionEvent.ACTION_DOWN;
import static com.android.launcher3.CellLayout.FOLDER;
import static com.android.launcher3.CellLayout.HOTSEAT;
import static com.android.launcher3.CellLayout.WORKSPACE;
import android.app.WallpaperManager;
@@ -146,7 +147,8 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.
// No need to add padding when cell layout border spacing is present.
boolean noPaddingX =
(dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE)
|| (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER);
|| (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER)
|| (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT);
int cellPaddingX = noPaddingX
? 0
: mContainerType == WORKSPACE
@@ -251,7 +253,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
// While the folder is open, the position of the icon cannot change.
lp.canReorder = false;
if (mContainerType == CellLayout.HOTSEAT) {
if (mContainerType == HOTSEAT) {
CellLayout cl = (CellLayout) getParent();
cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
}
@@ -260,7 +262,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.
@Override
public void clearFolderLeaveBehind(FolderIcon child) {
((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true;
if (mContainerType == CellLayout.HOTSEAT) {
if (mContainerType == HOTSEAT) {
CellLayout cl = (CellLayout) getParent();
cl.clearFolderLeaveBehind();
}

View File

@@ -23,7 +23,10 @@ package {
// Source code used for test
filegroup {
name: "launcher-tests-src",
srcs: ["src/**/*.java"],
srcs: [
"src/**/*.java",
"src/**/*.kt"
],
}
// Source code used for oop test helpers

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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.
*/
package com.android.launcher3
import android.content.Context
import android.graphics.PointF
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.util.DisplayController.Info
import com.android.launcher3.util.WindowBounds
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceProfileTest {
private var context: Context? = null
private var inv: InvariantDeviceProfile? = null
private var info: Info = mock(Info::class.java)
private var windowBounds: WindowBounds? = null
private var isMultiWindowMode: Boolean = false
private var transposeLayoutWithOrientation: Boolean = false
private var useTwoPanels: Boolean = false
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
// make sure to reset values
useTwoPanels = false
}
@Test
fun qsbWidth_is_match_parent_for_phones() {
initializeVarsForPhone()
val dp = DeviceProfile(
context,
inv,
info,
windowBounds,
isMultiWindowMode,
transposeLayoutWithOrientation,
useTwoPanels
)
assertThat(dp.isQsbInline).isFalse()
assertThat(dp.qsbWidth).isEqualTo(0)
}
@Test
fun qsbWidth_is_match_parent_for_tablet_portrait() {
initializeVarsForTablet()
val dp = DeviceProfile(
context,
inv,
info,
windowBounds,
isMultiWindowMode,
transposeLayoutWithOrientation,
useTwoPanels
)
assertThat(dp.isQsbInline).isFalse()
assertThat(dp.qsbWidth).isEqualTo(0)
}
@Test
fun qsbWidth_has_size_for_tablet_landscape() {
initializeVarsForTablet(true)
val dp = DeviceProfile(
context,
inv,
info,
windowBounds,
isMultiWindowMode,
transposeLayoutWithOrientation,
useTwoPanels
)
if (dp.hotseatQsbHeight > 0) {
assertThat(dp.isQsbInline).isTrue()
assertThat(dp.qsbWidth).isGreaterThan(0)
} else {
assertThat(dp.isQsbInline).isFalse()
assertThat(dp.qsbWidth).isEqualTo(0)
}
}
/**
* This test is to make sure that two panels don't inline the QSB as tablets do
*/
@Test
fun qsbWidth_is_match_parent_for_two_panel_landscape() {
initializeVarsForTablet(true)
useTwoPanels = true
val dp = DeviceProfile(
context,
inv,
info,
windowBounds,
isMultiWindowMode,
transposeLayoutWithOrientation,
useTwoPanels
)
assertThat(dp.isQsbInline).isFalse()
assertThat(dp.qsbWidth).isEqualTo(0)
}
private fun initializeVarsForPhone(isLandscape: Boolean = false) {
val (x, y) = if (isLandscape)
Pair(3120, 1440)
else
Pair(1440, 3120)
windowBounds = WindowBounds(x, y, x, y - 100)
`when`(info.isTablet(any())).thenReturn(false)
scalableInvariantDeviceProfile()
}
private fun initializeVarsForTablet(isLandscape: Boolean = false) {
val (x, y) = if (isLandscape)
Pair(2560, 1600)
else
Pair(1600, 2560)
windowBounds = WindowBounds(x, y, x, y - 100)
`when`(info.isTablet(any())).thenReturn(true)
scalableInvariantDeviceProfile()
}
/**
* A very generic grid, just to make qsb tests work. For real calculations, make sure to use
* values that better represent a real grid.
*/
private fun scalableInvariantDeviceProfile() {
inv = InvariantDeviceProfile().apply {
isScalable = true
numColumns = 5
numRows = 5
horizontalMargin = FloatArray(4) { 22f }
borderSpaces = listOf(
PointF(16f, 16f),
PointF(16f, 16f),
PointF(16f, 16f),
PointF(16f, 16f)
).toTypedArray()
allAppsBorderSpaces = listOf(
PointF(16f, 16f),
PointF(16f, 16f),
PointF(16f, 16f),
PointF(16f, 16f)
).toTypedArray()
hotseatBorderSpaces = FloatArray(4) { 16f }
iconSize = FloatArray(4) { 56f }
allAppsIconSize = FloatArray(4) { 56f }
iconTextSize = FloatArray(4) { 14f }
allAppsIconTextSize = FloatArray(4) { 14f }
minCellSize = listOf(
PointF(64f, 83f),
PointF(64f, 83f),
PointF(64f, 83f),
PointF(64f, 83f)
).toTypedArray()
}
}
}