Home » Android » Using Android's SlidingPaneLayout with ViewPager

Using Android's SlidingPaneLayout with ViewPager

Posted by: admin May 14, 2020 Leave a comment

Questions:

I’m trying to use the SlidingPaneLayout with ViewPager, like so

<?xml version="1.0" encoding="utf-8"?>

<android.support.v4.widget.SlidingPaneLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scientific_graph_slidingPaneLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <!--
         The first child view becomes the left pane.
    -->

    <ListView
            android:id="@+id/left_pane"
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="left" />
    <!--
         The second child becomes the right (content) pane.
    -->

    <android.support.v4.view.ViewPager
            android:id="@+id/scientific_graph_viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    </android.support.v4.view.ViewPager>

</android.support.v4.widget.SlidingPaneLayout>

The SlidingPaneLayout slides when I pull from the left edge; however, I can’t seem to get the ViewPager to slide when I pull from the right edge. When I pull from the right edge, it slides very little and then snaps back.

Is doing this even possible? Is there a better way to do this?

I found that by moving my finger up and the to the left, I can swipe the view pager.

How to&Answers:

The root cause is the implementation of #onInterceptTouchEvent. An older implementation of SlidingPaneLayout made a call to #canScroll, which would check if the touch target could scroll, and if so, would scroll the touch target instead of sliding the panel. The most recent implementation looks like it always intercepts the motion event, once the drag threshold exceeds the slop, except in the case where the X drag exceeds the slop and the Y drag exceeds the X drag (as noted by the OP).

One solution to this is to copy SlidingPaneLayout and make a few changes to get this to work. Those changes are:

  1. Modify the ACTION_MOVE case in #onInterceptTouchEvent to also check #canScroll,

    if (adx > slop && ady > adx || 
        canScroll(this, false, Math.round(x - mInitialMotionX), Math.round(x), Math.round(y)))
    { ... }
    
  2. Modify the final check in #canScroll to special case ViewPager. This modification could also be done in a subclass by overriding #canScroll, since it doesn’t access any private state.

    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        ...
        /* special case ViewPagers, which don't properly implement the scrolling interface */
        return checkV && (ViewCompat.canScrollHorizontally(v, -dx) ||
            ((v instanceof ViewPager) && canViewPagerScrollHorizontally((ViewPager) v, -dx)))
    }
    
    boolean canViewPagerScrollHorizontally(ViewPager p, int dx) {
        return !(dx < 0 && p.getCurrentItem() <= 0 ||
            0 < dx && p.getAdapter().getCount() - 1 <= p.getCurrentItem());       
    }
    

There is likely a more elegant way to do this by fixing the ViewDragHelper, but this is something Google should address in a future update of the support package. The hacks above should get the layout working with ViewPagers (and other horizontally scrolling containers?) now.

Answer:

Building off of @Brien Colwell’s solution, I’ve written a custom subclass of SlidingPaneLayout that handles this for you, and also adds edge swiping so that when the user scrolls far to the right, they don’t have to scroll all the way back to the left in order to open the pane.

Since this is a subclass of SlidingPaneLayout, you don’t need to change any of your references in Java, just make sure that you instantiate an instance of this class (usually in your XML).

package com.ryanharter.android.view;

import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.widget.SlidingPaneLayout;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

/**
 * SlidingPaneLayout that, if closed, checks if children can scroll before it intercepts
 * touch events.  This allows it to contain horizontally scrollable children without
 * intercepting all of their touches.
 *
 * To handle cases where the user is scrolled very far to the right, but should still be
 * able to open the pane without the need to scroll all the way back to the start, this
 * view also adds edge touch detection, so it will intercept edge swipes to open the pane.
 */
public class PagerEnabledSlidingPaneLayout extends SlidingPaneLayout {

    private float mInitialMotionX;
    private float mInitialMotionY;
    private float mEdgeSlop;

    public PagerEnabledSlidingPaneLayout(Context context) {
        this(context, null);
    }

    public PagerEnabledSlidingPaneLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PagerEnabledSlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        ViewConfiguration config = ViewConfiguration.get(context);
        mEdgeSlop = config.getScaledEdgeSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_DOWN: {
                mInitialMotionX = ev.getX();
                mInitialMotionY = ev.getY();
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final float x = ev.getX();
                final float y = ev.getY();
                // The user should always be able to "close" the pane, so we only check
                // for child scrollability if the pane is currently closed.
                if (mInitialMotionX > mEdgeSlop && !isOpen() && canScroll(this, false,
                        Math.round(x - mInitialMotionX), Math.round(x), Math.round(y))) {

                    // How do we set super.mIsUnableToDrag = true?

                    // send the parent a cancel event
                    MotionEvent cancelEvent = MotionEvent.obtain(ev);
                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                    return super.onInterceptTouchEvent(cancelEvent);
                }
            }
        }

        return super.onInterceptTouchEvent(ev);
    }
}