Skip to content

Commit 3b13e7c

Browse files
committed
[AppBarLayout] Use a uniform way to determine the target scrolling view
1 parent 9cf7bd3 commit 3b13e7c

File tree

2 files changed

+92
-52
lines changed

2 files changed

+92
-52
lines changed

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
@@ -75,15 +75,18 @@
7575
import com.google.android.material.color.MaterialColors;
7676
import com.google.android.material.drawable.DrawableUtils;
7777
import com.google.android.material.internal.ThemeEnforcement;
78+
import com.google.android.material.internal.ViewUtils;
7879
import com.google.android.material.motion.MotionUtils;
7980
import com.google.android.material.resources.MaterialResources;
8081
import com.google.android.material.shape.MaterialShapeDrawable;
8182
import com.google.android.material.shape.MaterialShapeUtils;
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.List;
89+
import java.util.Queue;
8790

8891
/**
8992
* AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of material
@@ -206,7 +209,7 @@ public interface LiftOnScrollListener {
206209

207210
private boolean liftOnScroll;
208211
@IdRes private int liftOnScrollTargetViewId;
209-
@Nullable private WeakReference<View> liftOnScrollTargetView;
212+
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
210213
private final boolean hasLiftOnScrollColor;
211214
@Nullable private ValueAnimator liftOnScrollColorAnimator;
212215
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
@@ -760,7 +763,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
760763
protected void onDetachedFromWindow() {
761764
super.onDetachedFromWindow();
762765

763-
clearLiftOnScrollTargetView();
766+
clearLiftOnScrollTargetViewRef();
764767
}
765768

766769
boolean hasChildWithInterpolator() {
@@ -1086,9 +1089,9 @@ public boolean isLiftOnScroll() {
10861089
public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
10871090
this.liftOnScrollTargetViewId = View.NO_ID;
10881091
if (liftOnScrollTargetView == null) {
1089-
clearLiftOnScrollTargetView();
1092+
clearLiftOnScrollTargetViewRef();
10901093
} else {
1091-
this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView);
1094+
this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
10921095
}
10931096
}
10941097

@@ -1099,7 +1102,7 @@ public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
10991102
public void setLiftOnScrollTargetViewId(@IdRes int liftOnScrollTargetViewId) {
11001103
this.liftOnScrollTargetViewId = liftOnScrollTargetViewId;
11011104
// Invalidate cached target view so it will be looked up on next scroll.
1102-
clearLiftOnScrollTargetView();
1105+
clearLiftOnScrollTargetViewRef();
11031106
}
11041107

11051108
/**
@@ -1111,39 +1114,88 @@ public int getLiftOnScrollTargetViewId() {
11111114
return liftOnScrollTargetViewId;
11121115
}
11131116

1114-
boolean shouldLift(@Nullable View defaultScrollingView) {
1115-
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
1116-
if (scrollingView == null) {
1117-
scrollingView = defaultScrollingView;
1118-
}
1117+
boolean shouldBeLifted() {
1118+
final View scrollingView = findLiftOnScrollTargetView();
11191119
return scrollingView != null
11201120
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
11211121
}
11221122

11231123
@Nullable
1124-
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
1124+
private View findLiftOnScrollTargetView() {
1125+
View liftOnScrollTargetView = liftOnScrollTargetViewRef != null
1126+
? liftOnScrollTargetViewRef.get()
1127+
: null;
1128+
1129+
final ViewGroup parent = (ViewGroup) getParent();
1130+
11251131
if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
1126-
View targetView = null;
1127-
if (defaultScrollingView != null) {
1128-
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
1132+
liftOnScrollTargetView = parent.findViewById(liftOnScrollTargetViewId);
1133+
if (liftOnScrollTargetView != null) {
1134+
clearLiftOnScrollTargetViewRef();
1135+
liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
11291136
}
1130-
if (targetView == null && getParent() instanceof ViewGroup) {
1131-
// Assumes the scrolling view is a child of the AppBarLayout's parent,
1132-
// which should be true due to the CoordinatorLayout pattern.
1133-
targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId);
1137+
}
1138+
1139+
return liftOnScrollTargetView != null
1140+
? liftOnScrollTargetView
1141+
: getDefaultLiftOnScrollTargetView(parent);
1142+
}
1143+
1144+
private View getDefaultLiftOnScrollTargetView(@NonNull ViewGroup parent) {
1145+
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
1146+
final View child = parent.getChildAt(i);
1147+
if (hasScrollingBehavior(child)) {
1148+
final View scrollableView = findClosestScrollableView(child);
1149+
if (scrollableView != null) {
1150+
return scrollableView;
1151+
}
11341152
}
1135-
if (targetView != null) {
1136-
liftOnScrollTargetView = new WeakReference<>(targetView);
1153+
}
1154+
return null;
1155+
}
1156+
1157+
private boolean hasScrollingBehavior(@NonNull View view) {
1158+
if (view.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) {
1159+
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) view.getLayoutParams();
1160+
return lp.getBehavior() instanceof ScrollingViewBehavior;
1161+
}
1162+
1163+
return false;
1164+
}
1165+
1166+
@Nullable
1167+
private View findClosestScrollableView(@NonNull View rootView) {
1168+
final Queue<View> queue = new ArrayDeque<>();
1169+
queue.add(rootView);
1170+
1171+
while (!queue.isEmpty()) {
1172+
final View view = queue.remove();
1173+
if (isScrollableView(view)) {
1174+
return view;
1175+
} else {
1176+
if (view instanceof ViewGroup) {
1177+
final ViewGroup viewGroup = (ViewGroup) view;
1178+
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
1179+
queue.add(viewGroup.getChildAt(i));
1180+
}
1181+
}
11371182
}
11381183
}
1139-
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;
1184+
1185+
return null;
11401186
}
11411187

1142-
private void clearLiftOnScrollTargetView() {
1143-
if (liftOnScrollTargetView != null) {
1144-
liftOnScrollTargetView.clear();
1188+
private boolean isScrollableView(@NonNull View view) {
1189+
return view instanceof NestedScrollingChild
1190+
|| view instanceof AbsListView
1191+
|| view instanceof ScrollView;
1192+
}
1193+
1194+
private void clearLiftOnScrollTargetViewRef() {
1195+
if (liftOnScrollTargetViewRef != null) {
1196+
liftOnScrollTargetViewRef.clear();
11451197
}
1146-
liftOnScrollTargetView = null;
1198+
liftOnScrollTargetViewRef = null;
11471199
}
11481200

11491201
/**
@@ -1560,12 +1612,12 @@ private boolean canScrollChildren(
15601612

15611613
@Override
15621614
public void onNestedPreScroll(
1563-
CoordinatorLayout coordinatorLayout,
1615+
@NonNull CoordinatorLayout coordinatorLayout,
15641616
@NonNull T child,
1565-
View target,
1617+
@NonNull View target,
15661618
int dx,
15671619
int dy,
1568-
int[] consumed,
1620+
@NonNull int[] consumed,
15691621
int type) {
15701622
if (dy != 0) {
15711623
int min;
@@ -1584,7 +1636,7 @@ public void onNestedPreScroll(
15841636
}
15851637
}
15861638
if (child.isLiftOnScroll()) {
1587-
child.setLiftedState(child.shouldLift(target));
1639+
child.setLiftedState(child.shouldBeLifted());
15881640
}
15891641
}
15901642

@@ -1615,7 +1667,10 @@ public void onNestedScroll(
16151667

16161668
@Override
16171669
public void onStopNestedScroll(
1618-
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
1670+
@NonNull CoordinatorLayout coordinatorLayout,
1671+
@NonNull T abl,
1672+
@NonNull View target,
1673+
int type) {
16191674
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
16201675
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
16211676
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
@@ -1624,7 +1679,7 @@ public void onStopNestedScroll(
16241679
// If we haven't been flung, or a fling is ending
16251680
snapToChildIfNeeded(coordinatorLayout, abl);
16261681
if (abl.isLiftOnScroll()) {
1627-
abl.setLiftedState(abl.shouldLift(target));
1682+
abl.setLiftedState(abl.shouldBeLifted());
16281683
}
16291684
}
16301685

@@ -2020,7 +2075,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
20202075
// At the end of a manual fling, check to see if we need to snap to the edge-child
20212076
snapToChildIfNeeded(parent, layout);
20222077
if (layout.isLiftOnScroll()) {
2023-
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
2078+
layout.setLiftedState(layout.shouldBeLifted());
20242079
}
20252080
}
20262081

@@ -2187,9 +2242,7 @@ private void updateAppBarLayoutDrawableState(
21872242
}
21882243

21892244
if (layout.isLiftOnScroll()) {
2190-
// Use first scrolling child as default scrolling view for updating lifted state because
2191-
// it represents the content that would be scrolled beneath the app bar.
2192-
lifted = layout.shouldLift(findFirstScrollingChild(parent));
2245+
lifted = layout.shouldBeLifted();
21932246
}
21942247

21952248
final boolean changed = layout.setLiftedState(lifted);
@@ -2239,19 +2292,6 @@ private static View getAppBarChildOnOffset(
22392292
return null;
22402293
}
22412294

2242-
@Nullable
2243-
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
2244-
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
2245-
final View child = parent.getChildAt(i);
2246-
if (child instanceof NestedScrollingChild
2247-
|| child instanceof AbsListView
2248-
|| child instanceof ScrollView) {
2249-
return child;
2250-
}
2251-
}
2252-
return null;
2253-
}
2254-
22552295
@Override
22562296
int getTopBottomOffsetForScrollingSibling() {
22572297
return getTopAndBottomOffset() + offsetDelta;
@@ -2388,7 +2428,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
23882428
public boolean onDependentViewChanged(
23892429
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
23902430
offsetChildAsNeeded(child, dependency);
2391-
updateLiftedStateIfNeeded(child, dependency);
2431+
updateLiftedStateIfNeeded(dependency);
23922432
return false;
23932433
}
23942434

@@ -2493,11 +2533,11 @@ int getScrollRange(View v) {
24932533
}
24942534
}
24952535

2496-
private void updateLiftedStateIfNeeded(View child, View dependency) {
2536+
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
24972537
if (dependency instanceof AppBarLayout) {
24982538
AppBarLayout appBarLayout = (AppBarLayout) dependency;
24992539
if (appBarLayout.isLiftOnScroll()) {
2500-
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
2540+
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
25012541
}
25022542
}
25032543
}

0 commit comments

Comments
 (0)