Retain add button on rotation for two-pane and bottom sheet

Reselects the WidgetCell that was selected when reloading the sheet on
rotation. WidgetsFullSheet is excluded because it does not retain the
open header on rotation.

Bug: 331429554
Test: manual, see screencast
Flag: ACONFIG com.android.launcher3.enable_widget_tap_to_add NEXTFOOD
Change-Id: Id3d21f44b1dc525e144296f513f5a460fc51474c
This commit is contained in:
Willie Koomson
2024-05-03 19:57:36 +00:00
parent 49e364cd0b
commit 245c244fed
5 changed files with 125 additions and 8 deletions
+25
View File
@@ -70,6 +70,7 @@ import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.annotation.ChecksSdkIntAtLeast;
@@ -104,6 +105,7 @@ import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -835,4 +837,27 @@ public final class Utilities {
// No-Op
}
}
/**
* Does a depth-first search through the View hierarchy starting at root, to find a view that
* matches the predicate. Returns null if no View was found. View has a findViewByPredicate
* member function but it is currently a @hide API.
*/
@Nullable
public static <T extends View> T findViewByPredicate(@NonNull View root,
@NonNull Predicate<View> predicate) {
if (predicate.test(root)) {
return (T) root;
}
if (root instanceof ViewGroup parent) {
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View view = findViewByPredicate(parent.getChildAt(i), predicate);
if (view != null) {
return (T) view;
}
}
}
return null;
}
}
@@ -42,6 +42,7 @@ import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -74,6 +75,7 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
private boolean mDisableNavBarScrim = false;
@Nullable private WidgetCell mWidgetCellWithAddButton = null;
@Nullable private WidgetItem mLastSelectedWidgetItem = null;
public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@@ -161,6 +163,11 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
}
mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
if (mWidgetCellWithAddButton != null) {
mLastSelectedWidgetItem = mWidgetCellWithAddButton.getWidgetItem();
} else {
mLastSelectedWidgetItem = null;
}
} else {
mActivityContext.getItemOnClickListener().onClick(wc);
}
@@ -236,6 +243,14 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
return 0;
}
/**
* Returns the component of the widget that is currently showing an add button, if any.
*/
@Nullable
protected WidgetItem getLastSelectedWidgetItem() {
return mLastSelectedWidgetItem;
}
@Override
public boolean onLongClick(View v) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
@@ -503,6 +503,15 @@ public class WidgetCell extends LinearLayout {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && isShowingAddButton()) {
post(this::setupIconOrTextButton);
}
}
/**
* Loads a high resolution package icon to show next to the widget title.
*/
@@ -627,4 +636,19 @@ public class WidgetCell extends LinearLayout {
set.playSequentially(hideAnim, showAnim);
set.start();
}
/**
* Returns true if this WidgetCell is displaying the same item as info.
*/
public boolean matchesItem(WidgetItem info) {
if (info == null || mItem == null) return false;
if (info.widgetInfo != null && mItem.widgetInfo != null) {
return info.widgetInfo.getUser().equals(mItem.widgetInfo.getUser())
&& info.widgetInfo.getComponent().equals(mItem.widgetInfo.getComponent());
} else if (info.activityInfo != null && mItem.activityInfo != null) {
return info.activityInfo.getUser().equals(mItem.activityInfo.getUser())
&& info.activityInfo.getComponent().equals(mItem.activityInfo.getComponent());
}
return false;
}
}
@@ -142,6 +142,9 @@ public class WidgetsBottomSheet extends BaseWidgetSheet {
row.forEach(widgetItem -> {
WidgetCell widget = addItemCell(tableRow);
widget.applyFromCellItem(widgetItem);
if (widget.matchesItem(getLastSelectedWidgetItem())) {
widget.callOnClick();
}
});
widgetsTable.addView(tableRow);
});
@@ -29,6 +29,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -40,6 +41,7 @@ import androidx.annotation.Px;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.PackageUserKey;
@@ -194,15 +196,21 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
layoutParams.width = 0;
}
layoutParams.weight = layoutParams.width == 0 ? 0.33F : 0;
leftPane.setLayoutParams(layoutParams);
requestApplyInsets();
if (mSelectedHeader != null) {
if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) {
mSuggestedWidgetsHeader.callOnClick();
} else {
getHeaderChangeListener().onHeaderChanged(mSelectedHeader);
post(() -> {
// The following calls all trigger requestLayout, so we post them to avoid
// calling requestLayout during a layout pass. This also fixes the related warnings
// in logcat.
leftPane.setLayoutParams(layoutParams);
requestApplyInsets();
if (mSelectedHeader != null) {
if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) {
mSuggestedWidgetsHeader.callOnClick();
} else {
getHeaderChangeListener().onHeaderChanged(mSelectedHeader);
}
}
}
});
}
}
@@ -222,6 +230,9 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
if (mSuggestedWidgetsContainer == null && mRecommendedWidgetsCount > 0) {
setupSuggestedWidgets(LayoutInflater.from(getContext()));
mSuggestedWidgetsHeader.callOnClick();
} else if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) {
// Reselect widget if we are reloading recommendations while it is currently showing.
selectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem());
}
}
@@ -269,6 +280,16 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
mRightPaneScrollView.setScrollY(0);
mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
final boolean isChangingHeaders =
!mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey);
if (isChangingHeaders) {
// If switching from another header, unselect any WidgetCells. This is necessary
// because we do not clear/recycle the WidgetCells in the recommendations container
// when the header is clicked, only when onRecommendationsBound is called. That
// means a WidgetCell in the recommendations container may still be selected from
// the last time the recommendations were shown.
unselectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem());
}
mSelectedHeader = mSuggestedWidgetsPackageUserKey;
});
mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader);
@@ -357,6 +378,8 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
return new HeaderChangeListener() {
@Override
public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
final boolean isSameHeader = mSelectedHeader != null
&& mSelectedHeader.equals(selectedHeader);
mSelectedHeader = selectedHeader;
WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
.getSelectedAppWidgets(selectedHeader);
@@ -384,11 +407,20 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
contentEntryToBind,
ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
Collections.EMPTY_LIST);
if (isSameHeader) {
// Reselect the last selected widget if we are reloading the same header.
selectWidgetCell(widgetsRowViewHolder.tableContainer,
getLastSelectedWidgetItem());
}
widgetsRowViewHolder.mDataCallback = data -> {
mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
contentEntryToBind,
ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
Collections.singletonList(data));
if (isSameHeader) {
selectWidgetCell(widgetsRowViewHolder.tableContainer,
getLastSelectedWidgetItem());
}
};
mRightPane.removeAllViews();
mRightPane.addView(widgetsRowViewHolder.itemView);
@@ -401,6 +433,24 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
};
}
private static void selectWidgetCell(ViewGroup parent, WidgetItem item) {
if (parent == null || item == null) return;
WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc
&& wc.matchesItem(item));
if (cell != null && !cell.isShowingAddButton()) {
cell.callOnClick();
}
}
private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) {
if (parent == null || item == null) return;
WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc
&& wc.matchesItem(item));
if (cell != null && cell.isShowingAddButton()) {
cell.hideAddButton(/* animate= */ false);
}
}
@Override
public void setInsets(Rect insets) {
super.setInsets(insets);