CoordinatorLayout+AppBarLayout+RecyclerView 滑动冲突引发屏幕抖动的解决方案

前言

项目示例图
这里写图片描述
出现的问题链接:https://ask.csdn.net/questions/363070
跟这个哥们遇到的问题一样一样的;

原因

用到的布局结构就是CoordinatorLayout+AppBarLayout+ViewPager 然后Viewpager里是两个RecyclerView;

当你appbar高度低的时候一般不会触发这个问题,因为appbar fling 豪无用武之地。
只有你的appbar的高度到达一定的程度,那么问题就出来。

So You know !! 原因是什么了吧。其实就是appbar 有向下滑动的事件 ,然后你recylerView有向上滑动的事件,两个事件冲突,你往下挪动一下,我往上挪动一下,沙卡拉卡一会。

解决

问题出来首先要找根源,recyclerview 跟appbar 关联的桥梁是什么, Behavior ,具体说是 AppBarLayout.Behavior

但是这个类中并么有处理滑动的东东,so 向上刨根问题。 看他的父类HeaderBehavior

 @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (mTouchSlop < 0) {
            mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }

        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();

                if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
                    mLastMotionY = y;
                    mActivePointerId = ev.getPointerId(0);
                    ensureVelocityTracker();
                } else {
                    return false;
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int dy = mLastMotionY - y;

                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
                    mIsBeingDragged = true;
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                }

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    // We're being dragged so scroll the ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

            case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
                // $FALLTHROUGH
            case MotionEvent.ACTION_CANCEL: {
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            }
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(ev);
        }

        return true;
    }

看到了吧 在这个类中赋予了滑动属性。 fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel); 这个就是问题出现的原因, fling 还在进行 你就开始向上滑动,肯定有问题啊。

解决办法跟NestedScrolling机制 有关。

不懂这套机制的可以参考这篇文章:https://blog.csdn.net/lmj623565791/article/details/52204039

简单说呢 就是recyclerView滑动前会触发onNestedPreScroll 方法 告诉他爸爸 让他爸爸知道他要开始滑动了,so我们在这个里做操作直接结束Appbar的滑动就行。

代码

import android.content.Context;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.OverScroller;

import java.lang.reflect.Field;

/**
 * Created by 于德海 on 2018/4/27.
 * package inter.baisong.widgets
 * email : yudehai0204@163.com
 *
 * @describe  自定义behavior  以解决滑动抖动
 *
 */

public class CustomBehavior extends  AppBarLayout.Behavior {
    private OverScroller mScroller;
    public CustomBehavior() {
    }

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

    /**
     * 反射获得滑动属性。
     *
     * @param context
     */
    private void getParentScroller(Context context) {
        if (mScroller != null) return;
        mScroller = new OverScroller(context);
        try {
            Class<?> reflex_class = getClass().getSuperclass().getSuperclass();//父类AppBarLayout.Behavior  父类的父类   HeaderBehavior
            Field fieldScroller = reflex_class.getDeclaredField("mScroller");
            fieldScroller.setAccessible(true);
            fieldScroller.set(this, mScroller);
        } catch (Exception e) {}
    }
    //fling上滑appbar然后迅速fling下滑recycler时, HeaderBehavior的mScroller并未停止, 会导致上下来回晃动
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if(mScroller!=null){ //当recyclerView 做好滑动准备的时候 直接干掉Appbar的滑动
            if (mScroller.computeScrollOffset()) {
                mScroller.abortAnimation();
            }
        }

        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent e) {

        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                break;
        }



        return super.onTouchEvent(parent,child,e);
    }
}

使用位置是在布局文件的appbarlayout中加behavior

示例:

<android.support.design.widget.AppBarLayout
            android:id="@+id/mAppbar"
            app:elevation="0dip"
            app:layout_behavior="inter.****.widgets.CustomBehavior"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

结尾

大工告成, game over !

二次问题

原来的代码会造成appbar无法拖动问题 出现原因不明
修复方式就是删除判断recylerview 滚动到头部问题

后有新问题 点击appbar部分并不能停止recyclerView的滚动

so 需要自己写appbar点击事件 找到自己的recyclerview 手动停止 比较low的方法 有好的方法望各位大佬指点

代码

 mAppbar.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(event.getAction()==MotionEvent.ACTION_DOWN){
                    for (int i =0;i<mViewPager.getChildCount();i++){
                        View view = mViewPager.getChildAt(i);
                        if(view instanceof RecyclerView ){
                            ViewCompat.stopNestedScroll(view);
                        }
                    }
                }
                return false;
            }
        });
<think> 嗯,用户提出了一个Android开发中嵌套滑动组件的典型问题,涉及CoordinatorLayout家族组件的交互细节。从用户引用的资料来看,ta已经做了不少调研——引用1提到多层RecyclerView嵌套导致的颤抖问题,引用3详细说明了CollapsingToolbarLayout的滑动标志位配置,引用4则展示了通过触摸事件干预滑动行为的技巧。用户明显是个有经验的开发者,可能正在调试复杂布局。 用户的核心诉求很明确:为什么AppBarLayout有时会"卡住",必须等RecyclerView滑动才能响应?这其实涉及到CoordinatorLayout嵌套滑动机制的三个关键点: 首先是滑动标志位配置问题。CollapsingToolbarLayout如果没有正确设置scroll|enterAlways这类flag,AppBarLayout就无法主动发起滑动,这正是引用3强调的重点。需要检查XML里是否漏掉scroll这个基础flag。 其次是焦点抢占的矛盾。当RecyclerView内容区域获取焦点时(比如包含EditText),触摸事件会被优先消费,导致AppBarLayout失去滑动机会。这时候需要像引用4那样主动干预事件分发,但更优雅的解法应该是清除RecyclerView内部控件的焦点。 最后是嵌套滚动链断裂的特殊情况。如果RecyclerView外层还有ScrollView等容器,或者启用了特殊的分页模式,可能破坏CoordinatorLayout滑动协调机制。这时就需要继承RecyclerView重写onInterceptTouchEvent,或者设置NestedScrollingChild3接口强制启用嵌套滑动。 用户没有提及具体场景,所以需要给出阶梯式解决方案:从最简单的XML配置检查开始(80%问题可能出在这里),再进阶到焦点管理和自定义嵌套滚动逻辑。考虑到ta引用的代码片段里有触摸事件拦截的尝试,应该特别说明ViewCompat.stopNestedScroll的正确用法,避免出现引用4方案中可能导致的滑动卡顿。 有趣的是引用2提到NestedScrollView替代RecyclerView的解法,这其实暗示了问题的本质:RecyclerView自身嵌套滑动支持需要显式激活(setNestedScrollingEnabled)。或许该提醒用户对比测试NestedScrollView的行为,能更快定位问题根源。 </think> 在 `CoordinatorLayout` + `AppBarLayout` + `CollapsingToolbarLayout` + `RecyclerView` 组合中,**AppBarLayout 无法立即滑动而需先滑动 RecyclerView** 的问题通常由以下原因及解决方案造成: --- ### **原因分析** 1. **滑动标志位(`layout_scrollFlags`)配置错误** - `CollapsingToolbarLayout` 的子视图(如 `Toolbar`)未正确设置 `app:layout_scrollFlags`,导致 CoordinatorLayout 无法识别滑动行为。 - 错误示例:未包含 `scroll` 标志位。 2. **嵌套滑动冲突** - RecyclerView 未启用嵌套滑动(`setNestedScrollingEnabled(false)`),或被父布局拦截事件。 - RecyclerView 内容未填满屏幕时,可能无法触发 AppBarLayout 的折叠[^3]。 3. **焦点抢占问题** - RecyclerView 的子项(如 EditText)获取焦点时,触摸事件优先传递给子项,导致 AppBarLayout 无法响应滑动。 4. **布局层级或滚动方向限制** - 嵌套多层 RecyclerView 或使用了不支持嵌套滑动的容器(如 ScrollView),破坏滑动链[^1]。 --- ### **解决方案** #### 1. **检查滑动标志位配置** ```xml <com.google.android.material.appbar.CollapsingToolbarLayout app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"> <!-- Toolbar/ImageView 等子视图 --> <androidx.appcompat.widget.Toolbar app:layout_collapseMode="pin"/> <!-- 固定Toolbar --> </com.google.android.material.appbar.CollapsingToolbarLayout> ``` - **关键点**:确保 `layout_scrollFlags` 包含 `scroll`(基础滑动标志)和 `exitUntilCollapsed`(折叠至最小高度)[^3]。 #### 2. **启用嵌套滑动** ```java recyclerView.setNestedScrollingEnabled(true); // 默认true,若被禁用需启用 recyclerView.setHasFixedSize(true); // 优化性能 ``` - 若 RecyclerView 外层还有 ScrollView,改为 `NestedScrollView` 并设置: ```xml <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> ``` #### 3. **解决焦点抢占** ```java // 在Activity中清除RecyclerView内部控件的焦点 recyclerView.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { v.clearFocus(); // 清除子项焦点 ViewCompat.stopNestedScroll(recyclerView); // 停止遗留滑动[^4] } return false; }); ``` #### 4. **强制传递滑动事件** ```java // 自定义RecyclerView,重写事件分发 public class FixRecyclerView extends RecyclerView { @Override public boolean dispatchTouchEvent(MotionEvent e) { getParent().requestDisallowInterceptTouchEvent(true); // 禁止父布局拦截事件 return super.dispatchTouchEvent(e); } } ``` #### 5. **检查布局约束** - 确保 RecyclerView 的高度设置为 `match_parent` 而非 `wrap_content`,避免内容不足时无法触发折叠。 - 移除不必要的嵌套滚动容器(如多层 RecyclerView),改用单一 `NestedScrollView`[^1][^2]。 --- ### **总结流程** 1. **检查 XML 标志位** → 2. **启用嵌套滑动** → 3. **处理焦点抢占** → 4. **优化布局层级**。 若仍无效,通过自定义事件分发或滑动行为拦截遗留滑动趋势[^4]。 ---
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫的于

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值