Skip to content

Commit afc5663

Browse files
committed
[AppBarLayout] Use a uniform way to determine the target scrolling view
1 parent c2051db commit afc5663

File tree

2 files changed

+91
-52
lines changed

2 files changed

+91
-52
lines changed

docs/components/TopAppBar.md

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

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

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

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@
8282
import java.lang.annotation.Retention;
8383
import java.lang.annotation.RetentionPolicy;
8484
import java.lang.ref.WeakReference;
85+
import java.util.ArrayDeque;
8586
import java.util.ArrayList;
8687
import java.util.LinkedHashSet;
8788
import java.util.List;
89+
import java.util.Queue;
8890

8991
/**
9092
* AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of material
@@ -227,7 +229,7 @@ public abstract void onUpdate(
227229
private boolean liftOnScroll;
228230
@Nullable private ColorStateList liftOnScrollColor;
229231
@IdRes private int liftOnScrollTargetViewId;
230-
@Nullable private WeakReference<View> liftOnScrollTargetView;
232+
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
231233
@Nullable private ValueAnimator liftOnScrollColorAnimator;
232234
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
233235
private final List<LiftOnScrollListener> liftOnScrollListeners = new ArrayList<>();
@@ -841,7 +843,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
841843
protected void onDetachedFromWindow() {
842844
super.onDetachedFromWindow();
843845

844-
clearLiftOnScrollTargetView();
846+
clearLiftOnScrollTargetViewRef();
845847
}
846848

847849
boolean hasChildWithInterpolator() {
@@ -1171,9 +1173,9 @@ public boolean isLiftOnScroll() {
11711173
public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
11721174
this.liftOnScrollTargetViewId = View.NO_ID;
11731175
if (liftOnScrollTargetView == null) {
1174-
clearLiftOnScrollTargetView();
1176+
clearLiftOnScrollTargetViewRef();
11751177
} else {
1176-
this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView);
1178+
this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
11771179
}
11781180
}
11791181

@@ -1184,7 +1186,7 @@ public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
11841186
public void setLiftOnScrollTargetViewId(@IdRes int liftOnScrollTargetViewId) {
11851187
this.liftOnScrollTargetViewId = liftOnScrollTargetViewId;
11861188
// Invalidate cached target view so it will be looked up on next scroll.
1187-
clearLiftOnScrollTargetView();
1189+
clearLiftOnScrollTargetViewRef();
11881190
}
11891191

11901192
/** Sets the color of the {@link AppBarLayout} when it is fully lifted. */
@@ -1207,39 +1209,88 @@ public int getLiftOnScrollTargetViewId() {
12071209
return liftOnScrollTargetViewId;
12081210
}
12091211

1210-
boolean shouldLift(@Nullable View defaultScrollingView) {
1211-
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
1212-
if (scrollingView == null) {
1213-
scrollingView = defaultScrollingView;
1214-
}
1212+
boolean shouldBeLifted() {
1213+
final View scrollingView = findLiftOnScrollTargetView();
12151214
return scrollingView != null
12161215
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
12171216
}
12181217

12191218
@Nullable
1220-
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
1219+
private View findLiftOnScrollTargetView() {
1220+
View liftOnScrollTargetView = liftOnScrollTargetViewRef != null
1221+
? liftOnScrollTargetViewRef.get()
1222+
: null;
1223+
1224+
final ViewGroup parent = (ViewGroup) getParent();
1225+
12211226
if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
1222-
View targetView = null;
1223-
if (defaultScrollingView != null) {
1224-
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
1227+
liftOnScrollTargetView = parent.findViewById(liftOnScrollTargetViewId);
1228+
if (liftOnScrollTargetView != null) {
1229+
clearLiftOnScrollTargetViewRef();
1230+
liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
12251231
}
1226-
if (targetView == null && getParent() instanceof ViewGroup) {
1227-
// Assumes the scrolling view is a child of the AppBarLayout's parent,
1228-
// which should be true due to the CoordinatorLayout pattern.
1229-
targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId);
1232+
}
1233+
1234+
return liftOnScrollTargetView != null
1235+
? liftOnScrollTargetView
1236+
: getDefaultLiftOnScrollTargetView(parent);
1237+
}
1238+
1239+
private View getDefaultLiftOnScrollTargetView(@NonNull ViewGroup parent) {
1240+
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
1241+
final View child = parent.getChildAt(i);
1242+
if (hasScrollingBehavior(child)) {
1243+
final View scrollableView = findClosestScrollableView(child);
1244+
if (scrollableView != null) {
1245+
return scrollableView;
1246+
}
12301247
}
1231-
if (targetView != null) {
1232-
liftOnScrollTargetView = new WeakReference<>(targetView);
1248+
}
1249+
return null;
1250+
}
1251+
1252+
private boolean hasScrollingBehavior(@NonNull View view) {
1253+
if (view.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) {
1254+
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) view.getLayoutParams();
1255+
return lp.getBehavior() instanceof ScrollingViewBehavior;
1256+
}
1257+
1258+
return false;
1259+
}
1260+
1261+
@Nullable
1262+
private View findClosestScrollableView(@NonNull View rootView) {
1263+
final Queue<View> queue = new ArrayDeque<>();
1264+
queue.add(rootView);
1265+
1266+
while (!queue.isEmpty()) {
1267+
final View view = queue.remove();
1268+
if (isScrollableView(view)) {
1269+
return view;
1270+
} else {
1271+
if (view instanceof ViewGroup) {
1272+
final ViewGroup viewGroup = (ViewGroup) view;
1273+
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
1274+
queue.add(viewGroup.getChildAt(i));
1275+
}
1276+
}
12331277
}
12341278
}
1235-
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;
1279+
1280+
return null;
12361281
}
12371282

1238-
private void clearLiftOnScrollTargetView() {
1239-
if (liftOnScrollTargetView != null) {
1240-
liftOnScrollTargetView.clear();
1283+
private boolean isScrollableView(@NonNull View view) {
1284+
return view instanceof NestedScrollingChild
1285+
|| view instanceof AbsListView
1286+
|| view instanceof ScrollView;
1287+
}
1288+
1289+
private void clearLiftOnScrollTargetViewRef() {
1290+
if (liftOnScrollTargetViewRef != null) {
1291+
liftOnScrollTargetViewRef.clear();
12411292
}
1242-
liftOnScrollTargetView = null;
1293+
liftOnScrollTargetViewRef = null;
12431294
}
12441295

12451296
/**
@@ -1655,12 +1706,12 @@ private boolean canScrollChildren(
16551706

16561707
@Override
16571708
public void onNestedPreScroll(
1658-
CoordinatorLayout coordinatorLayout,
1709+
@NonNull CoordinatorLayout coordinatorLayout,
16591710
@NonNull T child,
1660-
View target,
1711+
@NonNull View target,
16611712
int dx,
16621713
int dy,
1663-
int[] consumed,
1714+
@NonNull int[] consumed,
16641715
int type) {
16651716
if (dy != 0) {
16661717
int min;
@@ -1679,7 +1730,7 @@ public void onNestedPreScroll(
16791730
}
16801731
}
16811732
if (child.isLiftOnScroll()) {
1682-
child.setLiftedState(child.shouldLift(target));
1733+
child.setLiftedState(child.shouldBeLifted());
16831734
}
16841735
}
16851736

@@ -1710,7 +1761,10 @@ public void onNestedScroll(
17101761

17111762
@Override
17121763
public void onStopNestedScroll(
1713-
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
1764+
@NonNull CoordinatorLayout coordinatorLayout,
1765+
@NonNull T abl,
1766+
@NonNull View target,
1767+
int type) {
17141768
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
17151769
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
17161770
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
@@ -1719,7 +1773,7 @@ public void onStopNestedScroll(
17191773
// If we haven't been flung, or a fling is ending
17201774
snapToChildIfNeeded(coordinatorLayout, abl);
17211775
if (abl.isLiftOnScroll()) {
1722-
abl.setLiftedState(abl.shouldLift(target));
1776+
abl.setLiftedState(abl.shouldBeLifted());
17231777
}
17241778
}
17251779

@@ -2114,7 +2168,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
21142168
// At the end of a manual fling, check to see if we need to snap to the edge-child
21152169
snapToChildIfNeeded(parent, layout);
21162170
if (layout.isLiftOnScroll()) {
2117-
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
2171+
layout.setLiftedState(layout.shouldBeLifted());
21182172
}
21192173
}
21202174

@@ -2281,9 +2335,7 @@ private void updateAppBarLayoutDrawableState(
22812335
}
22822336

22832337
if (layout.isLiftOnScroll()) {
2284-
// Use first scrolling child as default scrolling view for updating lifted state because
2285-
// it represents the content that would be scrolled beneath the app bar.
2286-
lifted = layout.shouldLift(findFirstScrollingChild(parent));
2338+
lifted = layout.shouldBeLifted();
22872339
}
22882340

22892341
final boolean changed = layout.setLiftedState(lifted);
@@ -2333,19 +2385,6 @@ private static View getAppBarChildOnOffset(
23332385
return null;
23342386
}
23352387

2336-
@Nullable
2337-
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
2338-
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
2339-
final View child = parent.getChildAt(i);
2340-
if (child instanceof NestedScrollingChild
2341-
|| child instanceof AbsListView
2342-
|| child instanceof ScrollView) {
2343-
return child;
2344-
}
2345-
}
2346-
return null;
2347-
}
2348-
23492388
@Override
23502389
int getTopBottomOffsetForScrollingSibling() {
23512390
return getTopAndBottomOffset() + offsetDelta;
@@ -2482,7 +2521,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
24822521
public boolean onDependentViewChanged(
24832522
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
24842523
offsetChildAsNeeded(child, dependency);
2485-
updateLiftedStateIfNeeded(child, dependency);
2524+
updateLiftedStateIfNeeded(dependency);
24862525
return false;
24872526
}
24882527

@@ -2587,11 +2626,11 @@ int getScrollRange(View v) {
25872626
}
25882627
}
25892628

2590-
private void updateLiftedStateIfNeeded(View child, View dependency) {
2629+
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
25912630
if (dependency instanceof AppBarLayout) {
25922631
AppBarLayout appBarLayout = (AppBarLayout) dependency;
25932632
if (appBarLayout.isLiftOnScroll()) {
2594-
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
2633+
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
25952634
}
25962635
}
25972636
}

0 commit comments

Comments
 (0)