Search code examples
androiduser-interfaceandroid-viewpagerhorizontalscrollview

Scrolling behaviour: HorizontalScrollView inside ViewPager


I have a View Pager (VP) which contains a Horizontal Scroll View (HSV). If the HSV reaches one of its edges or is not able to scroll at all, on a new swipe in the blocked direction VP should take over scrolling to the next page. I hesitated to ask this question because I found similar ones like these:

Can I use Horizontal Scrollview Inside a Viewpager in Android?

or

horizontalscrollview inside viewpager

But the solution did not work for me. 'v instanceof HorizontalScrollView' gets true but viewPager does not scroll

Any other ideas how to achieve the desired behaviour?

public class MyViewPager extends ViewPager {

public MyViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
}
// Update 1
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
    //return super.onInterceptTouchEvent(ev);
}

/**
 * https://stackoverflow.com/questions/22781496/can-i-use-horizontal-scrollview-inside-a-viewpager-in-android
 */
@Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    if (v instanceof HorizontalScrollView) {
        return true;
    }
    return super.canScroll(v, checkV, dx, x, y);
}

}

child view: view_pager_page.xml:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center">
            <HorizontalScrollView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center">

                    <include layout="@layout/" />
                </LinearLayout>
            </HorizontalScrollView>
        </LinearLayout>
    </FrameLayout>
</RelativeLayout>

parent view: view_pager.xml

<android.support.design.widget.CoordinatorLayout>
...
<LinearLayout>
<packagepath.MyViewPager
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">
</packagepath.MyViewPager>
</LinearLayout>
...
<android.support.design.widget.CoordinatorLayout>

Update 1: When overriding 'onInterceptTouchEvent' and let it always return true VP scrolls, but HSV doesn't. I think this must return true only if HSV reaches edges right? How can I figure out in this method if it is the case?

Update 2: I reconstructed the touch event mechanism of android hoping to get some insight of how to intercept the motion event flow. E.g. in HSV I can simply return false to let VP consume this and all subsequent motion events. Unfortunately I need two motion events of type MotionEvent.MOVE to decide if HSV or VP should scroll when reaching an edge (if HSV has reached right edge, a right swipe scrolls HSV back and a left swipe scrolls to next page of VP). But if I skip the MotionEvent.DOWN action neither HSV or VP starts scrolling... so hard to solve. Any ideas?

Touchevent Mechanism in Android

(Warning: Graphic is not complete and will contain mistakes, everyone is invited to correct it :-))

Update 3: Finally I got it working. Understanding the Touchevent mechanism helped a lot and also the first comment of ZeroOne. I will post my solution when I have time for it.


Solution

  • 1.Extend ViewPager Class:

    public class ViewPagerContainingHorizontalScrollView extends ViewPager {
    private Float x_old;
    private boolean bDoIntercept = false;
    private boolean bHsvRightEdge = false;
    private boolean bHsvLeftEdge = true;
    
    
    public ViewPagerContainingHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    private float calculateDistanceSwipe(MotionEvent ev){
        float distance = 0;
        if (x_old == null) {
            x_old = ev.getX();
        } else {
            distance = ev.getX() - x_old;
            x_old = null;
        }
        return distance;
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDoIntercept = false;
        if(ev.getAction() == MotionEvent.ACTION_MOVE) {
            float distance = calculateDistanceSwipe(ev);
    
            if (distance < 0) {//scrolling left direction
                if (bHsvRightEdge) { //HSV right edge
                    bDoIntercept = true;
                    //When scrolling slow VP may not switch page.
                    //Then HSV snaps back into old position.
                    //To allow HSV to scroll into non blocked direction set following to false.
                    bHsvRightEdge = false;
                }
                bHsvLeftEdge = false;//scrolling left means left edge not reached
            } else if (distance > 0) {//scrolling right direction
                if (bHsvLeftEdge) { //HSV left edge
                    bDoIntercept = true;
                    //When scrolling slow VP may not switch page.
                    //Then HSV snaps back into old position.
                    //To allow HSV to scroll into non blocked direction set following to false.
                    bHsvLeftEdge = false;
                }
                bHsvRightEdge = false;//scrolling right means right edge not reached
            }
        }
        return super.dispatchTouchEvent(ev);
    }
    
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(bDoIntercept){
            return true;
        }else{
            return super.onInterceptTouchEvent(ev);
        }
    }
    
    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof HorizontalScrollView) {
            HorizontalScrollView hsv = (HorizontalScrollView) v;
            int max_scrollX = hsv.getChildAt(0).getWidth() - hsv.getWidth();
            int min_scrollX = 0;
            int current_scroll_x = hsv.getScrollX();
    
            if (current_scroll_x == max_scrollX) { //HSV right edge
                bHsvRightEdge = true;
            }
    
            if (current_scroll_x == min_scrollX) { //HSV left edge
                bHsvLeftEdge = true;
            }
            return true;
        }
        return super.canScroll(v, checkV, dx, x, y);
    }
    }
    
    1. Use this custom VP in XML.
    2. Enjoy nested HSV scrolling in VP :-)

    Touch Event Mechanism Overview for this specific case