【问题标题】:How to avoid CollapsingToolbarLayout not being snapped or being "wobbly" when scrolling?如何避免 CollapsingToolbarLayout 在滚动时不被捕捉或“摇摆不定”?
【发布时间】:2023-03-09 17:06:01
【问题描述】:

背景

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

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

【问题讨论】:

    标签: android android-collapsingtoolbarlayout


    【解决方案1】:

    更新 我稍微更改了代码以解决剩余的问题 - 至少是我可以重现的问题。关键更新是仅在 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() 的 super 之后出现的部分,它改变了 offsetInWindow[1] 的值。此代码背后的想法与该问题的公认答案中提出的想法相同。唯一的区别是,由于底层嵌套滚动代码已更改,参数offsetInWindow 有时为空。幸运的是,它在重要的时候似乎是非空的,所以最后一部分继续工作。

    需要注意的是,这个“修复”是针对所提出的问题非常具体的,并不是一个通用的解决方案。该修复程序的保质期可能会很短,因为我预计这样一个明显的问题会很快得到解决。

    【讨论】:

    • 遗憾的是,这并不能解决问题。它仍然可以停留在展开和折叠状态之间。有点难(甚至可能非常难),但仍然可以重现它。我认为它修复了“摇摆不定”的行为,尽管 UI 故障(我称它们为“工件”)有时仍会出现。因为它的表现比原来的好得多,所以我现在要对此表示赞同。再次感谢您对我的帮助。
    • 我也不确定,但我认为它现在会在我执行一个短手势切换到另一个状态时尝试捕捉到原始状态(例如:展开,尝试通过短手势折叠向下滚动,但它会迅速展开)
    • 顺便说一句,提示:我建议不要通过将 mAppBarTracking 的值放在 CTOR 中来强制 Activity 实现接口,因为视图可能位于 Fragment 中。
    • @androiddeveloper 我可以通过在滚动过程中向上翻然后立即向下翻来重现“介于”状态,以使顶部项目重新回到视图中。不过,我必须把握好时机,而且我只是偶尔会成功。这就是你所看到的吗?另外,我对“神器”很好奇。那些是什么?
    • @androiddeveloper 是的,MyRecyclerView 的代码已更新,以考虑可以关闭嵌套滚动。以前,它假设它始终处于开启状态。如果您认为所有问题都已解决,我可以在今天晚些时候用一些额外的解释性文字更新答案。
    【解决方案2】:

    看起来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。现在没有机会深入挖掘。

    【讨论】:

    • 这是整个解决方法,还是包含额外的代码?它是否需要其他答案的更改?请解释一下。
    • 是的,这是整个解决方法。对我来说,它工作正常。您甚至可以只使用 xml 中的 ExtBehavior。只需将 app:layout_behavior="com.example.ExtAppBarLayout$ExtBehavior 添加到您的 AppBarLayout xml 声明中。 AppBarLayout 没有必要子类化,但我这样做是为了方便。
    • 有趣。所以你说你现在不能(用你的新代码)重现我在视频中展示的任何问题?好的。希望尽快检查出来。
    • 我不确定它是否有助于 CollapsingToolbarLayout,在我的情况下,我使用普通的 AppBarLayout。但我认为这个问题只有一个原因。尝试时告诉我!
    • 如果所有代码都在行为中,为什么您实际上创建了 ExtAppBarLayout 类?无论如何,我现在已经测试了代码。它与 ClassCastException: android.support.v4.widget.NestedScrollView cannot be cast to android.support.design.widget.AppBarLayout 对我来说崩溃了。我已经在一个新项目中尝试过它,并且来自问题跟踪器。请展示如何使用您的代码。甚至可以通过在某处上传压缩文件来放置示例项目。
    【解决方案3】:

    编辑 代码已更新,使其更符合已接受答案的代码。此答案与NestedScrollView 有关,而接受的答案与RecyclerView 有关。


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

    我的想法是在NestedScrollView 的某个地方没有正确处理多余的卷轴。解决方法是在 AppBar 展开或折叠时安静地使用某些“非触摸”滚动 (type == ViewCompat.TYPE_NON_TOUCH) 滚动。这会阻止弹跳,允许快照,并且通常会使 AppBar 表现得更好。

    ScrollingActivity已修改为跟踪AppBar的状态以报告它是否展开。一个名为“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";
    }
    

    【讨论】:

    • 它甚至存在于最近几天发布的最新“最终”版本 (26.0.0)。
    • 我还是会通过预览渠道举报。如果他们注意到您的报告并决定修复它,它仍然可以发布最终版本。
    • 它也存在于非 Android-O 版本上。它与支持库有关,所以这是我报告的地方。
    • @androiddeveloper 提取答案以纠正某些问题。它现在被安置了。
    • @androiddeveloper RecyclerViewNestedScrollView 的代码在处理嵌套滚动的部分非常相似。我不知道当为NestedScrollView 关闭嵌套滚动时,什么会起作用或不会起作用。我认为MyRecyclerView 中的offsetInWindow 代码如果添加到MyNestedScrollView 中不会有害,它可能会有所帮助。
    【解决方案4】:

    由于截至 2020 年 2 月问题仍未解决(最新材质库版本为 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 的这个想法)。

    【讨论】:

    • 你能把它分享到 Github 上,以便我可以轻松地测试它是否存在可能的问题吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-09
    • 1970-01-01
    • 2016-03-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多