问题描述
Material Design中使用CoordinatorLayout和AppBarLayout可以让Toolbar和NestedScrollView或者RecyclerView联动滚动,设置不同的layout_behavior会产生不同的滚动效果。当layout_behavior设置为ScrollingViewBehavior,向上或者向下滚动NestedScrollView时,Toolbar的elevation和背景颜色会动态的变化。但在我们实际编码时有时会出现以下的问题,Toolbar的背景颜色随着滚动不停的闪烁。
原因
出现这种问题时,通常在布局时NestedScrollView不是直接作为CoordinatorLayout的child view,而是放在fragment中,同时frament加载在Framelayout上。
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="AppBarLayout Elevation"/>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/layout_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
NestedScrollView滚动过程中触发了AppBarLayout中的onLayoutChild()函数,接着会调用到updateAppBarLayoutDrawableState().
public boolean onLayoutChild(
@NonNull CoordinatorLayout parent, @NonNull T abl, int layoutDirection) {
...
// Update the AppBarLayout's drawable state for any elevation changes. This is needed so that
// the elevation is set in the first layout, so that we don't get a visual jump pre-N (due to
// the draw dispatch skip)
updateAppBarLayoutDrawableState(
parent, abl, getTopAndBottomOffset(), 0 /* direction */, true /* forceJump */);
// Make sure we dispatch the offset update
abl.onOffsetChanged(getTopAndBottomOffset());
updateAccessibilityActions(parent, abl);
return handled;
}
在updateAppBarLayoutDrawableState()方法中会调用setLiftedState()设置是否是lifted,从而使Toolbar的背景颜色动态变化。出现的问题的情况中正是因为lifted值计算有问题。
private void updateAppBarLayoutDrawableState(
@NonNull final CoordinatorLayout parent,
@NonNull final T layout,
final int offset,
final int direction,
final boolean forceJump) {
...
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));
}
final boolean changed = layout.setLiftedState(lifted);
...
}
现在来看看lifted值是如何计算得到的。shouldLift()方法需要传入一个默认的scrolling view。在findFirstScrollingChild()方法中,只是遍历查找一遍CoordinatorLayout的child view中符合条件的,由于我们的布局中NestedScrollView并不是CoordinatorLayout的child view, 因此这个findFirstScrollingChild()方法返回null。
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 ListView
|| child instanceof ScrollView) {
return child;
}
}
return null;
}
这里shouldLift()方法的入参为空,
boolean shouldLift(@Nullable View defaultScrollingView) {
View scrollingView = findLiftOnScrollTargetView(defaultScrollingView);
if (scrollingView == null) {
scrollingView = defaultScrollingView;
}
return scrollingView != null
&& (scrollingView.canScrollVertically(-1) || scrollingView.getScrollY() > 0);
}
private View findLiftOnScrollTargetView(@Nullable View defaultScrollingView) {
if (liftOnScrollTargetView == null && liftOnScrollTargetViewId != View.NO_ID) {
View targetView = null;
if (defaultScrollingView != null) {
targetView = defaultScrollingView.findViewById(liftOnScrollTargetViewId);
}
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);
}
if (targetView != null) {
liftOnScrollTargetView = new WeakReference<>(targetView);
}
}
return liftOnScrollTargetView != null ? liftOnScrollTargetView.get() : null;
}
接着看findLiftOnScrollTargetView()方法,入参defaultScrollingView是null,因为liftOnScrollTargetViewId这个参数没有设置,是默认的View.NO_ID,这里直接返回liftOnScrollTargetView引用的值,因为之前并没有赋值,所以这里也是返回null。现在回到shouldLift()函数中,scrollingView是null,计算得到的lifted就一直是false。而在正常情况下,NestedScrollView滚动时的lifted应是true,未滚动时才是false,这就导致了Toolbar的背景颜色闪烁。
解决方案
既然知道了原因,解决方法也就有了,一种是NestedScrollView直接作为CoordinatorLayout的child view,防止NestedScrollView嵌套的太深,但实际上往往布局复杂很难避免这种情况。
另外一种就是在代码中调用AppBarLayout的setLiftOnScrollTargetViewId()方法或是在xml文件中设置app:liftOnScrollTargetViewId属性来指定liftOnScrollTargetViewId,这样用来计算lifted状态的scroll view就不会出现为null的情况,直接计算所指定的scroll view的滚动状态能确保lifted值准确。