ViewPager2中包含水平滚动视图的问题

15

我为我的项目实现了新的ViewPager。 viewPager2 包含一个 fragment 列表。

 private class ViewPagerAdapter extends FragmentStateAdapter {

    private ArrayList<Integer> classifiedIds;

    ViewPagerAdapter(@NonNull Fragment fragment, final ArrayList<Integer> classifiedIds) {
        super(fragment);
        this.classifiedIds = classifiedIds;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return DetailsFragment.newInstance(classifiedIds.get(position));
    }

    @Override
    public int getItemCount() {
        return classifiedIds.size();
    }
}

在片段中,我有一个水平的recyclerView。

LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
recyclerViewPicture.setLayoutManager(layoutManager);

问题是当我尝试滚动recyclerview时,viewPager会接收到触摸并切换到下一个片段。

在使用旧的ViewPager时,我没有遇到这个问题。


你尝试过使用不可滑动的ViewPager吗?你可以在ViewPager上停止滑动事件。 - Prayag Gediya
@TakeInfos 是的,我刚试了一下,recyclerview 可以正确滚动。但是我想保留 viewpager 的滑动功能。 - Vodet
尝试使用 recyclerView.setNestedScrollingEnabled(true); 或者 ViewCompat.setNestedScrollingEnabled(recyclerView, true);,也许会有帮助。 - Prayag Gediya
是的,我尝试了,但有时候它并不起作用,有时候回收站会接受触摸,但有时候不会。 - Vodet
2
我有一个技巧,但我以前没有尝试过。您将获得recyclerview的最后一个可见状态。因此,当recyclerview到达最后一个项目时,您可以为viewpager启用用户交互。viewPager2.setUserInputEnabled(true)否则将其设置为false - Prayag Gediya
5个回答

21

我遇到了同样的问题:使用AndroidX,一个ViewPager2(水平方向)在其中一页内有一个具有水平方向的RecyclerView。

我找到的有效解决方案来自Google的issueTracker。这是我对Kotlin类的Java翻译:

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

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

// from https://issuetracker.google.com/issues/123006042#comment21

/**
 * 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.
 *
 * 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).
 */

public class NestedScrollableHost extends FrameLayout {

    private int touchSlop = 0;
    private float initialX = 0.0f;
    private float initialY = 0.0f;

    private ViewPager2 parentViewPager() {
        View v = (View)this.getParent();
        while( v != null && !(v instanceof ViewPager2) )
            v = (View)v.getParent();
        return (ViewPager2)v;
    }

    private View child() { return (this.getChildCount() > 0 ? this.getChildAt(0) : null); }

    private void init() {
        this.touchSlop = ViewConfiguration.get(this.getContext()).getScaledTouchSlop();
    }

    public NestedScrollableHost(@NonNull Context context) {
        super(context);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.init();
    }

    private boolean canChildScroll(int orientation, Float delta) {
        int direction = (int)(Math.signum(-delta));
        View child = this.child();

        if( child == null )
            return false;

        if( orientation == 0 )
            return child.canScrollHorizontally(direction);
        if( orientation == 1 )
            return child.canScrollVertically(direction);

        return false;
    }

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

    private void handleInterceptTouchEvent(MotionEvent ev) {
        ViewPager2 vp = this.parentViewPager();
        if( vp == null )
            return;

        int orientation = vp.getOrientation();

        // Early return if child can't scroll in same direction as parent
        if( !this.canChildScroll(orientation, -1.0f) && !this.canChildScroll(orientation, 1.0f) )
            return;

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

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            float scaleDx = Math.abs(dx) * (isVpHorizontal ? 0.5f : 1.0f);
            float scaleDy = Math.abs(dy) * (isVpHorizontal ? 1.0f : 0.5f);

            if( scaleDx > this.touchSlop || scaleDy > this.touchSlop ) {
                if( isVpHorizontal == (scaleDy > scaleDx) ) {
                    // Gesture is perpendicular, allow all parents to intercept
                    this.getParent().requestDisallowInterceptTouchEvent(false);
                }
                else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if( this.canChildScroll(orientation, (isVpHorizontal ? dx : dy)) ) {
                        this.getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    else {
                        // Child cannot scroll, allow all parents to intercept
                        this.getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }
            }
        }
    }
}

然后,只需将您的嵌套RecyclerView嵌入NestedScrollableHost容器中:

<mywishlist.sdk.Base.NestedScrollableHost
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/photos"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/photolist_collection_background"
        android:orientation="horizontal">

    </androidx.recyclerview.widget.RecyclerView>

</mywishlist.sdk.Base.NestedScrollableHost>

它解决了我嵌套的RecyclerView和它所在的ViewPager2之间的滚动冲突。

4
谢谢,这对我有用!我使用了在Google讨论中提到的经过修改的版本,可以在这里找到:https://gist.github.com/micer/a6169a84acf200b9d44b2d942600b139 - Kotlin - Micer
你的链接是我改编成Java的原始Kotlin版本 :-) - Nicolas Buquet
没错,就是有一些小改动。 - Micer
2
它对我起作用了。顺便说一下,这是来自Android小部件的一个组件 --> https://github.com/android/views-widgets-samples/blob/master/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt - GreatNews

14
我找到了一个解决方案,这是一个已知的错误,你可以在这里看到:https://issuetracker.google.com/issues/123006042,也许他们会在下一个更新中解决它。
感谢TakeInfos和链接中的示例项目。
 recyclerViewPicture.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
        int lastX = 0;
        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
            switch (e.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = (int) e.getX();
                    break;
                case MotionEvent.ACTION_MOVE:
                    boolean isScrollingRight = e.getX() < lastX;
                    if ((isScrollingRight && ((LinearLayoutManager) recyclerViewPicture.getLayoutManager()).findLastCompletelyVisibleItemPosition() == recyclerViewPicture.getAdapter().getItemCount() - 1) ||
                            (!isScrollingRight && ((LinearLayoutManager) recyclerViewPicture.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
                       viewPager.setUserInputEnabled(true);
                    } else {
                        viewPager.setUserInputEnabled(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    lastX = 0;
                    viewPager.setUserInputEnabled(true);
                    break;
            }
            return false;
        }

        @Override
        public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        }
    });

我正在检查用户是否向右或向左滚动。 如果用户到达recyclerView的末尾或开头,则我会启用或禁用视图页面上的滑动


1
请在issuetracker链接中检查NestedRecyclerViewSameOrientation.zip Aug 8 2019压缩文件。我记不清了,但可能是用它来寻找解决方案。 - Vodet
谢谢,我已经从https://issuetracker.google.com/action/issues/123006042/attachments/26855080?download=true下载了它。 - Gary Chen

1
最简单的方法是这样的: 首先监听页面更改事件,并在您拥有recyclerView的页面上禁用触摸:
    myPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            when(position){
                2 -> myPager.isUserInputEnabled = false //for recycler view
                else -> myPager.isUserInputEnabled = true
            }
            super.onPageSelected(position)
        }
    })

然后在您希望用户能够滑动页面的视图上,执行以下操作:

viewForPageSwipe.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    myPager?.isUserInputEnabled = true
                    viewForPageSwipe.onTouchEvent(event)
                    return@setOnTouchListener true
                }
            }
            return@setOnTouchListener false
        }

根据您的逻辑,您可能需要将myPager对象传递给您的recycleView Fragment。

0
在我看来,这个解决方案(从Daniel Knauf post中窃取)比创建一个包装器要简单得多,但仍然不是官方的。
recyclerViewPicture.addOnItemTouchListener(
    object : RecyclerView.OnItemTouchListener {
        private var startX = 0f

        override fun onInterceptTouchEvent(
            recyclerView: RecyclerView,
            event: MotionEvent
        ): Boolean =
            when (event.action) {
                MotionEvent.ACTION_DOWN -> startX = event.x
                MotionEvent.ACTION_MOVE -> {
                    val isScrollingRight = event.x < startX
                    val scrollItemsToRight = isScrollingRight && recyclerView.canScrollRight
                    val scrollItemsToLeft = !isScrollingRight && recyclerView.canScrollLeft
                    val disallowIntercept = scrollItemsToRight || scrollItemsToLeft
                    recyclerView.parent.requestDisallowInterceptTouchEvent(disallowIntercept)
                }
                MotionEvent.ACTION_UP -> startX = 0f
                else -> Unit
            }.let { false }

        override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) = Unit
        override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) = Unit
    }
)

val RecyclerView.canScrollRight: Boolean
    get() = canScrollHorizontally(SCROLL_DIRECTION_RIGHT)

val RecyclerView.canScrollLeft: Boolean
    get() = canScrollHorizontally(SCROLL_DIRECTION_LEFT)

private const val SCROLL_DIRECTION_RIGHT = 1
private const val SCROLL_DIRECTION_LEFT = -1

-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接