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
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a056e81..9ed1b72 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -183,6 +183,7 @@
<dimen name="widget_cell_add_button_height">48dp</dimen>
<dimen name="widget_cell_add_button_start_padding">8dp</dimen>
<dimen name="widget_cell_add_button_end_padding">16dp</dimen>
+ <dimen name="widget_cell_add_button_scroll_padding">24dp</dimen>
<dimen name="widget_tabs_button_horizontal_padding">4dp</dimen>
<dimen name="widget_tabs_horizontal_padding">16dp</dimen>
diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java
index d6481a9..090251f 100644
--- a/src/com/android/launcher3/views/StickyHeaderLayout.java
+++ b/src/com/android/launcher3/views/StickyHeaderLayout.java
@@ -36,6 +36,9 @@
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 @@
return p instanceof MyLayoutParams;
}
+ /**
+ * Return a list of all the children that have the sticky layout param set.
+ */
+ public List<View> getStickyChildren() {
+ List<View> 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 c17ae09..aa797ab 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -27,6 +27,7 @@
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 @@
}
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 @@
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 f1b80e4..97aa67d 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -28,6 +28,7 @@
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 @@
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 c3067a5..c6cbb60 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.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.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.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 @@
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 14985bf..cb8b14e 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.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 @@
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.
*/