Material Design使用(一) AppBarLayout中Toolbar背景颜色随着NestedScrollView滚动闪烁

983 阅读2分钟

问题描述

Material Design中使用CoordinatorLayoutAppBarLayout可以让ToolbarNestedScrollView或者RecyclerView联动滚动,设置不同的layout_behavior会产生不同的滚动效果。当layout_behavior设置为ScrollingViewBehavior,向上或者向下滚动NestedScrollView时,Toolbar的elevation和背景颜色会动态的变化。但在我们实际编码时有时会出现以下的问题,Toolbar的背景颜色随着滚动不停的闪烁。

ezgif-5-01aa5c6bde.gif

原因

出现这种问题时,通常在布局时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()方法,入参defaultScrollingViewnull,因为liftOnScrollTargetViewId这个参数没有设置,是默认的View.NO_ID,这里直接返回liftOnScrollTargetView引用的值,因为之前并没有赋值,所以这里也是返回null。现在回到shouldLift()函数中,scrollingViewnull,计算得到的lifted就一直是false。而在正常情况下,NestedScrollView滚动时的lifted应是true,未滚动时才是false,这就导致了Toolbar的背景颜色闪烁。

解决方案

既然知道了原因,解决方法也就有了,一种是NestedScrollView直接作为CoordinatorLayout的child view,防止NestedScrollView嵌套的太深,但实际上往往布局复杂很难避免这种情况。

另外一种就是在代码中调用AppBarLayoutsetLiftOnScrollTargetViewId()方法或是在xml文件中设置app:liftOnScrollTargetViewId属性来指定liftOnScrollTargetViewId,这样用来计算lifted状态的scroll view就不会出现为null的情况,直接计算所指定的scroll view的滚动状态能确保lifted值准确。

本文测试代码