代码之家  ›  专栏  ›  技术社区  ›  android developer

如何避免折叠工具栏布局在滚动时未被抓取或“摆动”?

  •  30
  • android developer  · 技术社区  · 7 年前

    出身背景

    假设您创建了一个应用程序,该应用程序的UI与您可以通过“滚动活动”向导创建的应用程序相似,但您希望滚动标志具有捕捉功能,例如:

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

    问题

    事实证明,在许多情况下,它都存在捕捉问题。有时,用户界面没有捕捉到顶部/底部,使折叠工具栏布局处于两者之间。

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

    我认为这是我在回收视图中使用setNestedScrollingEnabled(false)时遇到的问题之一,所以我问了这个问题 here ,但后来我注意到,即使使用该解决方案并且根本不使用此命令,甚至在使用简单的NestedScrollView(由向导创建)时,我仍然可以注意到这种行为。

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

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


    编辑:这是一个很好的改进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 回复  |  直到 7 年前
        1
  •  10
  •   Cheticamp    7 年前

    使现代化 我稍微修改了代码以解决剩余的问题-至少是我可以复制的问题。关键的更新是处理 dy 仅当AppBar展开或折叠时。在第一次迭代中, dispatchNestedPreScroll() 在未检查AppBar的状态是否为折叠状态的情况下处理滚动。


    这个答案解决了关于以下方面的问题: RecyclerView 我给出的另一个答案仍然适用于这里。 回收视图 NestedScrollView 在支持库的26.0.0-beta2中引入了。

    以下代码基于 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";
    }
    

    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";
    }
    

    这里发生了什么?

    从问题中可以明显看出,当用户的手指不在屏幕上时,布局无法按应有的方式关闭或打开应用程序栏。拖动时,应用程序栏的行为与它应该的一样。

    dispatchNestedPreScroll() 有一个新的 type 论点这个 类型 dx dy公司 是由于用户触摸屏幕 ViewCompat.TYPE_TOUCH ViewCompat.TYPE_NON_TOUCH .

    虽然没有确定导致问题的具体代码,但解决方法是消除垂直移动 dy公司 )当需要时,不要让垂直运动传播。实际上,应用程序栏在扩展时将锁定到位,并且在通过触摸手势关闭之前,不允许开始关闭。应用程序栏也将在关闭时锁定,直到 位于其最顶端,并且有足够的 在执行触摸手势时打开应用程序栏。

    因此,这与其说是一种修复,不如说是一种对问题条件的劝阻。

    MyRecyclerView 该代码处理在本文中识别的问题 question 当嵌套滚动被禁用时,处理不正确的滚动移动。这是在调用 dispatchNestedPreScroll() 这改变了 offsetInWindow[1] offsetInWindow 有时为空。幸运的是,当它重要时,它似乎是非空的,所以最后一部分继续工作。

    需要注意的是,这个“修复”非常特定于所提出的问题,不是一个通用的解决方案。修复可能会有很短的保质期,因为我预计这样一个明显的问题很快就会得到解决。

        2
  •  10
  •   vyndor    6 年前

    onStartNestedScroll onStopNestedScroll 调用可以重新排序,这会导致“抖动”捕捉。我在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 .现在没有机会深入挖掘。

        3
  •  5
  •   Cheticamp    7 年前

    编辑 NestedScrollView RecyclerView .


    这是API 26.0.0-beta2版本中引入的问题。在beta 1版本或API 25中不会出现这种情况。正如您所注意到的,API 26.0.0也会出现这种情况。一般来说,问题似乎与beta2中如何处理投掷和嵌套滚动有关。嵌套滚动有一个重大重写(请参阅 "Carry on Scrolling" )因此,出现此类问题也就不足为奇了。

    我的想法是,多余的卷轴没有被适当地处理 解决方法是悄悄地使用某些“非触摸”卷轴( type == ViewCompat.TYPE_NON_TOUCH )当AppBar展开或折叠时。这会停止反弹,允许捕捉,并且通常会使AppBar表现更好。

    ScrollingActivity 已修改以跟踪AppBar的状态,以报告其是否已展开。一个新的类调用“MyNestedScrollView”重写 dispatchNestedPreScroll() here )操纵多余卷轴的消耗。

    以下代码应足以停止 AppBarLayout 从摇晃和拒绝折断。(XML也必须更改以适应 MyNestedSrollView

    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";
    }
    
        4
  •  1
  •   Alex Vasilkov    5 年前

    由于该问题截至2020年2月仍未解决(最新的材质库版本为1.2.0-alpha5),我想与大家分享我对有缺陷的AppBar动画的解决方案。

    其思想是通过扩展AppBarLayout来实现自定义捕捉逻辑。行为(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()
    
    }
    

    <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>
    
    

    这仍然是一种黑客攻击,但它似乎工作得很好,它不需要将脏代码放入您的活动中,也不需要扩展RecyclerView和NestedScrollView小部件(感谢@vyndor的这一想法)。