From fd9bfd3aabe8d1d249188b4e047a4389cdc282f1 Mon Sep 17 00:00:00 2001 From: Stevie Kideckel Date: Mon, 7 Jun 2021 15:46:21 +0000 Subject: [PATCH] Fix accounting for the position/offset of headers with collapsing views The offset should be taken before views are modified, the position should be taken after. This fixes an off by one issue when expanding a header below another already expanded header. Bug: 183378651 Test: verified locally Change-Id: I4987d57846d7bcde23b76280f800f19350b3521e --- .../widget/picker/WidgetsListAdapter.java | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index 826c244a92..150bd9902f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -202,11 +202,16 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator); } + /** Returns whether {@code entry} matches {@link #mWidgetsContentVisiblePackageUserKey}. */ private boolean isHeaderForVisibleContent(WidgetsListBaseEntry entry) { + return isHeaderForPackageUserKey(entry, mWidgetsContentVisiblePackageUserKey); + } + + /** Returns whether {@code entry} matches {@code key}. */ + private boolean isHeaderForPackageUserKey(WidgetsListBaseEntry entry, PackageUserKey key) { return (entry instanceof WidgetsListHeaderEntry || entry instanceof WidgetsListSearchHeaderEntry) - && new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user) - .equals(mWidgetsContentVisiblePackageUserKey); + && new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user).equals(key); } /** @@ -270,36 +275,68 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC @Override public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) { + // Ignore invalid clicks, such as collapsing a package that isn't currently expanded. + if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return; + if (showWidgets) { mWidgetsContentVisiblePackageUserKey = packageUserKey; - updateVisibleEntries(); - // Scroll the layout manager to the header position to keep it anchored to the same - // position. - scrollToPositionAndMaintainOffset(getSelectedHeaderPosition()); mLauncher.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED); - } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) { - OptionalInt previouslySelectedPosition = getSelectedHeaderPosition(); - + } else { mWidgetsContentVisiblePackageUserKey = null; - updateVisibleEntries(); - - // Scroll to the header that was just collapsed so it maintains its scroll offset. - scrollToPositionAndMaintainOffset(previouslySelectedPosition); } + + // Get the current top of the header with the matching key before adjusting the visible + // entries. + OptionalInt topForPackageUserKey = + getOffsetForPosition(getPositionForPackageUserKey(packageUserKey)); + + updateVisibleEntries(); + + // Get the position for the clicked header after adjusting the visible entries. The + // position may have changed if another header had previously been expanded. + OptionalInt positionForPackageUserKey = getPositionForPackageUserKey(packageUserKey); + scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey); } - private OptionalInt getSelectedHeaderPosition() { + /** + * Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not + * present. + */ + private OptionalInt getPositionForPackageUserKey(PackageUserKey key) { return IntStream.range(0, mVisibleEntries.size()) - .filter(index -> isHeaderForVisibleContent(mVisibleEntries.get(index))) + .filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key)) .findFirst(); } /** - * Scrolls to the selected header position. LinearLayoutManager scrolls the minimum distance - * necessary, so this will keep the selected header in place during clicks, without interrupting - * the animation. + * Returns the top of {@code positionOptional} in the recycler view, or empty if its view + * can't be found for any reason, including the position not being currently visible. The + * returned value does not include the top padding of the recycler view. */ - private void scrollToPositionAndMaintainOffset(OptionalInt positionOptional) { + private OptionalInt getOffsetForPosition(OptionalInt positionOptional) { + if (!positionOptional.isPresent() || mRecyclerView == null) return OptionalInt.empty(); + + RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (layoutManager == null) return OptionalInt.empty(); + + View view = layoutManager.findViewByPosition(positionOptional.getAsInt()); + if (view == null) return OptionalInt.empty(); + + return OptionalInt.of(layoutManager.getDecoratedTop(view)); + } + + /** + * Scrolls to the selected header position with the provided offset. LinearLayoutManager + * scrolls the minimum distance necessary, so this will keep the selected header in place during + * clicks, without interrupting the animation. + * + * @param positionOptional The position too scroll to. No scrolling will be done if empty. + * @param offsetOptional The offset from the top to maintain. If empty, then the list will + * scroll to the top of the position. + */ + private void scrollToPositionAndMaintainOffset( + OptionalInt positionOptional, + OptionalInt offsetOptional) { if (!positionOptional.isPresent() || mRecyclerView == null) return; int position = positionOptional.getAsInt(); @@ -317,12 +354,9 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC // Scroll to the header view's current offset, accounting for the recycler view's padding. // If the header view couldn't be found, then it will appear at the top of the list. - View headerView = layoutManager.findViewByPosition(position); - int targetHeaderViewTop = - headerView == null ? 0 : layoutManager.getDecoratedTop(headerView); layoutManager.scrollToPositionWithOffset( position, - targetHeaderViewTop - mRecyclerView.getPaddingTop()); + offsetOptional.orElse(0) - mRecyclerView.getPaddingTop()); } /**