Skip to content

Commit fba8a74

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

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
@@ -76,15 +76,18 @@
7676
import com.google.android.material.color.MaterialColors;
7777
import com.google.android.material.drawable.DrawableUtils;
7878
import com.google.android.material.internal.ThemeEnforcement;
79+
import com.google.android.material.internal.ViewUtils;
7980
import com.google.android.material.motion.MotionUtils;
8081
import com.google.android.material.resources.MaterialResources;
8182
import com.google.android.material.shape.MaterialShapeDrawable;
8283
import com.google.android.material.shape.MaterialShapeUtils;
8384
import java.lang.annotation.Retention;
8485
import java.lang.annotation.RetentionPolicy;
8586
import java.lang.ref.WeakReference;
87+
import java.util.ArrayDeque;
8688
import java.util.ArrayList;
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
@@ -207,7 +210,7 @@ public interface LiftOnScrollListener {
207210

208211
private boolean liftOnScroll;
209212
@IdRes private int liftOnScrollTargetViewId;
210-
@Nullable private WeakReference<View> liftOnScrollTargetView;
213+
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
211214
private final boolean hasLiftOnScrollColor;
212215
@Nullable private ValueAnimator liftOnScrollColorAnimator;
213216
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
@@ -761,7 +764,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
761764
protected void onDetachedFromWindow() {
762765
super.onDetachedFromWindow();
763766

764-
clearLiftOnScrollTargetView();
767+
clearLiftOnScrollTargetViewRef();
765768
}
766769

767770
boolean hasChildWithInterpolator() {
@@ -1087,9 +1090,9 @@ public boolean isLiftOnScroll() {
10871090
public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
10881091
this.liftOnScrollTargetViewId = View.NO_ID;
10891092
if (liftOnScrollTargetView == null) {
1090-
clearLiftOnScrollTargetView();
1093+
clearLiftOnScrollTargetViewRef();
10911094
} else {
1092-
this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView);
1095+
this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
10931096
}
10941097
}
10951098

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

11061109
/**
@@ -1112,39 +1115,88 @@ public int getLiftOnScrollTargetViewId() {
11121115
return liftOnScrollTargetViewId;
11131116
}
11141117

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

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

1143-
private void clearLiftOnScrollTargetView() {
1144-
if (liftOnScrollTargetView != null) {
1145-
liftOnScrollTargetView.clear();
1189+
private boolean isScrollableView(@NonNull View view) {
1190+
return view instanceof NestedScrollingChild
1191+
|| view instanceof AbsListView
1192+
|| view instanceof ScrollView;
1193+
}
1194+
1195+
private void clearLiftOnScrollTargetViewRef() {
1196+
if (liftOnScrollTargetViewRef != null) {
1197+
liftOnScrollTargetViewRef.clear();
11461198
}
1147-
liftOnScrollTargetView = null;
1199+
liftOnScrollTargetViewRef = null;
11481200
}
11491201

11501202
/**
@@ -1561,12 +1613,12 @@ private boolean canScrollChildren(
15611613

15621614
@Override
15631615
public void onNestedPreScroll(
1564-
CoordinatorLayout coordinatorLayout,
1616+
@NonNull CoordinatorLayout coordinatorLayout,
15651617
@NonNull T child,
1566-
View target,
1618+
@NonNull View target,
15671619
int dx,
15681620
int dy,
1569-
int[] consumed,
1621+
@NonNull int[] consumed,
15701622
int type) {
15711623
if (dy != 0) {
15721624
int min;
@@ -1585,7 +1637,7 @@ public void onNestedPreScroll(
15851637
}
15861638
}
15871639
if (child.isLiftOnScroll()) {
1588-
child.setLiftedState(child.shouldLift(target));
1640+
child.setLiftedState(child.shouldBeLifted());
15891641
}
15901642
}
15911643

@@ -1616,7 +1668,10 @@ public void onNestedScroll(
16161668

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

@@ -2034,7 +2089,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
20342089
// At the end of a manual fling, check to see if we need to snap to the edge-child
20352090
snapToChildIfNeeded(parent, layout);
20362091
if (layout.isLiftOnScroll()) {
2037-
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
2092+
layout.setLiftedState(layout.shouldBeLifted());
20382093
}
20392094
}
20402095

@@ -2201,9 +2256,7 @@ private void updateAppBarLayoutDrawableState(
22012256
}
22022257

22032258
if (layout.isLiftOnScroll()) {
2204-
// Use first scrolling child as default scrolling view for updating lifted state because
2205-
// it represents the content that would be scrolled beneath the app bar.
2206-
lifted = layout.shouldLift(findFirstScrollingChild(parent));
2259+
lifted = layout.shouldBeLifted();
22072260
}
22082261

22092262
final boolean changed = layout.setLiftedState(lifted);
@@ -2253,19 +2306,6 @@ private static View getAppBarChildOnOffset(
22532306
return null;
22542307
}
22552308

2256-
@Nullable
2257-
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
2258-
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
2259-
final View child = parent.getChildAt(i);
2260-
if (child instanceof NestedScrollingChild
2261-
|| child instanceof AbsListView
2262-
|| child instanceof ScrollView) {
2263-
return child;
2264-
}
2265-
}
2266-
return null;
2267-
}
2268-
22692309
@Override
22702310
int getTopBottomOffsetForScrollingSibling() {
22712311
return getTopAndBottomOffset() + offsetDelta;
@@ -2402,7 +2442,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
24022442
public boolean onDependentViewChanged(
24032443
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
24042444
offsetChildAsNeeded(child, dependency);
2405-
updateLiftedStateIfNeeded(child, dependency);
2445+
updateLiftedStateIfNeeded(dependency);
24062446
return false;
24072447
}
24082448

@@ -2509,11 +2549,11 @@ int getScrollRange(View v) {
25092549
}
25102550
}
25112551

2512-
private void updateLiftedStateIfNeeded(View child, View dependency) {
2552+
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
25132553
if (dependency instanceof AppBarLayout) {
25142554
AppBarLayout appBarLayout = (AppBarLayout) dependency;
25152555
if (appBarLayout.isLiftOnScroll()) {
2516-
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
2556+
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
25172557
}
25182558
}
25192559
}

0 commit comments

Comments
 (0)