diff --git a/go/quickstep/res/values/config.xml b/go/quickstep/res/values/config.xml index 9dca137562..a21381c745 100644 --- a/go/quickstep/res/values/config.xml +++ b/go/quickstep/res/values/config.xml @@ -20,5 +20,5 @@ - false + true \ No newline at end of file diff --git a/quickstep/Android.bp b/quickstep/Android.bp index 585b6ada88..38c9919b07 100644 --- a/quickstep/Android.bp +++ b/quickstep/Android.bp @@ -12,6 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "packages_apps_Launcher3_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["packages_apps_Launcher3_license"], +} + filegroup { name: "launcher3-quickstep-robolectric-src", path: "robolectric_tests", diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml index 7b85d9bcd5..a3d0070697 100644 --- a/res/layout/widget_cell_content.xml +++ b/res/layout/widget_cell_content.xml @@ -18,10 +18,11 @@ android:layout_height="wrap_content"> + should be done using margins. + width & height are set at runtime after scaling the preview image. --> + android:textSize="@dimen/widget_cell_font_size" /> @@ -48,7 +49,7 @@ android:layout_height="wrap_content" android:gravity="center_horizontal" android:textColor="?android:attr/textColorTertiary" - android:textSize="14sp" + android:textSize="@dimen/widget_cell_font_size" android:alpha="0.8" /> + + diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml index 021058b65d..1db7462a0f 100644 --- a/res/layout/widgets_search_bar.xml +++ b/res/layout/widgets_search_bar.xml @@ -6,7 +6,8 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="16dp" - android:background="@drawable/bg_widgets_searchbox"> + android:background="@drawable/bg_widgets_searchbox" + android:elevation="2dp"> 8dp 16dp + 14sp 28dp @@ -186,7 +187,6 @@ 0dp 234dp 56dp - 48dp 6dp 16dp diff --git a/robolectric_tests/Android.bp b/robolectric_tests/Android.bp index 50309b7902..bf323623b2 100644 --- a/robolectric_tests/Android.bp +++ b/robolectric_tests/Android.bp @@ -16,6 +16,15 @@ // Launcher Robolectric test target. // // "robolectric_android-all-stub", not needed, we write our own stubs +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "packages_apps_Launcher3_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["packages_apps_Launcher3_license"], +} + filegroup { name: "launcher3-robolectric-resources", path: "resources", @@ -47,4 +56,3 @@ android_robolectric_test { timeout: 36000, }, } - diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java index 5954efa95f..c67efefa5d 100644 --- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java +++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java @@ -97,7 +97,7 @@ public class ClippedFolderIconLayoutRule { double thetaShift = 0; if (curNumItems == 3) { - thetaShift = Math.PI / 6; + thetaShift = Math.PI / 2; } else if (curNumItems == 4) { thetaShift = Math.PI / 4; } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index bcb3a54ef3..ec7155c1b1 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -26,6 +26,7 @@ import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTI import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED; +import static com.android.launcher3.util.DisplayController.getSingleFrameMs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -695,6 +696,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mPageIndicator.stopAllAnimations(); startAnimation(anim); + // Because t=0 has the folder match the folder icon, we can skip the + // first frame and have the same movement one frame earlier. + anim.setCurrentPlayTime(Math.min(getSingleFrameMs(getContext()), anim.getTotalDuration())); // Make sure the folder picks up the last drag move even if the finger doesn't move. if (mDragController.isDragging()) { diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java index d44d158010..e954480ca5 100644 --- a/src/com/android/launcher3/notification/NotificationItemView.java +++ b/src/com/android/launcher3/notification/NotificationItemView.java @@ -92,6 +92,10 @@ public class NotificationItemView { }); } + public void updateBackgroundColor(int color) { + mMainView.updateBackgroundColor(color); + } + public void addGutter() { if (mGutter == null) { mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView); diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java index 9b065233b9..c9956667db 100644 --- a/src/com/android/launcher3/notification/NotificationMainView.java +++ b/src/com/android/launcher3/notification/NotificationMainView.java @@ -97,15 +97,24 @@ public class NotificationMainView extends FrameLayout implements SingleAxisSwipe super.onFinishInflate(); mTextAndBackground = findViewById(R.id.text_and_background); - ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground(); - mBackgroundColor = colorBackground.getColor(); - RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf( - Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)), - colorBackground, null); - mTextAndBackground.setBackground(rippleBackground); mTitleView = mTextAndBackground.findViewById(R.id.title); mTextView = mTextAndBackground.findViewById(R.id.text); mIconView = findViewById(R.id.popup_item_icon); + + ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground(); + updateBackgroundColor(colorBackground.getColor()); + } + + public void updateBackgroundColor(int color) { + mBackgroundColor = color; + RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf( + Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)), + new ColorDrawable(color), null); + mTextAndBackground.setBackground(rippleBackground); + if (mNotificationInfo != null) { + mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), + mBackgroundColor)); + } } public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) { diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index 6aa98122cd..a53fe1f848 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -17,10 +17,12 @@ package com.android.launcher3.popup; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; +import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; @@ -28,6 +30,8 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Outline; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.util.AttributeSet; import android.util.Pair; @@ -63,6 +67,9 @@ import java.util.Collections; */ public abstract class ArrowPopup extends AbstractFloatingView { + // +1 for system shortcut view + private static final int MAX_NUM_CHILDREN = MAX_SHORTCUTS + 1; + private final Rect mTempRect = new Rect(); protected final LayoutInflater mInflater; @@ -93,6 +100,9 @@ public abstract class ArrowPopup extends Abstrac private Runnable mOnCloseCallback = () -> { }; + private int mArrowColor; + private final int[] mColors; + public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mInflater = LayoutInflater.from(context); @@ -121,15 +131,22 @@ public abstract class ArrowPopup extends Abstrac mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius); mRoundedTop = new GradientDrawable(); - mRoundedTop.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary)); mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, 0, 0, 0, 0}); mRoundedBottom = new GradientDrawable(); - mRoundedBottom.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary)); mRoundedBottom.setCornerRadii(new float[] { 0, 0, 0, 0, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius}); + int primaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary); + int secondaryColor = Themes.getAttrColor(context, R.attr.popupColorSecondary); + ArgbEvaluator argb = new ArgbEvaluator(); + mColors = new int[MAX_NUM_CHILDREN]; + // Interpolate between the two colors, exclusive. + float step = 1f / (MAX_NUM_CHILDREN + 1); + for (int i = 0; i < mColors.length; ++i) { + mColors[i] = (int) argb.evaluate((i + 1) * step, primaryColor, secondaryColor); + } } public ArrowPopup(Context context, AttributeSet attrs) { @@ -187,6 +204,7 @@ public abstract class ArrowPopup extends Abstrac int numVisibleShortcut = 0; View lastView = null; + int numVisibleChild = 0; for (int i = 0; i < count; i++) { View view = getChildAt(i); boolean isShortcut = view instanceof DeepShortcutView; @@ -207,15 +225,41 @@ public abstract class ArrowPopup extends Abstrac view.setBackground(mRoundedTop); } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) { view.setBackground(mRoundedBottom); + } else { + view.setBackgroundResource(R.drawable.middle_item_primary); } numVisibleShortcut++; } } + + int color = mColors[numVisibleChild % mColors.length]; + setChildColor(view, color); + + // Arrow color matches the first child or the last child. + if (!mIsAboveIcon && numVisibleChild == 0) { + mArrowColor = color; + } else if (mIsAboveIcon) { + mArrowColor = color; + } + + numVisibleChild++; } } measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); } + /** + * Sets the background color of the child. + */ + protected void setChildColor(View view, int color) { + Drawable bg = view.getBackground(); + if (bg instanceof GradientDrawable) { + ((GradientDrawable) bg.mutate()).setColor(color); + } else if (bg instanceof ColorDrawable) { + ((ColorDrawable) bg.mutate()).setColor(color); + } + } + /** * Shows the popup at the desired location, optionally reversing the children. * @param viewsToFlip number of views from the top to to flip in case of reverse order @@ -293,7 +337,7 @@ public abstract class ArrowPopup extends Abstrac mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(), mArrowOffsetHorizontal, -mArrowOffsetVertical, !mIsAboveIcon, mIsLeftAligned, - Themes.getAttrColor(getContext(), R.attr.popupColorPrimary))); + mArrowColor)); mArrow.setElevation(getElevation()); } diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 4087f49f8e..c282ae8287 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -60,6 +60,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.notification.NotificationItemView; import com.android.launcher3.notification.NotificationKeyData; +import com.android.launcher3.notification.NotificationMainView; import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; @@ -170,6 +171,14 @@ public class PopupContainerWithArrow extends Arr return false; } + @Override + protected void setChildColor(View v, int color) { + super.setChildColor(v, color); + if (v.getId() == R.id.notification_container && mNotificationItemView != null) { + mNotificationItemView.updateBackgroundColor(color); + } + } + /** * Returns true if we can show the container. */ @@ -333,19 +342,11 @@ public class PopupContainerWithArrow extends Arr private void updateHiddenShortcuts() { int allowedCount = mNotificationItemView != null ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; - int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height); - int itemHeight = mNotificationItemView != null ? - getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height) - : originalHeight; - float iconScale = ((float) itemHeight) / originalHeight; int total = mShortcuts.size(); for (int i = 0; i < total; i++) { DeepShortcutView view = mShortcuts.get(i); view.setVisibility(i >= allowedCount ? GONE : VISIBLE); - view.getLayoutParams().height = itemHeight; - view.getIconView().setScaleX(iconScale); - view.getIconView().setScaleY(iconScale); } } @@ -567,6 +568,7 @@ public class PopupContainerWithArrow extends Arr // No more notifications, remove the notification views and expand all shortcuts. mNotificationItemView.removeAllViews(); mNotificationItemView = null; + mNotificationContainer.setVisibility(GONE); updateHiddenShortcuts(); assignMarginsAndBackgrounds(); } else { diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 5328041d16..1b0e1ce0b2 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -74,6 +74,7 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { protected int mPreviewHeight; protected int mPresetPreviewSize; private int mCellSize; + private float mPreviewScale = 1f; private WidgetImageView mWidgetImage; private TextView mWidgetName; @@ -254,8 +255,8 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { } if (drawable != null) { LayoutParams layoutParams = (LayoutParams) mWidgetImage.getLayoutParams(); - layoutParams.width = drawable.getIntrinsicWidth(); - layoutParams.height = drawable.getIntrinsicHeight(); + layoutParams.width = (int) (drawable.getIntrinsicWidth() * mPreviewScale); + layoutParams.height = (int) (drawable.getIntrinsicHeight() * mPreviewScale); mWidgetImage.setLayoutParams(layoutParams); mWidgetImage.setDrawable(drawable, mWidgetPreviewLoader.getBadgeForUser(mItem.user, @@ -305,10 +306,16 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { /** Sets the widget preview image size in number of cells. */ public void setPreviewSize(int spanX, int spanY) { + setPreviewSize(spanX, spanY, 1f); + } + + /** Sets the widget preview image size, in number of cells, and preview scale. */ + public void setPreviewSize(int spanX, int spanY, float previewScale) { int padding = 2 * getResources() .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding; mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding; + mPreviewScale = previewScale; } @Override diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java index 7eb5b834fd..7f840776b2 100644 --- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java +++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java @@ -35,6 +35,7 @@ final class SearchAndRecommendationsScrollController implements private final SearchAndRecommendationViewHolder mViewHolder; private final WidgetsRecyclerView mPrimaryRecyclerView; private final WidgetsRecyclerView mSearchRecyclerView; + private final int mTabsHeight; // The following are only non null if mHasWorkProfile is true. @Nullable private final WidgetsRecyclerView mWorkRecyclerView; @@ -42,10 +43,28 @@ final class SearchAndRecommendationsScrollController implements @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager; private WidgetsRecyclerView mCurrentRecyclerView; - private int mMaxCollapsibleHeight = 0; + + /** + * The vertical distance, in pixels, until the search is pinned at the top of the screen when + * the user scrolls down the recycler view. + */ + private int mCollapsibleHeightForSearch = 0; + /** + * The vertical distance, in pixels, until the recommendation table disappears from the top of + * the screen when the user scrolls down the recycler view. + */ + private int mCollapsibleHeightForRecommendation = 0; + /** + * The vertical distance, in pixels, until the tabs is pinned at the top of the screen when the + * user scrolls down the recycler view. + * + *

Always 0 if there is no work profile. + */ + private int mCollapsibleHeightForTabs = 0; SearchAndRecommendationsScrollController( boolean hasWorkProfile, + int tabsHeight, SearchAndRecommendationViewHolder viewHolder, WidgetsRecyclerView primaryRecyclerView, @Nullable WidgetsRecyclerView workRecyclerView, @@ -55,46 +74,63 @@ final class SearchAndRecommendationsScrollController implements mHasWorkProfile = hasWorkProfile; mViewHolder = viewHolder; mPrimaryRecyclerView = primaryRecyclerView; + mCurrentRecyclerView = mPrimaryRecyclerView; mWorkRecyclerView = workRecyclerView; mSearchRecyclerView = searchRecyclerView; mPrimaryWorkTabsView = personalWorkTabsView; mPrimaryWorkViewPager = primaryWorkViewPager; mCurrentRecyclerView = mPrimaryRecyclerView; + mTabsHeight = tabsHeight; } /** Sets the current active {@link WidgetsRecyclerView}. */ public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) { mCurrentRecyclerView = currentRecyclerView; + mCurrentRecyclerView = currentRecyclerView; + mViewHolder.mHeaderTitle.setTranslationY(0); + mViewHolder.mRecommendedWidgetsTable.setTranslationY(0); + mViewHolder.mSearchBar.setTranslationY(0); + + if (mHasWorkProfile) { + mPrimaryWorkTabsView.setTranslationY(0); + } } /** * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views. + * + * @return {@code true} if margins or/and padding of views in the search and recommendations + * container have been updated. */ - public void updateMarginAndPadding() { - // The maximum vertical distance, in pixels, until the last collapsible element is not - // visible from the screen when the user scrolls down the recycler view. - mMaxCollapsibleHeight = mViewHolder.mContainer.getPaddingTop() - + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle) - + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle); + public boolean updateMarginAndPadding() { + boolean hasMarginOrPaddingUpdated = false; + mCollapsibleHeightForSearch = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle); + mCollapsibleHeightForRecommendation = + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle) + + measureHeightWithVerticalMargins((View) mViewHolder.mSearchBar) + + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer); if (mHasWorkProfile) { + mCollapsibleHeightForTabs = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle) + + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable); // In a work profile setup, the full widget sheet contains the following views: - // ------- -| - // Widgets -|---> LinearLayout for search & recommendations - // Search bar -| - // Personal | Work + // ------- (pinned) -| + // Widgets (collapsible) -|---> LinearLayout for search & recommendations + // Search bar (pinned) -| + // Widgets recommendation (collapsible)-| + // Personal | Work (pinned) // View Pager // // Views after the search & recommendations are not bound by RelativelyLayout param. // To position them on the expected location, padding & margin are added to these views // Tabs should have a padding of the height of the search & recommendations container. - mPrimaryWorkTabsView.setPadding( - mPrimaryWorkTabsView.getPaddingLeft(), - topContainerHeight, - mPrimaryWorkTabsView.getPaddingRight(), - mPrimaryWorkTabsView.getPaddingBottom()); + RelativeLayout.LayoutParams tabsLayoutParams = + (RelativeLayout.LayoutParams) mPrimaryWorkTabsView.getLayoutParams(); + tabsLayoutParams.topMargin = topContainerHeight; + mPrimaryWorkTabsView.setLayoutParams(tabsLayoutParams); // Instead of setting the top offset directly, we split the top offset into two values: // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible @@ -124,39 +160,52 @@ final class SearchAndRecommendationsScrollController implements // // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and // mMaxCollapsibleDistance should equal to the top container height. - int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView) - - mPrimaryWorkTabsView.getPaddingTop(); int topOffsetAfterAllViewsCollapsed = - topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight; + topContainerHeight + mTabsHeight - mCollapsibleHeightForTabs; - RelativeLayout.LayoutParams layoutParams = + RelativeLayout.LayoutParams viewPagerLayoutParams = (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams(); - layoutParams.setMargins(0, topOffsetAfterAllViewsCollapsed, 0, 0); - mPrimaryWorkViewPager.setLayoutParams(layoutParams); - mPrimaryWorkViewPager.requestLayout(); + if (viewPagerLayoutParams.topMargin != topOffsetAfterAllViewsCollapsed) { + viewPagerLayoutParams.topMargin = topOffsetAfterAllViewsCollapsed; + mPrimaryWorkViewPager.setLayoutParams(viewPagerLayoutParams); + hasMarginOrPaddingUpdated = true; + } - mPrimaryRecyclerView.setPadding( - mPrimaryRecyclerView.getPaddingLeft(), - mMaxCollapsibleHeight, - mPrimaryRecyclerView.getPaddingRight(), - mPrimaryRecyclerView.getPaddingBottom()); - mWorkRecyclerView.setPadding( - mWorkRecyclerView.getPaddingLeft(), - mMaxCollapsibleHeight, - mWorkRecyclerView.getPaddingRight(), - mWorkRecyclerView.getPaddingBottom()); + if (mPrimaryRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { + mPrimaryRecyclerView.setPadding( + mPrimaryRecyclerView.getPaddingLeft(), + mCollapsibleHeightForTabs, + mPrimaryRecyclerView.getPaddingRight(), + mPrimaryRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } + if (mWorkRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) { + mWorkRecyclerView.setPadding( + mWorkRecyclerView.getPaddingLeft(), + mCollapsibleHeightForTabs, + mWorkRecyclerView.getPaddingRight(), + mWorkRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } } else { - mPrimaryRecyclerView.setPadding( - mPrimaryRecyclerView.getPaddingLeft(), - topContainerHeight, - mPrimaryRecyclerView.getPaddingRight(), - mPrimaryRecyclerView.getPaddingBottom()); + if (mPrimaryRecyclerView.getPaddingTop() != topContainerHeight) { + mPrimaryRecyclerView.setPadding( + mPrimaryRecyclerView.getPaddingLeft(), + topContainerHeight, + mPrimaryRecyclerView.getPaddingRight(), + mPrimaryRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } } - mSearchRecyclerView.setPadding( - mSearchRecyclerView.getPaddingLeft(), - topContainerHeight, - mSearchRecyclerView.getPaddingRight(), - mSearchRecyclerView.getPaddingBottom()); + if (mSearchRecyclerView.getPaddingTop() != topContainerHeight) { + mSearchRecyclerView.setPadding( + mSearchRecyclerView.getPaddingLeft(), + topContainerHeight, + mSearchRecyclerView.getPaddingRight(), + mSearchRecyclerView.getPaddingBottom()); + hasMarginOrPaddingUpdated = true; + } + return hasMarginOrPaddingUpdated; } /** @@ -168,13 +217,22 @@ final class SearchAndRecommendationsScrollController implements // Always use the recycler view offset because fast scroller offset has a different scale. int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY(); if (recyclerViewYOffset < 0) return; - if (mMaxCollapsibleHeight > 0) { - int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight); + + if (mCollapsibleHeightForRecommendation > 0) { + int yDisplacement = Math.max(-recyclerViewYOffset, + -mCollapsibleHeightForRecommendation); mViewHolder.mHeaderTitle.setTranslationY(yDisplacement); - mViewHolder.mSearchBar.setTranslationY(yDisplacement); - if (mHasWorkProfile) { - mPrimaryWorkTabsView.setTranslationY(yDisplacement); - } + mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement); + } + + if (mCollapsibleHeightForSearch > 0) { + int searchYDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForSearch); + mViewHolder.mSearchBar.setTranslationY(searchYDisplacement); + } + + if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) { + int yDisplacementForTabs = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForTabs); + mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs); } } @@ -189,6 +247,9 @@ final class SearchAndRecommendationsScrollController implements /** private the height, in pixel, + the vertical margins of a given view. */ private static int measureHeightWithVerticalMargins(View view) { + if (view.getVisibility() != View.VISIBLE) { + return 0; + } MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams(); return view.getMeasuredHeight() + marginLayoutParams.bottomMargin + marginLayoutParams.topMargin; diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index dc375ca414..1c34ece115 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -33,6 +33,7 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.TextView; @@ -48,6 +49,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.model.WidgetItem; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.TopRoundedCornerView; import com.android.launcher3.widget.BaseWidgetSheet; @@ -55,6 +57,7 @@ import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListene import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.search.SearchModeListener; import com.android.launcher3.widget.picker.search.WidgetsSearchBar; +import com.android.launcher3.widget.util.WidgetsTableUtils; import com.android.launcher3.workprofile.PersonalWorkPagedView; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; @@ -68,10 +71,15 @@ import java.util.function.Predicate; public class WidgetsFullSheet extends BaseWidgetSheet implements Insettable, ProviderChangedListener, OnActivePageChangedListener, WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { + private static final String TAG = WidgetsFullSheet.class.getSimpleName(); private static final long DEFAULT_OPEN_DURATION = 267; private static final long FADE_IN_DURATION = 150; private static final float VERTICAL_START_POSITION = 0.3f; + // The widget recommendation table can easily take over the entire screen on devices with small + // resolution or landscape on phone. This ratio defines the max percentage of content area that + // the table can display. + private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f; private final Rect mInsets = new Rect(); private final boolean mHasWorkProfile; @@ -81,10 +89,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet mCurrentUser.equals(entry.mPkgItem.user); private final Predicate mWorkWidgetsFilter = mPrimaryWidgetsFilter.negate(); + private final int mTabsHeight; + private final int mWidgetCellHorizontalPadding; @Nullable private PersonalWorkPagedView mViewPager; - private int mInitialTabsHeight = 0; private boolean mIsInSearchMode; + private int mMaxSpansPerRow = 4; private View mTabsView; private TextView mNoWidgetsView; private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder; @@ -96,6 +106,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); + mTabsHeight = mHasWorkProfile + ? getContext().getResources() + .getDimensionPixelSize(R.dimen.all_apps_header_tab_height) + : 0; + mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset( + R.dimen.widget_cell_horizontal_padding); } public WidgetsFullSheet(Context context, AttributeSet attrs) { @@ -140,6 +156,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet findViewById(R.id.search_and_recommendations_container)); mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController( mHasWorkProfile, + mTabsHeight, mSearchAndRecommendationViewHolder, findViewById(R.id.primary_widgets_list_view), mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null, @@ -150,6 +167,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mNoWidgetsView = findViewById(R.id.no_widgets_text); + onRecommendedWidgetsBound(); onWidgetsBound(); mSearchAndRecommendationViewHolder.mSearchBar.initialize( @@ -257,6 +275,22 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + } + + if (updateMaxSpansPerRow()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) { + doMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + + private void doMeasure(int widthMeasureSpec, int heightMeasureSpec) { DeviceProfile deviceProfile = mLauncher.getDeviceProfile(); int widthUsed; if (mInsets.bottom > 0) { @@ -272,24 +306,29 @@ public class WidgetsFullSheet extends BaseWidgetSheet widthUsed, heightMeasureSpec, heightUsed); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); + } - int paddingPx = 2 * getResources().getDimensionPixelOffset( - R.dimen.widget_cell_horizontal_padding); - int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx + paddingPx); - mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); - mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); - if (mHasWorkProfile) { - mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( - maxSpansPerRow); + /** Returns {@code true} if the max spans have been updated. */ + private boolean updateMaxSpansPerRow() { + if (getMeasuredWidth() == 0) return false; + + int previousMaxSpansPerRow = mMaxSpansPerRow; + mMaxSpansPerRow = getMeasuredWidth() + / (mLauncher.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding); + + if (previousMaxSpansPerRow != mMaxSpansPerRow) { + mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + if (mHasWorkProfile) { + mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow( + mMaxSpansPerRow); + } + onRecommendedWidgetsBound(); + return true; } - - if (mInitialTabsHeight == 0 && mTabsView != null) { - mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView); - } - - mSearchAndRecommendationsScrollController.updateMarginAndPadding(); + return false; } @Override @@ -346,6 +385,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet mViewPager.snapToPage(AdapterHolder.PRIMARY); } attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); + + mSearchAndRecommendationsScrollController.updateMarginAndPadding(); } @Override @@ -357,6 +398,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { mIsInSearchMode = isInSearchMode; + mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable + .setVisibility(isInSearchMode ? GONE : VISIBLE); if (mHasWorkProfile) { mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE); mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE); @@ -374,6 +417,25 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader(); } + @Override + public void onRecommendedWidgetsBound() { + List recommendedWidgets = + mLauncher.getPopupDataProvider().getRecommendedWidgets(); + WidgetsRecommendationTableLayout table = + mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable; + if (recommendedWidgets.size() > 0) { + float maxTableHeight = + (mLauncher.getDeviceProfile().heightPx - mTabsHeight - getHeaderViewHeight()) + * RECOMMENDATION_TABLE_HEIGHT_RATIO; + List> recommendedWidgetsInTable = + WidgetsTableUtils.groupWidgetItemsIntoTable(recommendedWidgets, + mMaxSpansPerRow); + table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight); + } else { + table.setVisibility(GONE); + } + } + private void open(boolean animate) { if (animate) { if (getPopupContainer().getInsets().bottom > 0) { @@ -534,20 +596,29 @@ public class WidgetsFullSheet extends BaseWidgetSheet mWidgetsRecyclerView.setEdgeEffectFactory( ((TopRoundedCornerView) mContent).createEdgeEffectFactory()); mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView); + mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow); } } final class SearchAndRecommendationViewHolder { - final View mContainer; + final ViewGroup mContainer; final View mCollapseHandle; final WidgetsSearchBar mSearchBar; final TextView mHeaderTitle; + final WidgetsRecommendationTableLayout mRecommendedWidgetsTable; - SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) { + SearchAndRecommendationViewHolder(ViewGroup searchAndRecommendationContainer) { mContainer = searchAndRecommendationContainer; mCollapseHandle = mContainer.findViewById(R.id.collapse_handle); mSearchBar = mContainer.findViewById(R.id.widgets_search_bar); mHeaderTitle = mContainer.findViewById(R.id.title); + mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table); + mRecommendedWidgetsTable.setWidgetCellOnTouchListener((view, event) -> { + getRecyclerView().onTouchEvent(event); + return false; + }); + mRecommendedWidgetsTable.setWidgetCellLongClickListener(WidgetsFullSheet.this); + mRecommendedWidgetsTable.setWidgetCellOnClickListener(WidgetsFullSheet.this); } } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java new file mode 100644 index 0000000000..6569fb0d48 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2021 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.widget.picker; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TableLayout; +import android.widget.TableRow; + +import androidx.annotation.Nullable; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetImageView; + +import java.util.ArrayList; +import java.util.List; + +/** A {@link TableLayout} for showing recommended widgets. */ +public final class WidgetsRecommendationTableLayout extends TableLayout { + private static final float SCALE_DOWN_RATIO = 0.9f; + private final DeviceProfile mDeviceProfile; + private final float mWidgetCellTextViewsHeight; + + private float mRecommendationTableMaxHeight = Float.MAX_VALUE; + @Nullable private OnLongClickListener mWidgetCellOnLongClickListener; + @Nullable private OnClickListener mWidgetCellOnClickListener; + @Nullable private OnTouchListener mWidgetCellOnTouchListener; + + public WidgetsRecommendationTableLayout(Context context) { + this(context, /* attrs= */ null); + } + + public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile(); + // There are 1 row for title, 1 row for dimension and 2 rows for description. + mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size); + } + + /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */ + public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) { + mWidgetCellOnLongClickListener = onLongClickListener; + } + + /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */ + public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) { + mWidgetCellOnClickListener = widgetCellOnClickListener; + } + + /** Sets a {@link android.view.View.OnTouchListener} for all widget cells in this table. */ + public void setWidgetCellOnTouchListener(OnTouchListener widgetCellOnTouchListener) { + mWidgetCellOnTouchListener = widgetCellOnTouchListener; + } + + /** + * Sets a list of recommended widgets that would like to be displayed in this table within the + * desired {@code recommendationTableMaxHeight}. + * + *

If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a + * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only + * row still doesn't fit, we scale down the preview image. + */ + public void setRecommendedWidgets(List> recommendedWidgets, + float recommendationTableMaxHeight) { + mRecommendationTableMaxHeight = recommendationTableMaxHeight; + RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f, + recommendedWidgets); + bindData(data); + } + + private void bindData(RecommendationTableData data) { + if (data.mRecommendationTable.size() == 0) { + setVisibility(GONE); + return; + } + + removeAllViews(); + + for (int i = 0; i < data.mRecommendationTable.size(); i++) { + List widgetItems = data.mRecommendationTable.get(i); + TableRow tableRow = new TableRow(getContext()); + tableRow.setGravity(Gravity.TOP); + + for (WidgetItem widgetItem : widgetItems) { + WidgetCell widgetCell = addItemCell(tableRow); + widgetCell.setPreviewSize(widgetItem.spanX, widgetItem.spanY, data.mPreviewScale); + widgetCell.applyFromCellItem(widgetItem, + LauncherAppState.getInstance(getContext()).getWidgetCache()); + widgetCell.ensurePreview(); + } + addView(tableRow); + } + setVisibility(VISIBLE); + } + + private WidgetCell addItemCell(ViewGroup parent) { + WidgetCell widget = (WidgetCell) LayoutInflater.from( + getContext()).inflate(R.layout.widget_cell, parent, false); + + widget.setOnTouchListener(mWidgetCellOnTouchListener); + WidgetImageView preview = widget.findViewById(R.id.widget_preview); + preview.setOnClickListener(mWidgetCellOnClickListener); + preview.setOnLongClickListener(mWidgetCellOnLongClickListener); + widget.setAnimatePreview(false); + + parent.addView(widget); + return widget; + } + + private RecommendationTableData fitRecommendedWidgetsToTableSpace( + float previewScale, + List> recommendedWidgetsInTable) { + // A naive estimation of the widgets recommendation table height without inflation. + float totalHeight = 0; + for (int i = 0; i < recommendedWidgetsInTable.size(); i++) { + List widgetItems = recommendedWidgetsInTable.get(i); + float rowHeight = 0; + for (int j = 0; j < widgetItems.size(); j++) { + float previewHeight = widgetItems.get(j).spanY * mDeviceProfile.allAppsCellHeightPx + * previewScale; + rowHeight = Math.max(rowHeight, previewHeight + mWidgetCellTextViewsHeight); + } + totalHeight += rowHeight; + } + + if (totalHeight < mRecommendationTableMaxHeight) { + return new RecommendationTableData(recommendedWidgetsInTable, previewScale); + } + + if (recommendedWidgetsInTable.size() > 1) { + // We don't want to scale down widgets preview unless we really need to. Reduce the + // num of row by 1 to see if it fits. + return fitRecommendedWidgetsToTableSpace( + previewScale, + recommendedWidgetsInTable.subList(/* fromIndex= */0, + /* toIndex= */recommendedWidgetsInTable.size() - 1)); + } + + float nextPreviewScale = previewScale * SCALE_DOWN_RATIO; + return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable); + } + + /** Data class for the widgets recommendation table and widgets preview scaling. */ + private class RecommendationTableData { + private final List> mRecommendationTable; + private final float mPreviewScale; + + RecommendationTableData(List> recommendationTable, + float previewScale) { + mRecommendationTable = recommendationTable; + mPreviewScale = previewScale; + } + } +} diff --git a/tests/Android.bp b/tests/Android.bp index 8a73483bb3..da55c28684 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -11,6 +11,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. +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "packages_apps_Launcher3_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["packages_apps_Launcher3_license"], +} + filegroup { name: "launcher3-test-src-common", srcs: ["src_common/**/*.java"], diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index a7b92b7e9c..7bfe33c9ba 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -1214,7 +1214,7 @@ public final class LauncherInstrumentation { final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y); assertTrue("injectInputEvent failed", - mInstrumentation.getUiAutomation().injectInputEvent(event, true)); + mInstrumentation.getUiAutomation().injectInputEvent(event, true, false)); event.recycle(); } diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 22f4d31bd5..fe4c71282f 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -30,7 +30,6 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.testing.TestProtocol; import java.util.Collection; -import java.util.List; /** * All widgets container. @@ -106,7 +105,8 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS)); final Point displaySize = mLauncher.getRealDisplaySize(); - final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(); + Rect headerRect = new Rect(); + final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(headerRect); mLauncher.assertTrue("Can't locate widgets list for the test app: " + mLauncher.getLauncherPackageName(), widgetsContainer != null); @@ -132,7 +132,12 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { mLauncher.assertTrue("Too many attempts", ++i <= 40); final int scroll = getWidgetsScroll(); - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, tableRows, 0); + mLauncher.scroll( + fullWidgetsPicker, + Direction.DOWN, + headerRect, + 10, + true); final int newScroll = getWidgetsScroll(); mLauncher.assertTrue( "Scrolled in a wrong direction in Widgets: from " + scroll + " to " @@ -143,7 +148,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { } /** Finds the widgets list of this test app from the collapsed full widgets picker. */ - private UiObject2 findTestAppWidgetsTableContainer() { + private UiObject2 findTestAppWidgetsTableContainer(Rect outHeaderRect) { final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(), "widgets_list_header"); final BySelector targetAppSelector = By.clazz("android.widget.TextView").text( @@ -156,6 +161,7 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { UiObject2 fullWidgetsPicker = verifyActiveContainer(); UiObject2 header = fullWidgetsPicker.findObject(headerSelector); + outHeaderRect.set(0, 0, 0, header.getVisibleBounds().height()); mLauncher.assertTrue("Can't find a widget header", header != null); // Look for a header that has the test app name. @@ -179,11 +185,14 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { if (widgetsContainer != null) { return widgetsContainer; } - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, List.of(headerTitle), 0); - } else { - mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, fullWidgetsPicker.getChildren(), - 0); + } + mLauncher.scroll( + fullWidgetsPicker, + Direction.DOWN, + outHeaderRect, + /* steps= */ 10, + /* slowDown= */ true); } return null;