Skip to content

Commit 71a498f

Browse files
committed
[AppBarLayout] Use a uniform way to determine the target scrolling view
1 parent 8ec6b77 commit 71a498f

2 files changed

Lines changed: 92 additions & 52 deletions

File tree

docs/components/TopAppBar.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ In the layout:
252252
within another view (e.g., a `SwipeRefreshLayout`), you should make sure to set
253253
`app:liftOnScrollTargetViewId` on your `AppBarLayout` to the id of the scrolling
254254
view. This will ensure that the `AppBarLayout` is using the right view to
255-
determine whether it should lift or not, and it will help avoid flicker issues.
255+
determine whether it should lift or not.
256256

257257
The following example shows the top app bar disappearing upon scrolling up, and
258258
appearing upon scrolling down.

lib/java/com/google/android/material/appbar/AppBarLayout.java

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import com.google.android.material.color.MaterialColors;
7575
import com.google.android.material.drawable.DrawableUtils;
7676
import com.google.android.material.internal.ThemeEnforcement;
77+
import com.google.android.material.internal.ViewUtils;
7778
import com.google.android.material.motion.MotionUtils;
7879
import com.google.android.material.resources.MaterialResources;
7980
import com.google.android.material.shape.MaterialShapeDrawable;
@@ -82,9 +83,11 @@
8283
import java.lang.annotation.Retention;
8384
import java.lang.annotation.RetentionPolicy;
8485
import java.lang.ref.WeakReference;
86+
import java.util.ArrayDeque;
8587
import java.util.ArrayList;
8688
import java.util.LinkedHashSet;
8789
import java.util.List;
90+
import java.util.Queue;
8891

8992
/**
9093
* AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of material
@@ -226,7 +229,7 @@ public abstract void onUpdate(
226229

227230
private boolean liftOnScroll;
228231
@IdRes private int liftOnScrollTargetViewId;
229-
@Nullable private WeakReference<View> liftOnScrollTargetView;
232+
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
230233
private final boolean hasLiftOnScrollColor;
231234
@Nullable private ValueAnimator liftOnScrollColorAnimator;
232235
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
@@ -823,7 +826,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
823826
protected void onDetachedFromWindow() {
824827
super.onDetachedFromWindow();
825828

826-
clearLiftOnScrollTargetView();
829+
clearLiftOnScrollTargetViewRef();
827830
}
828831

829832
boolean hasChildWithInterpolator() {
@@ -1148,9 +1151,9 @@ public boolean isLiftOnScroll() {
11481151
public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
11491152
this.liftOnScrollTargetViewId = View.NO_ID;
11501153
if (liftOnScrollTargetView == null) {
1151-
clearLiftOnScrollTargetView();
1154+
clearLiftOnScrollTargetViewRef();
11521155
} else {
1153-
this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView);
1156+
this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
11541157
}
11551158
}
11561159

@@ -1161,7 +1164,7 @@ public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
11611164
public void setLiftOnScrollTargetViewId(@IdRes int liftOnScrollTargetViewId) {
11621165
this.liftOnScrollTargetViewId = liftOnScrollTargetViewId;
11631166
// Invalidate cached target view so it will be looked up on next scroll.
1164-
clearLiftOnScrollTargetView();
1167+
clearLiftOnScrollTargetViewRef();
11651168
}
11661169

11671170
/**
@@ -1173,39 +1176,88 @@ public int getLiftOnScrollTargetViewId() {
11731176
return liftOnScrollTargetViewId;
11741177
}
11751178

1176-
boolean shouldLift(@Nullable View defaultScrollingView) {
1177-
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
1178-
if (scrollingView == null) {
1179-
scrollingView = defaultScrollingView;
1180-
}
1179+
boolean shouldBeLifted() {
1180+
final View scrollingView = findLiftOnScrollTargetView();
11811181
return scrollingView != null
11821182
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
11831183
}
11841184

11851185
@Nullable
1186-
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
1186+
private View findLiftOnScrollTargetView() {
1187+
View liftOnScrollTargetView = liftOnScrollTargetViewRef != null
1188+
? liftOnScrollTargetViewRef.get()
1189+
: null;
1190+
1191+
final ViewGroup parent = (ViewGroup) getParent();
1192+
11871193
if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
1188-
View targetView = null;
1189-
if (defaultScrollingView != null) {
1190-
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
1194+
liftOnScrollTargetView = parent.findViewById(liftOnScrollTargetViewId);
1195+
if (liftOnScrollTargetView != null) {
1196+
clearLiftOnScrollTargetViewRef();
1197+
liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
11911198
}
1192-
if (targetView == null && getParent() instanceof ViewGroup) {
1193-
// Assumes the scrolling view is a child of the AppBarLayout's parent,
1194-
// which should be true due to the CoordinatorLayout pattern.
1195-
targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId);
1199+
}
1200+
1201+
return liftOnScrollTargetView != null
1202+
? liftOnScrollTargetView
1203+
: getDefaultLiftOnScrollTargetView(parent);
1204+
}
1205+
1206+
private View getDefaultLiftOnScrollTargetView(@NonNull ViewGroup parent) {
1207+
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
1208+
final View child = parent.getChildAt(i);
1209+
if (hasScrollingBehavior(child)) {
1210+
final View scrollableView = findClosestScrollableView(child);
1211+
if (scrollableView != null) {
1212+
return scrollableView;
1213+
}
11961214
}
1197-
if (targetView != null) {
1198-
liftOnScrollTargetView = new WeakReference<>(targetView);
1215+
}
1216+
return null;
1217+
}
1218+
1219+
private boolean hasScrollingBehavior(@NonNull View view) {
1220+
if (view.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) {
1221+
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) view.getLayoutParams();
1222+
return lp.getBehavior() instanceof ScrollingViewBehavior;
1223+
}
1224+
1225+
return false;
1226+
}
1227+
1228+
@Nullable
1229+
private View findClosestScrollableView(@NonNull View rootView) {
1230+
final Queue<View> queue = new ArrayDeque<>();
1231+
queue.add(rootView);
1232+
1233+
while (!queue.isEmpty()) {
1234+
final View view = queue.remove();
1235+
if (isScrollableView(view)) {
1236+
return view;
1237+
} else {
1238+
if (view instanceof ViewGroup) {
1239+
final ViewGroup viewGroup = (ViewGroup) view;
1240+
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
1241+
queue.add(viewGroup.getChildAt(i));
1242+
}
1243+
}
11991244
}
12001245
}
1201-
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;
1246+
1247+
return null;
12021248
}
12031249

1204-
private void clearLiftOnScrollTargetView() {
1205-
if (liftOnScrollTargetView != null) {
1206-
liftOnScrollTargetView.clear();
1250+
private boolean isScrollableView(@NonNull View view) {
1251+
return view instanceof NestedScrollingChild
1252+
|| view instanceof AbsListView
1253+
|| view instanceof ScrollView;
1254+
}
1255+
1256+
private void clearLiftOnScrollTargetViewRef() {
1257+
if (liftOnScrollTargetViewRef != null) {
1258+
liftOnScrollTargetViewRef.clear();
12071259
}
1208-
liftOnScrollTargetView = null;
1260+
liftOnScrollTargetViewRef = null;
12091261
}
12101262

12111263
/**
@@ -1621,12 +1673,12 @@ private boolean canScrollChildren(
16211673

16221674
@Override
16231675
public void onNestedPreScroll(
1624-
CoordinatorLayout coordinatorLayout,
1676+
@NonNull CoordinatorLayout coordinatorLayout,
16251677
@NonNull T child,
1626-
View target,
1678+
@NonNull View target,
16271679
int dx,
16281680
int dy,
1629-
int[] consumed,
1681+
@NonNull int[] consumed,
16301682
int type) {
16311683
if (dy != 0) {
16321684
int min;
@@ -1645,7 +1697,7 @@ public void onNestedPreScroll(
16451697
}
16461698
}
16471699
if (child.isLiftOnScroll()) {
1648-
child.setLiftedState(child.shouldLift(target));
1700+
child.setLiftedState(child.shouldBeLifted());
16491701
}
16501702
}
16511703

@@ -1676,7 +1728,10 @@ public void onNestedScroll(
16761728

16771729
@Override
16781730
public void onStopNestedScroll(
1679-
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
1731+
@NonNull CoordinatorLayout coordinatorLayout,
1732+
@NonNull T abl,
1733+
@NonNull View target,
1734+
int type) {
16801735
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
16811736
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
16821737
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
@@ -1685,7 +1740,7 @@ public void onStopNestedScroll(
16851740
// If we haven't been flung, or a fling is ending
16861741
snapToChildIfNeeded(coordinatorLayout, abl);
16871742
if (abl.isLiftOnScroll()) {
1688-
abl.setLiftedState(abl.shouldLift(target));
1743+
abl.setLiftedState(abl.shouldBeLifted());
16891744
}
16901745
}
16911746

@@ -2080,7 +2135,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
20802135
// At the end of a manual fling, check to see if we need to snap to the edge-child
20812136
snapToChildIfNeeded(parent, layout);
20822137
if (layout.isLiftOnScroll()) {
2083-
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
2138+
layout.setLiftedState(layout.shouldBeLifted());
20842139
}
20852140
}
20862141

@@ -2247,9 +2302,7 @@ private void updateAppBarLayoutDrawableState(
22472302
}
22482303

22492304
if (layout.isLiftOnScroll()) {
2250-
// Use first scrolling child as default scrolling view for updating lifted state because
2251-
// it represents the content that would be scrolled beneath the app bar.
2252-
lifted = layout.shouldLift(findFirstScrollingChild(parent));
2305+
lifted = layout.shouldBeLifted();
22532306
}
22542307

22552308
final boolean changed = layout.setLiftedState(lifted);
@@ -2299,19 +2352,6 @@ private static View getAppBarChildOnOffset(
22992352
return null;
23002353
}
23012354

2302-
@Nullable
2303-
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
2304-
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
2305-
final View child = parent.getChildAt(i);
2306-
if (child instanceof NestedScrollingChild
2307-
|| child instanceof AbsListView
2308-
|| child instanceof ScrollView) {
2309-
return child;
2310-
}
2311-
}
2312-
return null;
2313-
}
2314-
23152355
@Override
23162356
int getTopBottomOffsetForScrollingSibling() {
23172357
return getTopAndBottomOffset() + offsetDelta;
@@ -2448,7 +2488,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
24482488
public boolean onDependentViewChanged(
24492489
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
24502490
offsetChildAsNeeded(child, dependency);
2451-
updateLiftedStateIfNeeded(child, dependency);
2491+
updateLiftedStateIfNeeded(dependency);
24522492
return false;
24532493
}
24542494

@@ -2553,11 +2593,11 @@ int getScrollRange(View v) {
25532593
}
25542594
}
25552595

2556-
private void updateLiftedStateIfNeeded(View child, View dependency) {
2596+
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
25572597
if (dependency instanceof AppBarLayout) {
25582598
AppBarLayout appBarLayout = (AppBarLayout) dependency;
25592599
if (appBarLayout.isLiftOnScroll()) {
2560-
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
2600+
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
25612601
}
25622602
}
25632603
}

0 commit comments

Comments
 (0)