From f9d336b9175f84c93df5edb59394f441aaaa7d5c Mon Sep 17 00:00:00 2001 From: pubiqq Date: Fri, 15 Dec 2023 20:15:12 +0300 Subject: [PATCH] [AppBarLayout] Use a uniform way to determine the target scrolling view --- docs/components/TopAppBar.md | 2 +- .../android/material/appbar/AppBarLayout.java | 141 +++++++++++------- 2 files changed, 91 insertions(+), 52 deletions(-) diff --git a/docs/components/TopAppBar.md b/docs/components/TopAppBar.md index f25da2523c4..c2fdc1aa3fb 100644 --- a/docs/components/TopAppBar.md +++ b/docs/components/TopAppBar.md @@ -645,7 +645,7 @@ In the layout: within another view (e.g., a `SwipeRefreshLayout`), you should make sure to set `app:liftOnScrollTargetViewId` on your `AppBarLayout` to the id of the scrolling view. This will ensure that the `AppBarLayout` is using the right view to -determine whether it should lift or not, and it will help avoid flicker issues. +determine whether it should lift or not. The following example shows the top app bar disappearing upon scrolling up, and appearing upon scrolling down. diff --git a/lib/java/com/google/android/material/appbar/AppBarLayout.java b/lib/java/com/google/android/material/appbar/AppBarLayout.java index f2a09cb2f7e..f5f63881d8f 100644 --- a/lib/java/com/google/android/material/appbar/AppBarLayout.java +++ b/lib/java/com/google/android/material/appbar/AppBarLayout.java @@ -82,9 +82,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Queue; /** * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of material @@ -227,7 +229,7 @@ public abstract void onUpdate( private boolean liftOnScroll; @Nullable private ColorStateList liftOnScrollColor; @IdRes private int liftOnScrollTargetViewId; - @Nullable private WeakReference liftOnScrollTargetView; + @Nullable private WeakReference liftOnScrollTargetViewRef; @Nullable private ValueAnimator liftOnScrollColorAnimator; @Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener; private final List liftOnScrollListeners = new ArrayList<>(); @@ -841,7 +843,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - clearLiftOnScrollTargetView(); + clearLiftOnScrollTargetViewRef(); } boolean hasChildWithInterpolator() { @@ -1171,9 +1173,9 @@ public boolean isLiftOnScroll() { public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) { this.liftOnScrollTargetViewId = View.NO_ID; if (liftOnScrollTargetView == null) { - clearLiftOnScrollTargetView(); + clearLiftOnScrollTargetViewRef(); } else { - this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView); + this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView); } } @@ -1184,7 +1186,7 @@ public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) { public void setLiftOnScrollTargetViewId(@IdRes int liftOnScrollTargetViewId) { this.liftOnScrollTargetViewId = liftOnScrollTargetViewId; // Invalidate cached target view so it will be looked up on next scroll. - clearLiftOnScrollTargetView(); + clearLiftOnScrollTargetViewRef(); } /** Sets the color of the {@link AppBarLayout} when it is fully lifted. */ @@ -1207,39 +1209,88 @@ public int getLiftOnScrollTargetViewId() { return liftOnScrollTargetViewId; } - boolean shouldLift(@Nullable View defaultScrollingView) { - View scrollingView = findLiftOnScrollTargetView(defaultScrollingView); - if (scrollingView == null) { - scrollingView = defaultScrollingView; - } + boolean shouldBeLifted() { + final View scrollingView = findLiftOnScrollTargetView(); return scrollingView != null && (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0); } @Nullable - private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) { + private View findLiftOnScrollTargetView() { + View liftOnScrollTargetView = liftOnScrollTargetViewRef != null + ? liftOnScrollTargetViewRef.get() + : null; + + final ViewGroup parent = (ViewGroup) getParent(); + if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) { - View targetView = null; - if (defaultScrollingView != null) { - targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId); + liftOnScrollTargetView = parent.findViewById(liftOnScrollTargetViewId); + if (liftOnScrollTargetView != null) { + clearLiftOnScrollTargetViewRef(); + liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView); } - if (targetView == null && getParent() instanceof ViewGroup) { - // Assumes the scrolling view is a child of the AppBarLayout's parent, - // which should be true due to the CoordinatorLayout pattern. - targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId); + } + + return liftOnScrollTargetView != null + ? liftOnScrollTargetView + : getDefaultLiftOnScrollTargetView(parent); + } + + private View getDefaultLiftOnScrollTargetView(@NonNull ViewGroup parent) { + for (int i = 0, z = parent.getChildCount(); i < z; i++) { + final View child = parent.getChildAt(i); + if (hasScrollingBehavior(child)) { + final View scrollableView = findClosestScrollableView(child); + if (scrollableView != null) { + return scrollableView; + } } - if (targetView != null) { - liftOnScrollTargetView = new WeakReference<>(targetView); + } + return null; + } + + private boolean hasScrollingBehavior(@NonNull View view) { + if (view.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) { + CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) view.getLayoutParams(); + return lp.getBehavior() instanceof ScrollingViewBehavior; + } + + return false; + } + + @Nullable + private View findClosestScrollableView(@NonNull View rootView) { + final Queue queue = new ArrayDeque<>(); + queue.add(rootView); + + while (!queue.isEmpty()) { + final View view = queue.remove(); + if (isScrollableView(view)) { + return view; + } else { + if (view instanceof ViewGroup) { + final ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { + queue.add(viewGroup.getChildAt(i)); + } + } } } - return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null; + + return null; } - private void clearLiftOnScrollTargetView() { - if (liftOnScrollTargetView != null) { - liftOnScrollTargetView.clear(); + private boolean isScrollableView(@NonNull View view) { + return view instanceof NestedScrollingChild + || view instanceof AbsListView + || view instanceof ScrollView; + } + + private void clearLiftOnScrollTargetViewRef() { + if (liftOnScrollTargetViewRef != null) { + liftOnScrollTargetViewRef.clear(); } - liftOnScrollTargetView = null; + liftOnScrollTargetViewRef = null; } /** @@ -1655,12 +1706,12 @@ private boolean canScrollChildren( @Override public void onNestedPreScroll( - CoordinatorLayout coordinatorLayout, + @NonNull CoordinatorLayout coordinatorLayout, @NonNull T child, - View target, + @NonNull View target, int dx, int dy, - int[] consumed, + @NonNull int[] consumed, int type) { if (dy != 0) { int min; @@ -1679,7 +1730,7 @@ public void onNestedPreScroll( } } if (child.isLiftOnScroll()) { - child.setLiftedState(child.shouldLift(target)); + child.setLiftedState(child.shouldBeLifted()); } } @@ -1710,7 +1761,10 @@ public void onNestedScroll( @Override public void onStopNestedScroll( - CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) { + @NonNull CoordinatorLayout coordinatorLayout, + @NonNull T abl, + @NonNull View target, + int type) { // onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This // isn't necessarily guaranteed yet, but it should be in the future. We use this to our // advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll @@ -1719,7 +1773,7 @@ public void onStopNestedScroll( // If we haven't been flung, or a fling is ending snapToChildIfNeeded(coordinatorLayout, abl); if (abl.isLiftOnScroll()) { - abl.setLiftedState(abl.shouldLift(target)); + abl.setLiftedState(abl.shouldBeLifted()); } } @@ -2114,7 +2168,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) { // At the end of a manual fling, check to see if we need to snap to the edge-child snapToChildIfNeeded(parent, layout); if (layout.isLiftOnScroll()) { - layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent))); + layout.setLiftedState(layout.shouldBeLifted()); } } @@ -2281,9 +2335,7 @@ private void updateAppBarLayoutDrawableState( } if (layout.isLiftOnScroll()) { - // Use first scrolling child as default scrolling view for updating lifted state because - // it represents the content that would be scrolled beneath the app bar. - lifted = layout.shouldLift(findFirstScrollingChild(parent)); + lifted = layout.shouldBeLifted(); } final boolean changed = layout.setLiftedState(lifted); @@ -2333,19 +2385,6 @@ private static View getAppBarChildOnOffset( return null; } - @Nullable - private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) { - for (int i = 0, z = parent.getChildCount(); i < z; i++) { - final View child = parent.getChildAt(i); - if (child instanceof NestedScrollingChild - || child instanceof AbsListView - || child instanceof ScrollView) { - return child; - } - } - return null; - } - @Override int getTopBottomOffsetForScrollingSibling() { return getTopAndBottomOffset() + offsetDelta; @@ -2482,7 +2521,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend public boolean onDependentViewChanged( @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { offsetChildAsNeeded(child, dependency); - updateLiftedStateIfNeeded(child, dependency); + updateLiftedStateIfNeeded(dependency); return false; } @@ -2587,11 +2626,11 @@ int getScrollRange(View v) { } } - private void updateLiftedStateIfNeeded(View child, View dependency) { + private void updateLiftedStateIfNeeded(@NonNull View dependency) { if (dependency instanceof AppBarLayout) { AppBarLayout appBarLayout = (AppBarLayout) dependency; if (appBarLayout.isLiftOnScroll()) { - appBarLayout.setLiftedState(appBarLayout.shouldLift(child)); + appBarLayout.setLiftedState(appBarLayout.shouldBeLifted()); } } }