ViewPager2 with horizontal scrollView inside
Asked Answered
J

5

15

I implemented the new ViewPager for my project. The viewPager2 contains a list of 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();
    }
}

Inside the fragment I got an horizontal recyclerView

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

The issue is when I try to scroll the recyclerview the viewPager take the touch and swap to the next fragment

When I was using the old ViewPager I didn't have this issue

Jillane answered 21/8, 2019 at 8:39 Comment(5)
Have you try with non swipable viewpager? You can stop swipe event on viewpager.Fermium
@TakeInfos Yes I just try it and the recyclerview can scroll correctly. But I would like to keep the viewpager swipeJillane
Try recyclerView.setNestedScrollingEnabled(true); or this ViewCompat.setNestedScrollingEnabled(recyclerView, true); may be it will help.Fermium
Yeah I try it but this is not working very well sometimes the recycler take the touch but sometimes noJillane
I have one trick but i haven't try it before. You will get last item visible state for recyclerview. so when recyclerview reach at last item you can enable user interaction for viewpager. viewPager2.setUserInputEnabled(true) otherwise set it falseFermium
J
14

I find a solution it's a know bug as you can see here https://issuetracker.google.com/issues/123006042 maybe they would solve it in the next updates

Thanks to TakeInfos and the exemple project inside the link

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

        }
    });

I'm checking if the user scroll on the right or on the left. If the user reach the end or the start of the recyclerView I'm enable or disable the swipe on the view pager

Jillane answered 21/8, 2019 at 10:35 Comment(2)
Check the NestedRecyclerViewSameOrientation.zip Aug 8 2019 inside the issuetracker link. I don t remember but maybe I'm using it for find the solutionJillane
Thanks, I downloaded it on issuetracker.google.com/action/issues/123006042/attachments/…Omdurman
P
21

I met the same problem: using AndroidX, a ViewPager2 (with horizontal orientation) having a RecyclerView (with horizontal orientation) inside one of its page.

The working solution I found is from Google issueTracker. Here is my Java translation of the Kotlin class:

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

Then, just embed your nested RecyclerView inside a NestedScrollableHost container:

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

It solved my scrolling conflict between the nested RecyclerView and its hosting ViewPager2.

Pinelli answered 13/8, 2020 at 12:37 Comment(4)
This works for me, thanks! I've used a modified version mentioned in the discussion on Google, available here: gist.github.com/micer/a6169a84acf200b9d44b2d942600b139 - KotlinSparling
Your link is the original Kotlin version I adapted to Java :-)Pinelli
Yep exactly, just with few minor changes.Sparling
It worked for me. FYI, this is a component from Android Widgets --> github.com/android/views-widgets-samples/blob/master/ViewPager2/…Tantivy
J
14

I find a solution it's a know bug as you can see here https://issuetracker.google.com/issues/123006042 maybe they would solve it in the next updates

Thanks to TakeInfos and the exemple project inside the link

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

        }
    });

I'm checking if the user scroll on the right or on the left. If the user reach the end or the start of the recyclerView I'm enable or disable the swipe on the view pager

Jillane answered 21/8, 2019 at 10:35 Comment(2)
Check the NestedRecyclerViewSameOrientation.zip Aug 8 2019 inside the issuetracker link. I don t remember but maybe I'm using it for find the solutionJillane
Thanks, I downloaded it on issuetracker.google.com/action/issues/123006042/attachments/…Omdurman
T
1

The simplest would be this: First listen to page change and disable touch on the page where you have 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)
        }
    })

Then on the View(s) you want the user to be able to swipe Page, do this:

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

depending on your logic, you'll probably have to pass the myPager object to your recycleView Fragment.

Theriault answered 26/5, 2023 at 4:21 Comment(0)
D
0

In my opinion, this solution (stolen from Daniel Knauf post) is much simpler than creating a wrapper but still not official:

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
Disembark answered 4/12, 2022 at 13:17 Comment(0)
B
-1

Call ViewGroup#onInterceptTouchEvent(MotionEvent).

See This Documentation

Behm answered 21/8, 2019 at 9:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.