Skip to content

Commit f399ea6

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

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;
@@ -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
/**
@@ -1563,12 +1615,12 @@ private boolean canScrollChildren(
15631615

15641616
@Override
15651617
public void onNestedPreScroll(
1566-
CoordinatorLayout coordinatorLayout,
1618+
@NonNull CoordinatorLayout coordinatorLayout,
15671619
@NonNull T child,
1568-
View target,
1620+
@NonNull View target,
15691621
int dx,
15701622
int dy,
1571-
int[] consumed,
1623+
@NonNull int[] consumed,
15721624
int type) {
15731625
if (dy != 0) {
15741626
int min;
@@ -1587,7 +1639,7 @@ public void onNestedPreScroll(
15871639
}
15881640
}
15891641
if (child.isLiftOnScroll()) {
1590-
child.setLiftedState(child.shouldLift(target));
1642+
child.setLiftedState(child.shouldBeLifted());
15911643
}
15921644
}
15931645

@@ -1618,7 +1670,10 @@ public void onNestedScroll(
16181670

16191671
@Override
16201672
public void onStopNestedScroll(
1621-
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
1673+
@NonNull CoordinatorLayout coordinatorLayout,
1674+
@NonNull T abl,
1675+
@NonNull View target,
1676+
int type) {
16221677
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
16231678
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
16241679
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
@@ -1627,7 +1682,7 @@ public void onStopNestedScroll(
16271682
// If we haven't been flung, or a fling is ending
16281683
snapToChildIfNeeded(coordinatorLayout, abl);
16291684
if (abl.isLiftOnScroll()) {
1630-
abl.setLiftedState(abl.shouldLift(target));
1685+
abl.setLiftedState(abl.shouldBeLifted());
16311686
}
16321687
}
16331688

@@ -2023,7 +2078,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
20232078
// At the end of a manual fling, check to see if we need to snap to the edge-child
20242079
snapToChildIfNeeded(parent, layout);
20252080
if (layout.isLiftOnScroll()) {
2026-
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
2081+
layout.setLiftedState(layout.shouldBeLifted());
20272082
}
20282083
}
20292084

@@ -2190,9 +2245,7 @@ private void updateAppBarLayoutDrawableState(
21902245
}
21912246

21922247
if (layout.isLiftOnScroll()) {
2193-
// Use first scrolling child as default scrolling view for updating lifted state because
2194-
// it represents the content that would be scrolled beneath the app bar.
2195-
lifted = layout.shouldLift(findFirstScrollingChild(parent));
2248+
lifted = layout.shouldBeLifted();
21962249
}
21972250

21982251
final boolean changed = layout.setLiftedState(lifted);
@@ -2242,19 +2295,6 @@ private static View getAppBarChildOnOffset(
22422295
return null;
22432296
}
22442297

2245-
@Nullable
2246-
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
2247-
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
2248-
final View child = parent.getChildAt(i);
2249-
if (child instanceof NestedScrollingChild
2250-
|| child instanceof AbsListView
2251-
|| child instanceof ScrollView) {
2252-
return child;
2253-
}
2254-
}
2255-
return null;
2256-
}
2257-
22582298
@Override
22592299
int getTopBottomOffsetForScrollingSibling() {
22602300
return getTopAndBottomOffset() + offsetDelta;
@@ -2391,7 +2431,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
23912431
public boolean onDependentViewChanged(
23922432
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
23932433
offsetChildAsNeeded(child, dependency);
2394-
updateLiftedStateIfNeeded(child, dependency);
2434+
updateLiftedStateIfNeeded(dependency);
23952435
return false;
23962436
}
23972437

@@ -2496,11 +2536,11 @@ int getScrollRange(View v) {
24962536
}
24972537
}
24982538

2499-
private void updateLiftedStateIfNeeded(View child, View dependency) {
2539+
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
25002540
if (dependency instanceof AppBarLayout) {
25012541
AppBarLayout appBarLayout = (AppBarLayout) dependency;
25022542
if (appBarLayout.isLiftOnScroll()) {
2503-
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
2543+
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
25042544
}
25052545
}
25062546
}

0 commit comments

Comments
 (0)