Upon expanding, expand just enough so the header shows.
This issue only happens when there is a lot of private space apps that scrolling to the bottom
will not sure the private space header.
Formula to calculate how many rows to scroll to =
(appListHeight - privateHeaderHeight - headerProtectionHeight) / cellHeight.
bug: 299294792
Test:
manually - https://screenshot.googleplex.com/76UJPT2Jnpnp2Ab
before: it just scrolls all the way to the bottom
after: https://drive.google.com/file/d/1AbprxFm1RWTQKvpt7M4khbUfc1o6-XGF/view?usp=sharing
after PHONE WITH TABS:
2x2- https://drive.google.com/file/d/1SLPsWPHenCuZuisiS7HeEy5JwtNPeONs/view?usp=sharing
3x3- https://drive.google.com/file/d/1SK82jeNZMzFJK2odIuHnfTNLYfppne83/view?usp=sharing
4x4- https://drive.google.com/file/d/1T7EhFRq2tDv2zYIvs_FMsTKcFZXwGUaD/view?usp=sharing
4x5- https://drive.google.com/file/d/1SMUPuKjO1Yg36U6P6cDOb6dTkHn6Bh7D/view?usp=sharing
5x5- https://drive.google.com/file/d/1SJCQn1O_Yq5P7C__VUfZHc5I67CEdIpb/view?usp=sharing
AFTER PHONE NO TABS:
2x2: https://drive.google.com/file/d/1THU2xrAIt0hTmN5_GwBrgN9Lqj-W4Kfr/view?usp=sharing
3x3: https://drive.google.com/file/d/1TPTUx7PcHW3GsVwVAg_L5rCcn0QYoiY2/view?usp=sharing
4x4: https://drive.google.com/file/d/1TWVWpAX6bZp_JfFKtmXYO0askl4e5qKO/view?usp=sharing
4x5: https://drive.google.com/file/d/1TDJK-swmY3Y3C4ARH_2eljqUkBGEnD3e/view?usp=sharing
5x5- https://drive.google.com/file/d/1TBJtAynwvZrGyOc-29f637wyrJZpMXBJ/view?usp=sharing
Tablet:
landscape: https://drive.google.com/file/d/1SfyPdoUnCV7e7BWLnpxXWN2HiBOQkRo2/view?usp=sharing
portrait: https://drive.google.com/file/d/1SgZq0iE9WMvIFtc8mBb577nYlS9jBa_g/view?usp=sharing
Flag: ACONFIG com.android.launcher3.Flags.private_space_animation TRUNKFOOD
Change-Id: If70df1299572f8f2edc6376dd2a6df5d74287264
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 6acfcd0..965e97c 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -189,6 +189,7 @@
private float mBottomSheetAlpha = 1f;
private boolean mForceBottomSheetVisible;
private int mTabsProtectionAlpha;
+ private float mTotalHeaderProtectionHeight;
@Nullable private AllAppsTransitionController mAllAppsTransitionController;
private PrivateSpaceHeaderViewController mPrivateSpaceHeaderViewController;
@@ -1429,9 +1430,11 @@
mTmpPath.reset();
mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
canvas.drawPath(mTmpPath, mHeaderPaint);
+ mTotalHeaderProtectionHeight = headerBottomWithScaleOnTablet;
}
} else {
canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint);
+ mTotalHeaderProtectionHeight = headerBottomWithScaleOnPhone;
}
// If tab exist (such as work profile), extend header with tab height
@@ -1461,10 +1464,19 @@
right,
tabBottomWithScale,
mHeaderPaint);
+ mTotalHeaderProtectionHeight = tabBottomWithScale;
}
}
/**
+ * The height of the header protection is dynamically calculated during the time of drawing the
+ * header.
+ */
+ float getHeaderProtectionHeight() {
+ return mTotalHeaderProtectionHeight;
+ }
+
+ /**
* redraws header protection
*/
public void invalidateHeader() {
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index 6067454..b151b3a 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -41,15 +41,18 @@
import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import com.android.app.animation.Interpolators;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.allapps.UserProfileManager.UserProfileState;
import com.android.launcher3.anim.AnimatedPropertySetter;
import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.RecyclerViewFastScroller;
import java.util.List;
@@ -59,7 +62,6 @@
* {@link UserProfileState}
*/
public class PrivateSpaceHeaderViewController {
- private static final int EXPAND_SCROLL_DURATION = 2000;
private static final int EXPAND_COLLAPSE_DURATION = 800;
private static final int SETTINGS_OPACITY_DURATION = 160;
private final ActivityAllAppsContainerView mAllApps;
@@ -174,7 +176,12 @@
&& mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) {
// Animate the text and settings icon.
updatePrivateStateAnimator(true, header);
- mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION);
+ DeviceProfile deviceProfile =
+ ActivityContext.lookupContext(mAllApps.getContext()).getDeviceProfile();
+ AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView();
+ scrollForViewToBeVisibleInContainer(allAppsRecyclerView,
+ allAppsRecyclerView.getApps().getAdapterItems(),
+ header.getHeight(), deviceProfile.allAppsCellHeightPx);
}
}
@@ -212,6 +219,57 @@
}
}
+ /**
+ * Upon expanding, only scroll to the item position in the adapter that allows the header to be
+ * visible.
+ */
+ @VisibleForTesting
+ public int scrollForViewToBeVisibleInContainer(
+ AllAppsRecyclerView allAppsRecyclerView,
+ List<BaseAllAppsAdapter.AdapterItem> appListAdapterItems,
+ int psHeaderHeight,
+ int allAppsCellHeight) {
+ int rowToExpandToWithRespectToHeader = -1;
+ int itemToScrollTo = -1;
+ // Looks for the item in the app list to scroll to so that the header is visible.
+ for (int i = 0; i < appListAdapterItems.size(); i++) {
+ BaseAllAppsAdapter.AdapterItem currentItem = appListAdapterItems.get(i);
+ if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
+ itemToScrollTo = i;
+ continue;
+ }
+ if (itemToScrollTo != -1) {
+ if (rowToExpandToWithRespectToHeader == -1) {
+ rowToExpandToWithRespectToHeader = currentItem.rowIndex;
+ }
+ int rowToScrollTo =
+ (int) Math.floor((double) (mAllApps.getHeight() - psHeaderHeight
+ - mAllApps.getHeaderProtectionHeight()) / allAppsCellHeight);
+ int currentRowDistance = currentItem.rowIndex - rowToExpandToWithRespectToHeader;
+ // rowToScrollTo - 1 since the item to scroll to is 0 indexed.
+ if (currentRowDistance == rowToScrollTo - 1) {
+ itemToScrollTo = i;
+ break;
+ }
+ }
+ }
+ if (itemToScrollTo != -1) {
+ // Note: SmoothScroller is meant to be used once.
+ RecyclerView.SmoothScroller smoothScroller =
+ new LinearSmoothScroller(mAllApps.getContext()) {
+ @Override protected int getVerticalSnapPreference() {
+ return LinearSmoothScroller.SNAP_TO_ANY;
+ }
+ };
+ smoothScroller.setTargetPosition(itemToScrollTo);
+ RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
+ if (layoutManager != null) {
+ layoutManager.startSmoothScroll(smoothScroller);
+ }
+ }
+ return itemToScrollTo;
+ }
+
PrivateProfileManager getPrivateProfileManager() {
return mPrivateProfileManager;
}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
index 490cb47..043461d 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
@@ -18,6 +18,7 @@
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
import static com.android.launcher3.allapps.UserProfileManager.STATE_TRANSITION;
@@ -25,13 +26,19 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalAnswers.answer;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Process;
+import android.os.UserHandle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
@@ -44,6 +51,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.R;
+import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.ActivityContextWrapper;
import org.junit.Before;
@@ -52,32 +60,53 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public class PrivateSpaceHeaderViewControllerTest {
+ private static final UserHandle MAIN_HANDLE = Process.myUserHandle();
+ private static final UserHandle PRIVATE_HANDLE = new UserHandle(11);
private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1;
private static final int LOCK_UNLOCK_BUTTON_COUNT = 1;
private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1;
private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0;
private static final int PS_TRANSITION_IMAGE_COUNT = 1;
+ private static final int NUM_APP_COLS = 4;
+ private static final int NUM_PRIVATE_SPACE_APPS = 50;
+ private static final int ALL_APPS_HEIGHT = 10;
+ private static final int ALL_APPS_CELL_HEIGHT = 1;
+ private static final int PS_HEADER_HEIGHT = 1;
+ private static final int BIGGER_PS_HEADER_HEIGHT = 2;
+ private static final int SCROLL_NO_WHERE = -1;
+ private static final float HEADER_PROTECTION_HEIGHT = 1F;
private Context mContext;
private PrivateSpaceHeaderViewController mPsHeaderViewController;
private RelativeLayout mPsHeaderLayout;
+ private AlphabeticalAppsList<?> mAlphabeticalAppsList;
@Mock
private PrivateProfileManager mPrivateProfileManager;
@Mock
private ActivityAllAppsContainerView mAllApps;
+ @Mock
+ private AllAppsStore<?> mAllAppsStore;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = new ActivityContextWrapper(getApplicationContext());
+ when(mPrivateProfileManager.getItemInfoMatcher()).thenReturn(info ->
+ info != null && info.user.equals(PRIVATE_HANDLE));
mPsHeaderViewController = new PrivateSpaceHeaderViewController(mAllApps,
mPrivateProfileManager);
mPsHeaderLayout = (RelativeLayout) LayoutInflater.from(mContext).inflate(
R.layout.private_space_header, null);
+ mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore,
+ null, mPrivateProfileManager);
+ mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS);
}
@Test
@@ -223,6 +252,88 @@
assertEquals(PS_TRANSITION_IMAGE_COUNT, totalLockUnlockButtonView);
}
+ @Test
+ public void scrollForViewToBeVisibleInContainer_withHeader() {
+ when(mAllAppsStore.getApps()).thenReturn(createAppInfoList());
+ when(mPrivateProfileManager.addPrivateSpaceHeader(any()))
+ .thenAnswer(answer(this::addPrivateSpaceHeader));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
+ when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps())
+ .thenReturn(iteminfo -> iteminfo.componentName == null
+ || !iteminfo.componentName.getPackageName()
+ .equals("com.android.launcher3.tests.camera"));
+ when(mAllApps.getContext()).thenReturn(mContext);
+ mAlphabeticalAppsList.updateItemFilter(info -> info != null
+ && info.user.equals(MAIN_HANDLE));
+ when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT);
+ when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT);
+ int rows = (int) (ALL_APPS_HEIGHT - PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
+ int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
+
+ // The number of adapterItems should be the private space apps + one main app + header.
+ assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+ mAlphabeticalAppsList.getAdapterItems().size());
+ assertEquals(position,
+ mPsHeaderViewController.scrollForViewToBeVisibleInContainer(
+ new AllAppsRecyclerView(mContext),
+ mAlphabeticalAppsList.getAdapterItems(),
+ PS_HEADER_HEIGHT,
+ ALL_APPS_CELL_HEIGHT));
+ }
+
+ @Test
+ public void scrollForViewToBeVisibleInContainer_withHeaderAndLessAppRowSpace() {
+ when(mAllAppsStore.getApps()).thenReturn(createAppInfoList());
+ when(mPrivateProfileManager.addPrivateSpaceHeader(any()))
+ .thenAnswer(answer(this::addPrivateSpaceHeader));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
+ when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps())
+ .thenReturn(iteminfo -> iteminfo.componentName == null
+ || !iteminfo.componentName.getPackageName()
+ .equals("com.android.launcher3.tests.camera"));
+ when(mAllApps.getContext()).thenReturn(mContext);
+ mAlphabeticalAppsList.updateItemFilter(info -> info != null
+ && info.user.equals(MAIN_HANDLE));
+ when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT);
+ when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT);
+ int rows = (int) (ALL_APPS_HEIGHT - BIGGER_PS_HEADER_HEIGHT - HEADER_PROTECTION_HEIGHT);
+ int position = rows * NUM_APP_COLS - (NUM_APP_COLS-1) + 1;
+
+ // The number of adapterItems should be the private space apps + one main app + header.
+ assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1,
+ mAlphabeticalAppsList.getAdapterItems().size());
+ assertEquals(position,
+ mPsHeaderViewController.scrollForViewToBeVisibleInContainer(
+ new AllAppsRecyclerView(mContext),
+ mAlphabeticalAppsList.getAdapterItems(),
+ BIGGER_PS_HEADER_HEIGHT,
+ ALL_APPS_CELL_HEIGHT));
+ }
+
+ @Test
+ public void scrollForViewToBeVisibleInContainer_withNoHeader() {
+ when(mAllAppsStore.getApps()).thenReturn(createAppInfoList());
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
+ when(mPrivateProfileManager.splitIntoUserInstalledAndSystemApps())
+ .thenReturn(iteminfo -> iteminfo.componentName == null
+ || !iteminfo.componentName.getPackageName()
+ .equals("com.android.launcher3.tests.camera"));
+ when(mAllApps.getContext()).thenReturn(mContext);
+ mAlphabeticalAppsList.updateItemFilter(info -> info != null
+ && info.user.equals(MAIN_HANDLE));
+ when(mAllApps.getHeight()).thenReturn(ALL_APPS_HEIGHT);
+ when(mAllApps.getHeaderProtectionHeight()).thenReturn(HEADER_PROTECTION_HEIGHT);
+
+ // The number of adapterItems should be the private space apps + one main app.
+ assertEquals(NUM_PRIVATE_SPACE_APPS + 1,
+ mAlphabeticalAppsList.getAdapterItems().size());
+ assertEquals(SCROLL_NO_WHERE, mPsHeaderViewController.scrollForViewToBeVisibleInContainer(
+ new AllAppsRecyclerView(mContext),
+ mAlphabeticalAppsList.getAdapterItems(),
+ BIGGER_PS_HEADER_HEIGHT,
+ ALL_APPS_CELL_HEIGHT));
+ }
+
private Bitmap getBitmap(Drawable drawable) {
Bitmap result;
if (drawable instanceof BitmapDrawable) {
@@ -249,4 +360,28 @@
private static void awaitTasksCompleted() throws Exception {
UI_HELPER_EXECUTOR.submit(() -> null).get();
}
+
+ private int addPrivateSpaceHeader(List<BaseAllAppsAdapter.AdapterItem> adapterItemList) {
+ BaseAllAppsAdapter.AdapterItem privateSpaceHeader =
+ new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER);
+ adapterItemList.add(privateSpaceHeader);
+ return adapterItemList.size();
+ }
+
+ private AppInfo[] createAppInfoList() {
+ List<AppInfo> appInfos = new ArrayList<>();
+ ComponentName gmailComponentName = new ComponentName(mContext,
+ "com.android.launcher3.tests.Activity" + "Gmail");
+ AppInfo gmailAppInfo = new
+ AppInfo(gmailComponentName, "Gmail", MAIN_HANDLE, new Intent());
+ appInfos.add(gmailAppInfo);
+ ComponentName privateCameraComponentName = new ComponentName(
+ "com.android.launcher3.tests.camera", "CameraActivity");
+ for (int i = 0; i < NUM_PRIVATE_SPACE_APPS; i++) {
+ AppInfo privateCameraAppInfo = new AppInfo(privateCameraComponentName,
+ "Private Camera " + i, PRIVATE_HANDLE, new Intent());
+ appInfos.add(privateCameraAppInfo);
+ }
+ return appInfos.toArray(AppInfo[]::new);
+ }
}