26

I have a RecyclerView with a LinearLayoutManager in orientation HORIZONTAL. Each item in it can have interactive elements (including vertical ScrollViews). Is there a way to easily make the RecyclerView ignore any attempts by the user to scroll or fling the RecyclerView horizontally without intercepting touch events to the children?

I am programmatically controlling the scroll of the RecyclerView which works great until the user flings it.

I have tried something very simple where the idea is when I call smoothScrollToPosition in response to some event, I enable scrolling and disable touch events until the scroll settles. Like this:

  private class NoScrollHorizontalLayoutManager extends LinearLayoutManager {
    ScrollingTouchInterceptor interceptor = new ScrollingTouchInterceptor();
    protected boolean canScroll;

    public NoScrollHorizontalLayoutManager(Context ctx) {
      super(ctx, LinearLayoutManager.HORIZONTAL, false);
    }

    public RecyclerView.OnItemTouchListener getInterceptor() {
      return interceptor;
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
      canScroll = true;
      super.smoothScrollToPosition(recyclerView, state, position);
    }

    @Override
    public void onScrollStateChanged(int state) {
      super.onScrollStateChanged(state);
      if(state == RecyclerView.SCROLL_STATE_IDLE) {
        canScroll = false;
      }
    }

    @Override
    public boolean canScrollHorizontally() {
      return canScroll;
    }

    public class ScrollingTouchInterceptor implements RecyclerView.OnItemTouchListener {
      @Override
      public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        return canScrollHorizontally();
      }

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

      @Override
      public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
      }
    }
  }

And use it like this..

NoScrollHorizontalLayoutManager layout = new NoScrollHorizontalLayoutManager(context);
recycler.setLayoutManager(layout);
recycler.addOnItemTouchListener(layout.getInterceptor());

which actually almost works... but I can still screw up the programmatic scroll by tapping the screen when the smooth scroll is in motion. clearly I'm missing something obvious or there is a much smarter way to do this.

UPDATE the non-RecyclerView solution was found here: How do disable paging by swiping with finger in ViewPager but still be able to swipe programmatically?

/**
 * ViewPager that doesn't allow swiping.
 */
public class NonSwipeableViewPager extends ViewPager {

  public NonSwipeableViewPager(Context context) {
    super(context);
  }

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

  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // Never allow swiping to switch between pages
    return false;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Never allow swiping to switch between pages
    return false;
  }
}
Community
  • 1
  • 1
danb
  • 10,239
  • 14
  • 60
  • 76

10 Answers10

2

Overriding onInterceptTouchEvent you avoid the stop on click behaviour.

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    return false;
}
Borja
  • 1,318
  • 13
  • 18
2

The answer to disable RecyclerView scrolling by user touch input yet enable scrolling programmatically is to use:

 recyclerView.setOnTouchListener{ _, _ -> true }
 

and then you can use recyclerView.smoothScrollToPosition(position) to scroll programmatically.

coditive
  • 21
  • 3
1

First thing, I don't think this is achievable non-programmatically.

Way 1: Simple - Disable the other view based on the touch.

If user touch the parent disable the child views scroll. vice versa as well

Piece of code to do this

recyclerView.setOnTouchListener(new View.OnTouchListener() 
{
   public boolean onTouch(View p_v, MotionEvent p_event) 
    {
       yourChildView.getParent().requestDisallowInterceptTouchEvent(false);
       return false;
    }
});

Way 2: Since you are having small issues in your implementation. If you post it somebody can fix that as well.

Viswanath Lekshmanan
  • 9,945
  • 1
  • 40
  • 64
  • 1
    I think you misunderstand. The problem is that I don't want the user to be able to swipe the recycler view. I only want it to move when I tell it to in code. All the child views should act normally (and they do) – danb Oct 09 '15 at 18:37
  • yeah probably. Looking in to it. Could you try one thing more. Set the touch listener for `viewpager` and return true always – Viswanath Lekshmanan Oct 09 '15 at 19:15
  • The viewpager solution I added in the answer works as intended. It's in production now – danb Oct 09 '15 at 19:17
1

Here's a solution for horizontal scrolling that allows you to turn off scrolling, but still enable it via calling smoothScrollToPosition. It also lets the user interact with child views within the recycler view.

I found that the other solutions which enabled scrolling temporarily to be able to call smoothScrollToPosition had the side effect of letting the user interrupt scrolling, even when disabling touch events or adding another view on top of the recyclerview.

The key is to getting smoothScrollToPosition to work is to override LinearSmoothScroller.calculateDxToMakeVisible and remove the check to canScrollHorizontally()

class NoScrollHorizontalLayoutManager(context: Context) : LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) {

    override fun canScrollHorizontally(): Boolean {
        return false
    }

    override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?, position: Int) {
        val linearSmoothScroller = ForceHorizontalLinearSmoothScroller(recyclerView.context)
        linearSmoothScroller.targetPosition = position
        startSmoothScroll(linearSmoothScroller)
    }
}



class ForceHorizontalLinearSmoothScroller(context: Context) : LinearSmoothScroller(context) {

    override fun calculateDxToMakeVisible(view: android.view.View, snapPreference: Int): Int {
        val layoutManager = layoutManager
        if (layoutManager == null) {
            return 0
        }
        val params = view.layoutParams as RecyclerView.LayoutParams
        val left = layoutManager.getDecoratedLeft(view) - params.leftMargin
        val right = layoutManager.getDecoratedRight(view) + params.rightMargin
        val start = layoutManager.paddingLeft
        val end = layoutManager.width - layoutManager.paddingRight
        return calculateDtToFit(left, right, start, end, snapPreference)
    }
}

EDIT: I found that the user could still stop scrolling when tapping on the recycle view while the scroll was in progress. The fix was to override the RecycleView.onInterceptTouchEvent and prevent events when the smooth scroll was in progress or when the scroll state was set to SCROLL_STATE_SETTLING. (Using RecycleView.addOnItemTouchListener for this wont work because the RecycleView stops scrolling then the listener returns true in RecyleView.onInterceptTouchEvent.)

class NoScrollRecyclerView : RecyclerView {
    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

    override fun onInterceptTouchEvent(e : MotionEvent) : Boolean {
        if (layoutManager?.isSmoothScrolling() == true || scrollState == SCROLL_STATE_SETTLING) {
            return true
        }
        return super.onInterceptTouchEvent(e)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(e :MotionEvent) : Boolean {
        if (layoutManager?.isSmoothScrolling() == true || scrollState == SCROLL_STATE_SETTLING) {
            return true
        }
        return super.onTouchEvent(e)
    }
}
dru
  • 81
  • 1
  • 2
0
recyclerView.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          return true;
      }
  });
ZeroOne
  • 8,996
  • 4
  • 27
  • 45
0

This gives the effect of continuity.

recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                if (action == MotionEvent.ACTION_DOWN) {
                    recyclerView.smoothScrollToPosition(position);
                    return true;
                } else if (action == MotionEvent.ACTION_UP) {
                    recyclerView.smoothScrollToPosition(position);
                    return true;
                }
                return false;
            }
        });
0

I was trying to figure this out and the correct answer to disable RecyclerView scrolling by user touch input yet enable scrolling programmatically is to use:

recyclerView.suppressLayout(true)

Here is the surpressLayout documentation:

Tells this RecyclerView to suppress all layout and scroll calls until layout suppression is disabled with a later call to suppressLayout(false). When layout suppression is disabled, a requestLayout() call is sent if requestLayout() was attempted while layout was being suppressed. In addition to the layout suppression smoothScrollBy(int, int), scrollBy(int, int), scrollToPosition(int) and smoothScrollToPosition(int) are dropped; TouchEvents and GenericMotionEvents are dropped; RecyclerView.LayoutManager.onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State) will not be called. suppressLayout(true) does not prevent app from directly calling RecyclerView.LayoutManager.scrollToPosition(int), RecyclerView.LayoutManager.smoothScrollToPosition(RecyclerView, RecyclerView.State, int). setAdapter(RecyclerView.Adapter) and swapAdapter(RecyclerView.Adapter, boolean) will automatically stop suppressing. Note: Running ItemAnimator is not stopped automatically, it's caller's responsibility to call ItemAnimator.end(). Params: suppress – true to suppress layout and scroll, false to re-enable.

Kyle Williamson
  • 2,251
  • 6
  • 43
  • 75
0

I extended both RecyclerView and LayoutManager. Heavily customised but it works.

RecyclerView: accepts only the custom layoutmanager, override the RecyclerView's onInterceptTouchEvent, which if you look in the source code first handles the onInterceptTouchEvent, then calling the other onInterceptTouchEvent_s, added via addOnInterceptTouchEvent, then calling cancelScroll. Also an OnTouchListener has to be set.

public class NoManualScrollGravitySnapRecyclerView extends GravitySnapRecyclerView {
  
  private NoManualScrollGridLayoutManager layoutManager;
  
  public NoManualScrollGravitySnapRecyclerView(@NonNull Context context)
  {
    super(context);
    init(context);
  }
  
  public NoManualScrollGravitySnapRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs)
  {
    super(context, attrs);
    init(context);
  
  }
  
  public NoManualScrollGravitySnapRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)
  {
    super(context, attrs, defStyleAttr);
    init(context);
  
  }
  
  @Override
  public void setLayoutManager(@Nullable LayoutManager layout)
  {
    if(layout instanceof NoManualScrollGridLayoutManager)
      {
        layoutManager = (NoManualScrollGridLayoutManager) layout;
        super.setLayoutManager(layout);
      }
  }
  
  private void init(Context context)
  {
    setSnapListener((position)-> 
   layoutManager.setCanScrollVertically(false));

        if(layoutManager == null)
          {layoutManager = new NoManualScrollGridLayoutManager(context, 1);}
    layoutManager.setCanScrollVertically(false);

    setOnTouchListener(new OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event)
      {
        if(layoutManager.canScrollVertically()){ return true;}
        v.performClick();
        return false;
      }
    });
  }
  
  @Override
  public void smoothScrollToPosition(int position)
  {
    layoutManager.setCanScrollVertically(true);
    super.smoothScrollToPosition(position);
  }
  
  @Override
  public boolean onInterceptTouchEvent(MotionEvent e)
  {
    if(layoutManager.canScrollVertically()){ return true;}
    return super.onInterceptTouchEvent(e);
  }
  
}

LayoutManager: simply handle canScrollVertically or Horizontally and setCanScrollV/H by yourself.

public class NoManualScrollGridLayoutManager extends GridLayoutManager {
  private boolean canScrollVertically;
  
 ...constructors...
  
  @Override
  public boolean canScrollVertically()
  {
    return canScrollVertically;
  }
  
  public void setCanScrollVertically(boolean canScrollVertically)
  {
    this.canScrollVertically = canScrollVertically;
  }

}
Waqar UlHaq
  • 6,144
  • 2
  • 34
  • 42
Stef
  • 45
  • 8
-2
android:nestedScrollingEnabled="false"

Do the above one in the xml file , in the RecyclerView TAG.

Shivani Gupta
  • 271
  • 3
  • 12
-4
 recyclerView.setNestedScrollingEnabled(false);

For more info see documentation.

Istiak Morsalin
  • 10,621
  • 9
  • 33
  • 65