ViewPager2嵌套滚动解决方案

NestedScrollableHost是一个入口类,用于解决ViewPager2中嵌套的可滚动视图(如RecyclerView)的滑动冲突问题。它使用策略模式封装了两个内部类:DisableNestedScrollableHost和EnableNestedScrollableHost,分别处理子视图到达边缘时是否将事件传递给父视图。通过XML属性nestedScrollingEnabled可以控制是否启用嵌套滚动。文章提供了具体的代码实现和使用示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用以下类对ViewPager2中的recycleView进行包裹即可
解决的是如果子view可以水平滚动,则将水平滚动优先交给子类处理。

NestedScrollableHost 是入口
使用策略模式封装了两个类
DisableNestedScrollableHost

EnableNestedScrollableHost
两者的区别是
水平滚动到达子类的端点的时候,是否将事件交给父类。
DisableNestedScrollableHost是全部自己处理
EnableNestedScrollableHost是交给父类处理,因此可以嵌套滚动。

两种策略,通过xml的属性来确定使用哪一种。

注意:
DisableNestedScrollableHost
EnableNestedScrollableHost
的构造函数都是包级作用域的。为了避免被误用。
三个类需要声明在一个包内。

NestedScrollableHost 入口类

package com.trs.v6.news.ui.view;


import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.trs.news.R;


/**
 * <pre>
 * 用于解决ViewPager2嵌套产生的滑动冲突
 * 可以通过在xml中声明属性,来控制是否开启嵌套滚动。
 * 如果关闭嵌套滚动。那么在子控件滑动到断点时,事件不会传递给父控件。
 * <code>
 *       app:nestedScrollingEnabled="true"
 * </code>
 *
 * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
 * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
 * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
 * <p>
 * This solution has limitations when using multiple levels of nested scrollable elements
 * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
 * </pre>
 */
public class NestedScrollableHost extends FrameLayout {

    ViewGroup proxyView;
    private boolean nestedScrollable ;

    public NestedScrollableHost(@NonNull Context context) {
        this(context,null);

    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray array = context.obtainStyledAttributes(attrs, new int[]{R.attr.nestedScrollingEnabled});
        nestedScrollable = array.getBoolean(0, true);
        //此处使用策略模式,将可以嵌套滚动和不能嵌套滚动的逻辑封装进两个view
        //可以降低相关代码的逻辑复杂性。
        proxyView=nestedScrollable?new EnableNestedScrollableHost(context):new DisableNestedScrollableHost(context);
        addView(proxyView,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
        array.recycle();
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if(child!=proxyView){
            proxyView.addView(child,index,params);
            return;
        }
        super.addView(child, index, params);
    }
}

DisableNestedScrollableHost

package com.trs.v6.news.ui.view;


import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;

import com.trs.news.R;


/**
 * <pre>
 * 将左右的滑动事件全部传递给子view。上下滚动事件交由父类处理。
 *</pre>
 */
public class DisableNestedScrollableHost extends FrameLayout {

    private ViewPager2 parentViewPager;
    private int touchSlop = 0;
    private float initialX = 0f;
    private float initialY = 0f;


    /**
     * 将构造方法声明为包内级别,防止大家误用
     * @param context
     */
     DisableNestedScrollableHost(@NonNull Context context) {
        super(context);
        init(context);
    }




    private void init(Context context) {
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();


        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                View v = (View) getParent();
                while (v != null && !(v instanceof ViewPager2)) {
                    v = (View) v.getParent();
                }
                parentViewPager = (ViewPager2) v;

                getViewTreeObserver().removeOnPreDrawListener(this);
                return false;
            }
        });
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        handleInterceptTouchEvent(ev);
        return false;
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

        boolean isHorizontal = View.SCROLL_AXIS_HORIZONTAL == nestedScrollAxes;
        if (isHorizontal) {
            //如果不支持嵌套滚动 而且是横向滚动就拦截
            return true;
        }

        return super.onStartNestedScroll(child, target, nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, 0, dyUnconsumed);

    }


    private boolean handleInterceptTouchEvent(MotionEvent e) {
        if (parentViewPager == null) return false;
        int orientation = parentViewPager.getOrientation();


        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            initialX = e.getX();
            initialY = e.getY();
            getParent().requestDisallowInterceptTouchEvent(true);
        } else if (e.getAction() == MotionEvent.ACTION_MOVE) {
            float dx = e.getX() - initialX;
            float dy = e.getY() - initialY;
            boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;


            float scaledDx = Math.abs(dx);
            float scaledDy = Math.abs(dy);
            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // Gesture is perpendicular, allow all parents to intercept
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        }
        return false;
    }
}

EnableNestedScrollableHost

package com.trs.v6.news.ui.view;


import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;

import com.trs.news.R;


/**
 * <pre>
 *     允许子类左右滚动,左右滚动到端点以后,继续滚动,会将事件交给父类处理
 *     上下滚动的事件直接交给父类处理
 * </pre>
 */
public class EnableNestedScrollableHost extends FrameLayout {

    private ViewPager2 parentViewPager;
    private int touchSlop = 0;
    private float initialX = 0f;
    private float initialY = 0f;


     EnableNestedScrollableHost(@NonNull Context context) {
        super(context);
        init(context);
    }




    private void init(Context context) {
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();


        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                View v = (View) getParent();
                while (v != null && !(v instanceof ViewPager2)) {
                    v = (View) v.getParent();
                }
                parentViewPager = (ViewPager2) v;

                getViewTreeObserver().removeOnPreDrawListener(this);
                return false;
            }
        });
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return handleInterceptTouchEvent(ev);
    }




    private boolean canChildScroll(int orientation, float delta) {
        int direction = (int) -delta;
        View child = getChildAt(0);
        if (orientation == 0) {
            return child.canScrollHorizontally(direction);
        } else if (orientation == 1) {
            return child.canScrollVertically(direction);
        } else {
            throw new IllegalArgumentException();
        }
    }


    private boolean handleInterceptTouchEvent(MotionEvent e) {
        if (parentViewPager == null) return false;
        int orientation = parentViewPager.getOrientation();



        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            initialX = e.getX();
            initialY = e.getY();
            getParent().requestDisallowInterceptTouchEvent(true);
        } else if (e.getAction() == MotionEvent.ACTION_MOVE) {
            float dx = e.getX() - initialX;
            float dy = e.getY() - initialY;
            boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            float scaledDx = Math.abs(dx) * (isVpHorizontal ? .5f : 1f);
            float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1f : .5f);
            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // Gesture is perpendicular, allow all parents to intercept
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {
                        // Child can scroll, disallow all parents to intercept
                        getParent().requestDisallowInterceptTouchEvent(true);

                    } else {
                        // Child cannot scroll, allow all parents to intercept
                            getParent().requestDisallowInterceptTouchEvent(false);

                        //直接拦截子view的事件,这样事件就会使用嵌套滚动的机制交给父view处理
                        return true;
                    }
                }
            }
        }
        return false;
    }
}

属性

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <!--用于在NestedScrollableHost和HorizontalScrollViewParent中使用-->
    <!--用于判断是否支持嵌套滚动,所谓的嵌套滚动,定义为在和父控件同方向上,子控件达到端点后,是否将滑动事件交由父控件继续处理-->
    <attr name="nestedScrollingEnabled"  tools:ignore="MissingDefaultResource" tools:override="true"/>
</resources>

使用方法

   <com.trs.v6.news.ui.view.NestedScrollableHost
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.trs.v6.news.ui.view.NestedScrollableHost>

或者包裹RecycleView

 <com.trs.v6.news.ui.view.NestedScrollableHost
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:layout_below="@id/tag_layout">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:paddingStart="13dp"
            android:clipChildren="false"
            android:clipToPadding="false"
            android:layout_width="match_parent"
            android:layout_gravity="center_vertical"
            android:layout_height="wrap_content"
         />
    </com.trs.v6.news.ui.view.NestedScrollableHost>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值