Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/components/TopAppBar.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,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
141 changes: 90 additions & 51 deletions lib/java/com/google/android/material/appbar/AppBarLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@
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.LinkedHashSet;
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 @@ -227,7 +229,7 @@ public abstract void onUpdate(
private boolean liftOnScroll;
@Nullable private ColorStateList liftOnScrollColor;
@IdRes private int liftOnScrollTargetViewId;
@Nullable private WeakReference<View> liftOnScrollTargetView;
@Nullable private WeakReference<View> liftOnScrollTargetViewRef;
@Nullable private ValueAnimator liftOnScrollColorAnimator;
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
private final List<LiftOnScrollListener> liftOnScrollListeners = new ArrayList<>();
Expand Down Expand Up @@ -841,7 +843,7 @@ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();

clearLiftOnScrollTargetView();
clearLiftOnScrollTargetViewRef();
}

boolean hasChildWithInterpolator() {
Expand Down Expand Up @@ -1171,9 +1173,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 @@ -1184,7 +1186,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();
}

/** Sets the color of the {@link AppBarLayout} when it is fully lifted. */
Expand All @@ -1207,39 +1209,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 @@ -1655,12 +1706,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 @@ -1679,7 +1730,7 @@ public void onNestedPreScroll(
}
}
if (child.isLiftOnScroll()) {
child.setLiftedState(child.shouldLift(target));
child.setLiftedState(child.shouldBeLifted());
}
}

Expand Down Expand Up @@ -1710,7 +1761,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 @@ -1719,7 +1773,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 @@ -2114,7 +2168,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 @@ -2281,9 +2335,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 @@ -2333,19 +2385,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 @@ -2482,7 +2521,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 @@ -2587,11 +2626,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