From dcc2d82d4ece05f9aca2a5a2912e45cf3e2af69c Mon Sep 17 00:00:00 2001 From: Willie Koomson Date: Thu, 4 Apr 2024 23:33:35 +0000 Subject: [PATCH] Scroll to show WidgetCell when it is tapped. Scroll to show WidgetCell when it is tapped in a widget sheet. Otherwise, the add button may show/hide without the user seeing it if the bottom is clipped. Bug: 329861721 Test: manual- tap WidgetCell when top or bottom is scrolled out of view Flag: ACONFIG com.android.launcher3.enable_widget_tap_to_add TEAMFOOD Change-Id: Ie21730c193e845cb1c1fa447b7c0a7e719984a8f --- res/values/dimens.xml | 1 + .../launcher3/views/StickyHeaderLayout.java | 19 +++++++ .../launcher3/widget/BaseWidgetSheet.java | 48 ++++++++++++++++ .../launcher3/widget/WidgetsBottomSheet.java | 13 +++++ .../widget/picker/WidgetsFullSheet.java | 57 +++++++++++++++++++ .../widget/picker/WidgetsTwoPaneSheet.java | 18 ++++++ 6 files changed, 156 insertions(+) diff --git a/res/values/dimens.xml b/res/values/dimens.xml index a056e81506..9ed1b7229b 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -183,6 +183,7 @@ 48dp 8dp 16dp + 24dp 4dp 16dp diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java index d6481a9b57..090251f257 100644 --- a/src/com/android/launcher3/views/StickyHeaderLayout.java +++ b/src/com/android/launcher3/views/StickyHeaderLayout.java @@ -36,6 +36,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.R; +import java.util.ArrayList; +import java.util.List; + /** * A {@link LinearLayout} container which allows scrolling parts of its content based on the * scroll of a different view. Views which are marked as sticky are not scrolled, giving the @@ -242,6 +245,22 @@ public class StickyHeaderLayout extends LinearLayout implements return p instanceof MyLayoutParams; } + /** + * Return a list of all the children that have the sticky layout param set. + */ + public List getStickyChildren() { + List stickyChildren = new ArrayList<>(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View v = getChildAt(i); + MyLayoutParams lp = (MyLayoutParams) v.getLayoutParams(); + if (lp.sticky) { + stickyChildren.add(v); + } + } + return stickyChildren; + } + private static class MyLayoutParams extends LayoutParams { public final boolean sticky; diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java index c17ae0910a..aa797ab8a0 100644 --- a/src/com/android/launcher3/widget/BaseWidgetSheet.java +++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java @@ -27,6 +27,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -141,6 +142,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView } if (enableWidgetTapToAdd()) { + scrollToWidgetCell(wc); if (mWidgetCellWithAddButton != null) { // If there is a add button currently showing, hide it. mWidgetCellWithAddButton.hideAddButton(/* animate= */ true); @@ -187,6 +189,52 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView handleClose(true); } + /** + * Scroll to show the widget cell. If both the bottom and top of the cell are clipped, this will + * prioritize showing the bottom of the cell (where the add button is). + */ + private void scrollToWidgetCell(@NonNull WidgetCell wc) { + final int headerTopClip = getHeaderTopClip(wc); + final Rect visibleRect = new Rect(); + final boolean isPartiallyVisible = wc.getLocalVisibleRect(visibleRect); + int scrollByY = 0; + if (isPartiallyVisible) { + final int scrollPadding = getResources() + .getDimensionPixelSize(R.dimen.widget_cell_add_button_scroll_padding); + final int topClip = visibleRect.top + headerTopClip; + final int bottomClip = wc.getHeight() - visibleRect.bottom; + if (bottomClip != 0) { + scrollByY = bottomClip + scrollPadding; + } else if (topClip != 0) { + scrollByY = -topClip - scrollPadding; + } + } + + if (isPartiallyVisible && scrollByY == 0) { + // Widget is fully visible. + return; + } else if (!isPartiallyVisible) { + Log.e("BaseWidgetSheet", "click on invisible WidgetCell should not be possible"); + return; + } + + scrollCellContainerByY(wc, scrollByY); + } + + /** + * Find the nearest scrollable container of the given WidgetCell, and scroll by the given + * amount. + */ + protected abstract void scrollCellContainerByY(WidgetCell wc, int scrollByY); + + + /** + * Return the top clip of any sticky headers over the given cell. + */ + protected int getHeaderTopClip(@NonNull WidgetCell cell) { + return 0; + } + @Override public boolean onLongClick(View v) { TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick"); diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index f1b80e41d1..97aa67de33 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -28,6 +28,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.animation.Interpolator; import android.widget.ScrollView; import android.widget.TableLayout; @@ -282,4 +283,16 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { float distanceToMove, Interpolator interpolator, PendingAnimation target) { target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator); } + + @Override + protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { + for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { + if (parent instanceof ScrollView scrollView) { + scrollView.smoothScrollBy(0, scrollByY); + return; + } else if (parent == this) { + return; + } + } + } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index c3067a5583..c6cbb6026d 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -38,6 +38,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.WindowInsets; import android.view.WindowInsetsController; import android.view.animation.AnimationUtils; @@ -46,6 +47,7 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.VisibleForTesting; @@ -69,6 +71,7 @@ import com.android.launcher3.views.SpringRelativeLayout; import com.android.launcher3.views.StickyHeaderLayout; import com.android.launcher3.views.WidgetsEduView; import com.android.launcher3.widget.BaseWidgetSheet; +import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.picker.search.SearchModeListener; import com.android.launcher3.widget.picker.search.WidgetsSearchBar; @@ -991,6 +994,60 @@ public class WidgetsFullSheet extends BaseWidgetSheet mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop(); } + @Override + protected int getHeaderTopClip(@NonNull WidgetCell cell) { + StickyHeaderLayout header = findViewById(R.id.search_and_recommendations_container); + if (header == null) { + return 0; + } + Rect cellRect = new Rect(); + boolean cellIsPartiallyVisible = cell.getGlobalVisibleRect(cellRect); + if (cellIsPartiallyVisible) { + Rect occludingRect = new Rect(); + for (View headerChild : header.getStickyChildren()) { + Rect childRect = new Rect(); + boolean childVisible = headerChild.getGlobalVisibleRect(childRect); + if (childVisible && childRect.intersect(cellRect)) { + occludingRect.union(childRect); + } + } + if (!occludingRect.isEmpty() && cellRect.top < occludingRect.bottom) { + return occludingRect.bottom - cellRect.top; + } + } + return 0; + } + + @Override + protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { + for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { + if (parent instanceof WidgetsRecyclerView recyclerView) { + // Scrollable container for main widget list. + recyclerView.smoothScrollBy(0, scrollByY); + return; + } else if (parent instanceof StickyHeaderLayout header) { + // Scrollable container for recommendations. We still scroll on the recycler (even + // though the recommendations are not in the recycler view) because the + // StickyHeaderLayout scroll is connected to the currently visible recycler view. + WidgetsRecyclerView recyclerView = findVisibleRecyclerView(); + if (recyclerView != null) { + recyclerView.smoothScrollBy(0, scrollByY); + } + return; + } else if (parent == this) { + return; + } + } + } + + @Nullable + private WidgetsRecyclerView findVisibleRecyclerView() { + if (mViewPager != null) { + return (WidgetsRecyclerView) mViewPager.getPageAt(mViewPager.getCurrentPage()); + } + return findViewById(R.id.primary_widgets_list_view); + } + /** A holder class for holding adapters & their corresponding recycler view. */ final class AdapterHolder { static final int PRIMARY = 0; diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 14985bf148..cb8b14ea75 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -44,6 +44,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.recyclerview.ViewHolderBinder; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; @@ -423,6 +424,23 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { return true; } + @Override + protected int getHeaderTopClip(@NonNull WidgetCell cell) { + return 0; + } + + @Override + protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) { + for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) { + if (parent instanceof ScrollView scrollView) { + scrollView.smoothScrollBy(0, scrollByY); + return; + } else if (parent == this) { + return; + } + } + } + /** * This is a listener for when the selected header gets changed in the left pane. */