如何避免 CollapsingToolbarLayout 在滚动时不被捕捉或 "wobbly"?

How to avoid CollapsingToolbarLayout not being snapped or being "wobbly" when scrolling?

背景

假设您创建的应用程序与您可以通过 "scrolling activity" 的向导创建的应用程序具有相似的 UI,但您希望滚动标志具有捕捉功能,因为这样:

<android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >

问题

事实证明,在很多情况下它都存在捕捉问题。有时 UI 不会对齐到 top/bottom,使得 CollapsingToolbarLayout 停留在两者之间。

有时它也会尝试捕捉到一个方向,然后决定捕捉到另一个方向。

您可以在附加视频中看到这两个问题 here

我试过的

我认为这是我在 RecyclerView 中使用 setNestedScrollingEnabled(false) 时遇到的问题之一,所以我询问了它 ,但后来我注意到即使有解决方案也没有解决方案完全使用此命令,甚至在使用简单的 NestedScrollView(由向导创建)时,我仍然可以注意到此行为。

这就是为什么我决定将此作为一个问题进行报告,here

遗憾的是,我在 Whosebug 上找不到任何解决这些奇怪错误的方法。

问题

它为什么会发生,更重要的是:我如何才能避免这些问题,同时仍然使用它应该具有的行为?


编辑:这是已接受答案的一个很好的改进 Kotlin 版本:

class RecyclerViewEx @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
    private var mAppBarTracking: AppBarTracking? = null
    private var mView: View? = null
    private var mTopPos: Int = 0
    private var mLayoutManager: LinearLayoutManager? = null

    interface AppBarTracking {
        fun isAppBarIdle(): Boolean
        fun isAppBarExpanded(): Boolean
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
        if (mAppBarTracking == null)
            return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
                && isNestedScrollingEnabled) {
            if (dy > 0) {
                if (mAppBarTracking!!.isAppBarExpanded()) {
                    consumed!![1] = dy
                    return true
                }
            } else {
                mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
                if (mTopPos == 0) {
                    mView = mLayoutManager!!.findViewByPosition(mTopPos)
                    if (-mView!!.top + dy <= 0) {
                        consumed!![1] = dy - mView!!.top
                        return true
                    }
                }
            }
        }
        if (dy < 0 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
            consumed!![1] = dy
            return true
        }

        val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
        if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
            offsetInWindow[1] = 0
        return returnValue
    }

    override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
        super.setLayoutManager(layout)
        mLayoutManager = layoutManager as LinearLayoutManager
    }

    fun setAppBarTracking(appBarTracking: AppBarTracking) {
        mAppBarTracking = appBarTracking
    }

    fun setAppBarTracking(appBarLayout: AppBarLayout) {
        val appBarIdle = AtomicBoolean(true)
        val appBarExpanded = AtomicBoolean()
        appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
            private var mAppBarOffset = Integer.MIN_VALUE

            override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
                if (mAppBarOffset == verticalOffset)
                    return
                mAppBarOffset = verticalOffset
                appBarExpanded.set(verticalOffset == 0)
                appBarIdle.set(mAppBarOffset >= 0 || mAppBarOffset <= -appBarLayout.totalScrollRange)
            }
        })
        setAppBarTracking(object : AppBarTracking {
            override fun isAppBarIdle(): Boolean = appBarIdle.get()
            override fun isAppBarExpanded(): Boolean = appBarExpanded.get()
        })
    }

    override fun fling(velocityX: Int, inputVelocityY: Int): Boolean {
        var velocityY = inputVelocityY
        if (mAppBarTracking != null && !mAppBarTracking!!.isAppBarIdle()) {
            val vc = ViewConfiguration.get(context)
            velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
            else vc.scaledMinimumFlingVelocity
        }

        return super.fling(velocityX, velocityY)
    }
}

编辑 代码已更新,使其更符合已接受答案的代码。这个答案涉及 NestedScrollView 而接受的答案是关于 RecyclerView.


这是 API 26.0.0-beta2 版本中引入的问题。它不会发生在 beta 1 版本或 API 25 上。正如您所指出的,它也会发生在 API 26.0.0 上。通常,问题似乎与 beta2 中处理 flings 和嵌套滚动的方式有关。对嵌套滚动进行了重大重写(参见 "Carry on Scrolling"),因此出现此类问题也就不足为奇了。

我的想法是 NestedScrollView 中的某个地方没有正确处理多余的滚动条。解决方法是在展开或折叠 AppBar 时安静地消耗某些 "non-touch" 滚动条 (type == ViewCompat.TYPE_NON_TOUCH) 的滚动条。这会停止弹跳,允许快照,并且通常会使 AppBar 表现得更好。

ScrollingActivity 已修改为跟踪 AppBar 的状态以报告其是否展开。一个新的class调用"MyNestedScrollView"覆盖dispatchNestedPreScroll()(新的,见here)来操纵多余滚动的消耗。

下面的代码应该足以阻止 AppBarLayout 摇晃和拒绝捕捉。 (XML 也必须更改以适应 MyNestedSrollView。以下仅适用于支持 lib 26.0.0-beta2 及更高版本。)

AppBarTracking.java

public interface AppBarTracking {
    boolean isAppBarIdle();
    boolean isAppBarExpanded();
}

ScrollingActivity.java

public class ScrollingActivity extends AppCompatActivity implements AppBarTracking {

    private int mAppBarOffset;
    private int mAppBarMaxOffset;
    private MyNestedScrollView mNestedView;
    private boolean mAppBarIdle = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AppBarLayout appBar;

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        final Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        appBar = findViewById(R.id.app_bar);
        mNestedView = findViewById(R.id.nestedScrollView);
        mNestedView.setAppBarTracking(this);
        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
            }
        });

        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
                // mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
                // mAppBarOffset = mAppBarMaxOffset
                // mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
                // mAppBarOffset should never be > zero or less than mAppBarMaxOffset
                mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
            }
        });

        mNestedView.post(new Runnable() {
            @Override
            public void run() {
                mAppBarMaxOffset = mNestedView.getMaxScrollAmount();
            }
        });
    }

    @Override
    public boolean isAppBarIdle() {
        return mAppBarIdle;
    }

    @Override
    public boolean isAppBarExpanded() {
        return mAppBarOffset == 0;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_scrolling, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("unused")
    private static final String TAG = "ScrollingActivity";
}

MyNestedScrollView.java

public class MyNestedScrollView extends NestedScrollView {

    public MyNestedScrollView(Context context) {
        this(context, null);
    }

    public MyNestedScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View view, int x, int y, int oldx, int oldy) {
                mScrollPosition = y;
            }
        });
    }

    private AppBarTracking mAppBarTracking;
    private int mScrollPosition;

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {

        // App bar latching trouble is only with this type of movement when app bar is expanded
        // or collapsed. In touch mode, everything is OK regardless of the open/closed status
        // of the app bar.
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
                && isNestedScrollingEnabled()) {
            // Make sure the AppBar stays expanded when it should.
            if (dy > 0) { // swiped up
                if (mAppBarTracking.isAppBarExpanded()) {
                    // Appbar can only leave its expanded state under the power of touch...
                    consumed[1] = dy;
                    return true;
                }
            } else { // swiped down (or no change)
                // Make sure the AppBar stays collapsed when it should.
                if (mScrollPosition + dy < 0) {
                    // Scroll until scroll position = 0 and AppBar is still collapsed.
                    consumed[1] = dy + mScrollPosition;
                    return true;
                }
            }
        }

        boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        // Fix the scrolling problems when scrolling is disabled. This issue existed prior
        // to 26.0.0-beta2. (Not sure that this is a problem for 26.0.0-beta2 and later.)
        if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
            Log.d(TAG, "<<<<offsetInWindow[1] forced to zero");
            offsetInWindow[1] = 0;
        }
        return returnValue;
    }

    public void setAppBarTracking(AppBarTracking appBarTracking) {
        mAppBarTracking = appBarTracking;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "MyNestedScrollView";
}

更新 我稍微更改了代码以解决剩余的问题 - 至少是我可以重现的问题。关键更新是仅在展开或折叠 AppBar 时处理 dy。在第一次迭代中,dispatchNestedPreScroll() 处理滚动时没有检查 AppBar 的状态是否处于折叠状态。

其他变化较小,属于清理范畴。代码块更新如下。


这个答案解决了关于 RecyclerView 的问题。我给出的另一个答案仍然适用于此。 RecyclerView 与支持库的 26.0.0-beta2 中引入的 NestedScrollView 存在相同的问题。

下面的代码基于 相关问题,但包含对 AppBar 不稳定行为的修复。我删除了修复奇怪滚动的代码,因为它似乎不再需要了。

AppBarTracking.java

public interface AppBarTracking {
    boolean isAppBarIdle();
    boolean isAppBarExpanded();
}

MyRecyclerView.java

public class MyRecyclerView extends RecyclerView {

    public MyRecyclerView(Context context) {
        this(context, null);
    }

    public MyRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    private AppBarTracking mAppBarTracking;
    private View mView;
    private int mTopPos;
    private LinearLayoutManager mLayoutManager;

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {

        // App bar latching trouble is only with this type of movement when app bar is expanded
        // or collapsed. In touch mode, everything is OK regardless of the open/closed status
        // of the app bar.
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
                && isNestedScrollingEnabled()) {
            // Make sure the AppBar stays expanded when it should.
            if (dy > 0) { // swiped up
                if (mAppBarTracking.isAppBarExpanded()) {
                    // Appbar can only leave its expanded state under the power of touch...
                    consumed[1] = dy;
                    return true;
                }
            } else { // swiped down (or no change)
                // Make sure the AppBar stays collapsed when it should.
                // Only dy < 0 will open the AppBar. Stop it from opening by consuming dy if needed.
                mTopPos = mLayoutManager.findFirstVisibleItemPosition();
                if (mTopPos == 0) {
                    mView = mLayoutManager.findViewByPosition(mTopPos);
                    if (-mView.getTop() + dy <= 0) {
                        // Scroll until scroll position = 0 and AppBar is still collapsed.
                        consumed[1] = dy - mView.getTop();
                        return true;
                    }
                }
            }
        }

        boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        // Fix the scrolling problems when scrolling is disabled. This issue existed prior
        // to 26.0.0-beta2.
        if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
            offsetInWindow[1] = 0;
        }
        return returnValue;
    }

    @Override
    public void setLayoutManager(RecyclerView.LayoutManager layout) {
        super.setLayoutManager(layout);
        mLayoutManager = (LinearLayoutManager) getLayoutManager();
    }

    public void setAppBarTracking(AppBarTracking appBarTracking) {
        mAppBarTracking = appBarTracking;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "MyRecyclerView";
}

ScrollingActivity.java

public class ScrollingActivity extends AppCompatActivity
        implements AppBarTracking {

    private MyRecyclerView mNestedView;
    private int mAppBarOffset;
    private boolean mAppBarIdle = false;
    private int mAppBarMaxOffset;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mNestedView = findViewById(R.id.nestedView);

        final AppBarLayout appBar = findViewById(R.id.app_bar);

        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
                // mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
                // mAppBarOffset = mAppBarMaxOffset
                // mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
                // mAppBarOffset should never be > zero or less than mAppBarMaxOffset
                mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
            }
        });

        appBar.post(new Runnable() {
            @Override
            public void run() {
                mAppBarMaxOffset = -appBar.getTotalScrollRange();
            }
        });

        findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                // If the AppBar is fully expanded or fully collapsed (idle), then disable
                // expansion and apply the patch; otherwise, set a flag to disable the expansion
                // and apply the patch when the AppBar is idle.
                setExpandEnabled(false);
            }
        });

        findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                setExpandEnabled(true);
            }
        });

        mNestedView.setAppBarTracking(this);
        mNestedView.setLayoutManager(new LinearLayoutManager(this));
        mNestedView.setAdapter(new Adapter() {
            @Override
            public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                        android.R.layout.simple_list_item_1,
                        parent,
                        false)) {
                };
            }

            @SuppressLint("SetTextI18n")
            @Override
            public void onBindViewHolder(final ViewHolder holder, final int position) {
                ((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        });
    }

    private void setExpandEnabled(boolean enabled) {
        mNestedView.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isAppBarExpanded() {
        return mAppBarOffset == 0;
    }

    @Override
    public boolean isAppBarIdle() {
        return mAppBarIdle;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "ScrollingActivity";
}

这里发生了什么?

从问题中可以明显看出,当用户的手指不在屏幕上时,布局无法按应有的方式关闭或打开应用栏。拖动时,应用栏会正常运行。

在版本 26.0.0-beta2 中,引入了一些新方法 - 特别是 dispatchNestedPreScroll() with a new type argument. The type argument specifies if the movement specified by dx and dy are due to the user touching the screen ViewCompat.TYPE_TOUCH or not ViewCompat.TYPE_NON_TOUCH

虽然没有确定导致问题的具体代码,但修复的重点是在需要时通过不让垂直移动传播来终止 dispatchNestedPreScroll() 中的垂直移动(处理 dy) .实际上,应用栏在展开时会锁定到位,并且在通过触摸手势关闭之前不允许开始关闭。应用栏在关闭时也将被锁定,直到 RecyclerView 位于其最顶端并且有足够的 dy 在执行触摸手势时打开应用栏。

因此,这与其说是一种修复,不如说是一种对有问题情况的阻止。

MyRecyclerView 代码的最后一部分处理了在此 中发现的一个问题,即禁用嵌套滚动时不正确的滚动移动。这是在调用 dispatchNestedPreScroll() 的 super 改变 offsetInWindow[1] 的值之后出现的部分。此代码背后的想法与已接受的问题答案中的想法相同。唯一的区别是,由于底层嵌套滚动代码已更改,参数 offsetInWindow 有时为空。幸运的是,它在重要的时候似乎是非空的,所以最后一部分继续工作。

需要注意的是,这个 "fix" 非常针对所问的问题,并不是通用的解决方案。该修复程序的保质期可能会很短,因为我希望这样一个明显的问题很快就会得到解决。

看起来 onStartNestedScrollonStopNestedScroll 调用可以重新排序,这会导致 "wobbly" 崩溃。我在 AppBarLayout.Behavior 里面做了一个小改动。真的不想像其他答案所建议的那样弄乱 activity 中的所有内容。

@SuppressWarnings("unused")
public class ExtAppBarLayoutBehavior extends AppBarLayout.Behavior {

    private int mStartedScrollType = -1;
    private boolean mSkipNextStop;

    public ExtAppBarLayoutBehavior() {
        super();
    }

    public ExtAppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
        if (mStartedScrollType != -1) {
            onStopNestedScroll(parent, child, target, mStartedScrollType);
            mSkipNextStop = true;
        }
        mStartedScrollType = type;
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
        if (mSkipNextStop) {
            mSkipNextStop = false;
            return;
        }
        if (mStartedScrollType == -1) {
            return;
        }
        mStartedScrollType = -1;
        // Always pass TYPE_TOUCH, because want to snap even after fling
        super.onStopNestedScroll(coordinatorLayout, abl, target, ViewCompat.TYPE_TOUCH);
    }
}

在 XML 布局中的用法:

<android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.AppBarLayout
        app:layout_behavior="com.example.ExtAppBarLayoutBehavior">

        <!-- Put here everything you usually add to AppBarLayout: CollapsingToolbarLayout, etc... -->

    </android.support.design.widget.AppBarLayout>

    <!-- Content: recycler for example -->
    <android.support.v7.widget.RecyclerView
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    ...

</android.support.design.widget.CoordinatorLayout>

问题的根本原因很可能在RecyclerView。现在没有机会深入挖掘。

由于截至 2020 年 2 月问题仍未解决(最新的 material 库版本为 1.2.0-alpha5)我想分享我对有问题的 AppBar 动画的解决方案。

想法是通过扩展 AppBarLayout.Behavior(Kotlin 版本)来实现自定义捕捉逻辑:

package com.example

import android.content.Context
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams

@Suppress("unused")
class AppBarBehaviorFixed(context: Context?, attrs: AttributeSet?) :
    AppBarLayout.Behavior(context, attrs) {

    private var view: AppBarLayout? = null
    private var snapEnabled = false

    private var isUpdating = false
    private var isScrolling = false
    private var isTouching = false

    private var lastOffset = 0

    private val handler = Handler()

    private val snapAction = Runnable {
        val view = view ?: return@Runnable
        val offset = -lastOffset
        val height = view.run { height - paddingTop - paddingBottom - getChildAt(0).minimumHeight }

        if (offset > 1 && offset < height - 1) view.setExpanded(offset < height / 2)
    }

    private val updateFinishDetector = Runnable {
        isUpdating = false
        scheduleSnapping()
    }

    private fun initView(view: AppBarLayout) {
        if (this.view != null) return

        this.view = view

        // Checking "snap" flag existence (applied through child view) and removing it
        val child = view.getChildAt(0)
        val params = child.layoutParams as LayoutParams
        snapEnabled = params.scrollFlags hasFlag LayoutParams.SCROLL_FLAG_SNAP
        params.scrollFlags = params.scrollFlags removeFlag LayoutParams.SCROLL_FLAG_SNAP
        child.layoutParams = params

        // Listening for offset changes
        view.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, offset ->
            lastOffset = offset

            isUpdating = true
            scheduleSnapping()

            handler.removeCallbacks(updateFinishDetector)
            handler.postDelayed(updateFinishDetector, 50L)
        })
    }

    private fun scheduleSnapping() {
        handler.removeCallbacks(snapAction)
        if (snapEnabled && !isUpdating && !isScrolling && !isTouching) {
            handler.postDelayed(snapAction, 50L)
        }
    }

    override fun onLayoutChild(
        parent: CoordinatorLayout,
        abl: AppBarLayout,
        layoutDirection: Int
    ): Boolean {
        initView(abl)
        return super.onLayoutChild(parent, abl, layoutDirection)
    }

    override fun onTouchEvent(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        ev: MotionEvent
    ): Boolean {
        isTouching =
            ev.actionMasked != MotionEvent.ACTION_UP && ev.actionMasked != MotionEvent.ACTION_CANCEL
        scheduleSnapping()
        return super.onTouchEvent(parent, child, ev)
    }

    override fun onStartNestedScroll(
        parent: CoordinatorLayout,
        child: AppBarLayout,
        directTargetChild: View,
        target: View,
        nestedScrollAxes: Int,
        type: Int
    ): Boolean {
        val started = super.onStartNestedScroll(
            parent, child, directTargetChild, target, nestedScrollAxes, type
        )

        if (started) {
            isScrolling = true
            scheduleSnapping()
        }

        return started
    }

    override fun onStopNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        abl: AppBarLayout,
        target: View,
        type: Int
    ) {
        isScrolling = false
        scheduleSnapping()

        super.onStopNestedScroll(coordinatorLayout, abl, target, type)
    }


    private infix fun Int.hasFlag(flag: Int) = flag and this == flag

    private infix fun Int.removeFlag(flag: Int) = this and flag.inv()

}

现在将此行为应用于 xml 中的 AppBarLayout:

<android.support.design.widget.CoordinatorLayout>

    <android.support.design.widget.AppBarLayout
        app:layout_behavior="com.example.AppBarBehaviorFixed">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <!-- Toolbar declaration -->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <!-- Scrolling view (RecyclerView, NestedScrollView) -->

</android.support.design.widget.CoordinatorLayout>

这仍然是一个 hack,但它似乎工作得很好,并且它不需要将脏代码放入您的 activity 或扩展 RecyclerView 和 NestedScrollView 小部件(感谢 @vyndor 的这个想法)。