> 文章列表 > ViewPager2嵌套滚动解决方案

ViewPager2嵌套滚动解决方案

ViewPager2嵌套滚动解决方案

使用以下类对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();}@Overridepublic 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() {@Overridepublic boolean onPreDraw() {View v = (View) getParent();while (v != null && !(v instanceof ViewPager2)) {v = (View) v.getParent();}parentViewPager = (ViewPager2) v;getViewTreeObserver().removeOnPreDrawListener(this);return false;}});}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {handleInterceptTouchEvent(ev);return false;}@Overridepublic 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);}@Overridepublic 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 interceptgetParent().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() {@Overridepublic boolean onPreDraw() {View v = (View) getParent();while (v != null && !(v instanceof ViewPager2)) {v = (View) v.getParent();}parentViewPager = (ViewPager2) v;getViewTreeObserver().removeOnPreDrawListener(this);return false;}});}@Overridepublic 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 childfloat 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 interceptgetParent().requestDisallowInterceptTouchEvent(false);} else {// Gesture is parallel, query child if movement in that direction is possibleif (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {// Child can scroll, disallow all parents to interceptgetParent().requestDisallowInterceptTouchEvent(true);} else {// Child cannot scroll, allow all parents to interceptgetParent().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.NestedScrollableHostandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"><androidx.viewpager2.widget.ViewPager2android: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.NestedScrollableHostandroid:layout_width="match_parent"android:layout_height="90dp"android:clipChildren="false"android:clipToPadding="false"android:layout_below="@id/tag_layout"><androidx.recyclerview.widget.RecyclerViewandroid: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>