滚动时如何避免CollapsingToolbarLayout没有被捕捉或“颤抖”?

时间:2017-07-19 13:51:49

标签: android android-collapsingtoolbarlayout

背景

假设您创建的应用程序具有与您可以通过“滚动活动”向导创建的应用程序类似的用户界面,但您希望滚动标记具有对齐功能,如下所示:

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

问题

事实证明,在许多情况下,它存在捕捉问题。有时UI不会捕捉到顶部/底部,使得CollapsingToolbarLayout保持在两者之间。

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

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

我尝试了什么

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

这就是为什么我决定报告这个问题,here

可悲的是,我找不到StackOverflow上那些奇怪错误的解决方法。

问题

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

编辑:这是一个很好的改进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)
    }
}

4 个答案:

答案 0 :(得分:9)

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

其他变化很小,属于清理类别。代码块在下面更新。

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

以下代码基于this answer相关问题,但包含针对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(),带有新的type参数。 type参数指定dxdy指定的移动是由于用户触摸屏幕ViewCompat.TYPE_TOUCH还是ViewCompat.TYPE_NON_TOUCH而导致的。

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

所以,这并不像对有问题的条件的沮丧那么多。

MyRecyclerView代码的最后一部分处理了此question中标识的问题,该问题涉及禁用嵌套滚动时的不正确滚动移动。这是调用dispatchNestedPreScroll()超级用户后改变offsetInWindow[1]值的部分。此代码背后的想法与问题的已接受答案中提供的内容相同。唯一的区别是,由于底层嵌套滚动代码已更改,因此参数offsetInWindow有时为空。幸运的是,它在重要时似乎是非空的,所以最后一部分继续有效。

需要注意的是这个&#34;修复&#34;问题非常具体,不是一般解决方案。这个修复可能会有很短的保质期,因为我预计很快就会解决这个明显的问题。

答案 1 :(得分:9)

看起来onStartNestedScrollonStopNestedScroll来电可以重新排序,导致“摇摆不定”。我在AppBarLayout.Behavior中做了一个小黑客。不要像其他答案所提出的那样,真的想要搞乱活动中的所有东西。

@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中很可能是问题的根本原因。现在没有机会深入挖掘。

答案 2 :(得分:5)

编辑代码已更新,使其更符合接受答案的代码。此答案涉及NestedScrollView,而接受的答案约为RecyclerView

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

我的想法是在NestedScrollView的某处没有正确处理过多的滚动。解决方法是悄悄地使用某些非触摸式的卷轴。在展开或折叠AppBar时滚动(type == ViewCompat.TYPE_NON_TOUCH)。这会停止弹跳,允许捕捉,通常会使AppBar表现得更好。

已修改

ScrollingActivity以跟踪AppBar的状态,以报告其是否已展开。一个新的类调用&#34; MyNestedScrollView&#34;覆盖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";
}

答案 3 :(得分:1)

由于到2020年2月该问题仍未得到解决(最新的材料库版本为1.2.0-alpha5),所以我想分享对有问题的AppBar动画的解决方案。

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

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,但是它似乎运行得很好,它不需要在您的活动中放入肮脏的代码,也不需要扩展RecyclerView和NestedScrollView小部件(感谢@vyndor提供这个想法)。