Skip to content

Commit

Permalink
[AppBarLayout] Use a uniform way to determine the target scrolling view
Browse files Browse the repository at this point in the history
  • Loading branch information
pubiqq committed May 6, 2024
1 parent 0d265b7 commit 04b2bd0
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 52 deletions.
2 changes: 1 addition & 1 deletion docs/components/TopAppBar.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ In the layout:
within another view (e.g., a `SwipeRefreshLayout`), you should make sure to set
`app:liftOnScrollTargetViewId` on your `AppBarLayout` to the id of the scrolling
view. This will ensure that the `AppBarLayout` is using the right view to
determine whether it should lift or not, and it will help avoid flicker issues.
determine whether it should lift or not.

The following example shows the top app bar disappearing upon scrolling up, and
appearing upon scrolling down.
Expand Down
142 changes: 91 additions & 51 deletions lib/java/com/google/android/material/appbar/AppBarLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,18 @@
import com.google.android.material.color.MaterialColors;
import com.google.android.material.drawable.DrawableUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.motion.MotionUtils;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.MaterialShapeUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

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

private boolean liftOnScroll;
@IdRes private int liftOnScrollTargetViewId;
@Nullable private WeakReference<View> liftOnScrollTargetView;
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
private final boolean hasLiftOnScrollColor;
@Nullable private ValueAnimator liftOnScrollColorAnimator;
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
Expand Down Expand Up @@ -760,7 +763,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();

clearLiftOnScrollTargetView();
clearLiftOnScrollTargetViewRef();
}

boolean hasChildWithInterpolator() {
Expand Down Expand Up @@ -1086,9 +1089,9 @@ public boolean isLiftOnScroll() {
public void setLiftOnScrollTargetView(@Nullable View liftOnScrollTargetView) {
this.liftOnScrollTargetViewId = View.NO_ID;
if (liftOnScrollTargetView == null) {
clearLiftOnScrollTargetView();
clearLiftOnScrollTargetViewRef();
} else {
this.liftOnScrollTargetView = new WeakReference<>(liftOnScrollTargetView);
this.liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
}
}

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

/**
Expand All @@ -1111,39 +1114,88 @@ public int getLiftOnScrollTargetViewId() {
return liftOnScrollTargetViewId;
}

boolean shouldLift(@Nullable View defaultScrollingView) {
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
if (scrollingView == null) {
scrollingView = defaultScrollingView;
}
boolean shouldBeLifted() {
final View scrollingView = findLiftOnScrollTargetView();
return scrollingView != null
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
}

@Nullable
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
private View findLiftOnScrollTargetView() {
View liftOnScrollTargetView = liftOnScrollTargetViewRef != null
? liftOnScrollTargetViewRef.get()
: null;

final ViewGroup parent = (ViewGroup) getParent();

if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
View targetView = null;
if (defaultScrollingView != null) {
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
liftOnScrollTargetView = parent.findViewById(liftOnScrollTargetViewId);
if (liftOnScrollTargetView != null) {
clearLiftOnScrollTargetViewRef();
liftOnScrollTargetViewRef = new WeakReference<>(liftOnScrollTargetView);
}
if (targetView == null && getParent() instanceof ViewGroup) {
// Assumes the scrolling view is a child of the AppBarLayout's parent,
// which should be true due to the CoordinatorLayout pattern.
targetView = ((ViewGroup) getParent()).findViewById(liftOnScrollTargetViewId);
}

return liftOnScrollTargetView != null
? liftOnScrollTargetView
: getDefaultLiftOnScrollTargetView(parent);
}

private View getDefaultLiftOnScrollTargetView(@NonNull ViewGroup parent) {
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
final View child = parent.getChildAt(i);
if (hasScrollingBehavior(child)) {
final View scrollableView = findClosestScrollableView(child);
if (scrollableView != null) {
return scrollableView;
}
}
if (targetView != null) {
liftOnScrollTargetView = new WeakReference<>(targetView);
}
return null;
}

private boolean hasScrollingBehavior(@NonNull View view) {
if (view.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) view.getLayoutParams();
return lp.getBehavior() instanceof ScrollingViewBehavior;
}

return false;
}

@Nullable
private View findClosestScrollableView(@NonNull View rootView) {
final Queue<View> queue = new ArrayDeque<>();
queue.add(rootView);

while (!queue.isEmpty()) {
final View view = queue.remove();
if (isScrollableView(view)) {
return view;
} else {
if (view instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
queue.add(viewGroup.getChildAt(i));
}
}
}
}
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;

return null;
}

private void clearLiftOnScrollTargetView() {
if (liftOnScrollTargetView != null) {
liftOnScrollTargetView.clear();
private boolean isScrollableView(@NonNull View view) {
return view instanceof NestedScrollingChild
|| view instanceof AbsListView
|| view instanceof ScrollView;
}

private void clearLiftOnScrollTargetViewRef() {
if (liftOnScrollTargetViewRef != null) {
liftOnScrollTargetViewRef.clear();
}
liftOnScrollTargetView = null;
liftOnScrollTargetViewRef = null;
}

/**
Expand Down Expand Up @@ -1560,12 +1612,12 @@ private boolean canScrollChildren(

@Override
public void onNestedPreScroll(
CoordinatorLayout coordinatorLayout,
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull T child,
View target,
@NonNull View target,
int dx,
int dy,
int[] consumed,
@NonNull int[] consumed,
int type) {
if (dy != 0) {
int min;
Expand All @@ -1584,7 +1636,7 @@ public void onNestedPreScroll(
}
}
if (child.isLiftOnScroll()) {
child.setLiftedState(child.shouldLift(target));
child.setLiftedState(child.shouldBeLifted());
}
}

Expand Down Expand Up @@ -1615,7 +1667,10 @@ public void onNestedScroll(

@Override
public void onStopNestedScroll(
CoordinatorLayout coordinatorLayout, @NonNull T abl, View target, int type) {
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull T abl,
@NonNull View target,
int type) {
// onStartNestedScroll for a fling will happen before onStopNestedScroll for the scroll. This
// isn't necessarily guaranteed yet, but it should be in the future. We use this to our
// advantage to check if a fling (ViewCompat.TYPE_NON_TOUCH) will start after the touch scroll
Expand All @@ -1624,7 +1679,7 @@ public void onStopNestedScroll(
// If we haven't been flung, or a fling is ending
snapToChildIfNeeded(coordinatorLayout, abl);
if (abl.isLiftOnScroll()) {
abl.setLiftedState(abl.shouldLift(target));
abl.setLiftedState(abl.shouldBeLifted());
}
}

Expand Down Expand Up @@ -2020,7 +2075,7 @@ void onFlingFinished(@NonNull CoordinatorLayout parent, @NonNull T layout) {
// At the end of a manual fling, check to see if we need to snap to the edge-child
snapToChildIfNeeded(parent, layout);
if (layout.isLiftOnScroll()) {
layout.setLiftedState(layout.shouldLift(findFirstScrollingChild(parent)));
layout.setLiftedState(layout.shouldBeLifted());
}
}

Expand Down Expand Up @@ -2187,9 +2242,7 @@ private void updateAppBarLayoutDrawableState(
}

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

final boolean changed = layout.setLiftedState(lifted);
Expand Down Expand Up @@ -2239,19 +2292,6 @@ private static View getAppBarChildOnOffset(
return null;
}

@Nullable
private View findFirstScrollingChild(@NonNull CoordinatorLayout parent) {
for (int i = 0, z = parent.getChildCount(); i < z; i++) {
final View child = parent.getChildAt(i);
if (child instanceof NestedScrollingChild
|| child instanceof AbsListView
|| child instanceof ScrollView) {
return child;
}
}
return null;
}

@Override
int getTopBottomOffsetForScrollingSibling() {
return getTopAndBottomOffset() + offsetDelta;
Expand Down Expand Up @@ -2388,7 +2428,7 @@ public boolean layoutDependsOn(CoordinatorLayout parent, View child, View depend
public boolean onDependentViewChanged(
@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
offsetChildAsNeeded(child, dependency);
updateLiftedStateIfNeeded(child, dependency);
updateLiftedStateIfNeeded(dependency);
return false;
}

Expand Down Expand Up @@ -2493,11 +2533,11 @@ int getScrollRange(View v) {
}
}

private void updateLiftedStateIfNeeded(View child, View dependency) {
private void updateLiftedStateIfNeeded(@NonNull View dependency) {
if (dependency instanceof AppBarLayout) {
AppBarLayout appBarLayout = (AppBarLayout) dependency;
if (appBarLayout.isLiftOnScroll()) {
appBarLayout.setLiftedState(appBarLayout.shouldLift(child));
appBarLayout.setLiftedState(appBarLayout.shouldBeLifted());
}
}
}
Expand Down

0 comments on commit 04b2bd0

Please sign in to comment.