SlidingUpPanelLayout and ScrollView

user1796624 picture user1796624 · Mar 18, 2014 · Viewed 12.3k times · Source

I have a SlidingUpPanelLayout that holds a image as a top view, and a view pager that needs to slide. The viewpager has 3 fragments and two of them are list views. So I want to be able to expand the view pager on pulling up, and once the view pager is up I want to be able to scroll the scrollviews inside the fragments. But when pulling down on the scrollview in case there is no more to scroll, I want to start collapsing the viewpager. So please suggest how to make the SlidingUpPanelLayout collapse on pulling the scrollview in case there is no more contents to scroll?

Here I post some of my code: I have tried capture the touch events and overwrite the SlidingUpPanel onInterceptTouchEvent function in the following way:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (isHandled) {
        Log.i("interceptToch", "HEREEE");
        return onTouchEvent(ev);
    }
    return false;
}

So when the SlidingUpPanelLayout is expanded I set isHandled = false. So when the slidingUpPanelLayout expands, all touch events are passed to its child views.

And I also put onTouchEvent in the scrollView, in-order to unblock the SlidingUpPanelLayout.onInterceptTouchEvent:

public boolean onTouch(View v, MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        scroll = 0;
        y = event.getY();
    } else if (action == MotionEvent.ACTION_MOVE) {
        if (scroll_view_summary.getScrollY() == 0 && event.getY() > y) {
            scroll = scroll + 1;
            if (scroll > 2) {
                // the user has pulled the list and the slidingUpPanelLauout 
                // should be able to handle the toch events again
                SlidingUpPanelLayoutCustom las = 
                    ((SaleDetailsActivity) getActivity()).getLayout();
                las.setHandle(true);
                scroll = 0;
                return false;
            }
        }
    }
    return false;
}

But this is not working. The problem is that once the scrollview.onTouch event is in MotionEvent.ACTION_MOVE SlidingUpPanelLayout.onInterceptTouchEvent is not called. SlidingUpPanelLayout.onInterceptTouchEvent is called after MotionEvent.ACTION_CANCEL. This means that the event can't be passed to the SlidingUpPanelLayout and the panel can't collapse.

Answer

themarshal picture themarshal · Mar 28, 2014

Unfortunately you can't rely on SlidingUpPanelLayout's onInterceptTouchEvent method for the aforementioned reasons. Once a child view's onTouchEvent method returns true, onInterceptTouchEvent is no longer called.

My solution is a bit convoluted, but it allows you to achieve exactly what (I think) you're looking for. A single touch/drag event will drag the panel into place and, once in place, continue scrolling the child view. Likewise when dragging down, a single touch/drag event can scroll the child view and, once completely scrolled, will begin dragging the panel down.

Updated 2015-04-12 Updated to version 3.0.0 of the SlidingUpPanelLayout code. Also accounting for ListViews instead of just ScrollViews.

1) In the res/ folder of SlidingUpPanel's library project, open the attrs.xml and add

<attr name="scrollView" format="reference" />

You'll use this to identify a single child view that will usurp the touch event once the panel has been dragged into position. In your layout xml file, you can then add

sothree:scrollView="@+id/myScrollView"

Or whatever the ID of your scrollView is. Also make sure that you do not declare a sothree:dragView ID, so the entire view is draggable.

The rest of the steps are all done within SlidingUpPanelLayout.java...

2) Declare the following variables:

View mScrollView;
int mScrollViewResId = -1;
boolean isChildHandlingTouch = false;
float mPrevMotionX;
float mPrevMotionY;

3) In the constructor, just after mDragViewResId is set, add the following line:

mScrollViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_scrollView, -1);

4) In onFinishInflate, add the following code:

if (mScrollViewResId != -1) {
    mScrollView = findViewById(mScrollViewResId);
}

5) Add the following method:

private boolean isScrollViewUnder(int x, int y) {
    if (mScrollView == null)
        return false;

    int[] viewLocation = new int[2];
    mScrollView.getLocationOnScreen(viewLocation);
    int[] parentLocation = new int[2];
    this.getLocationOnScreen(parentLocation);
    int screenX = parentLocation[0] + x;
    int screenY = parentLocation[1] + y;
    return screenX >= viewLocation[0] && 
           screenX < viewLocation[0] + mScrollView.getWidth() && 
           screenY >= viewLocation[1] && 
           screenY < viewLocation[1] + mScrollView.getHeight();
}

6) Remove onInterceptTouchEvent.

7) Modify onTouchEvent to the following:

public boolean onTouchEvent(MotionEvent ev) {
    if (!isEnabled() || !isTouchEnabled()) {
        return super.onTouchEvent(ev);
    }
    try {
        mDragHelper.processTouchEvent(ev);

        final int action = ev.getAction();
        boolean wantTouchEvents = false;

        switch (action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_UP: {
                final float x = ev.getX();
                final float y = ev.getY();
                final float dx = x - mInitialMotionX;
                final float dy = y - mInitialMotionY;
                final int slop = mDragHelper.getTouchSlop();
                View dragView = mDragView != null ? mDragView : mSlideableView;

                if (dx * dx + dy * dy < slop * slop &&
                        isDragViewUnder((int) x, (int) y) &&
                        !isScrollViewUnder((int) x, (int) y)) {
                    dragView.playSoundEffect(SoundEffectConstants.CLICK);

                    if ((PanelState.EXPANDED != mSlideState) && (PanelState.ANCHORED != mSlideState)) {
                        setPanelState(PanelState.ANCHORED);
                    } else {
                        setPanelState(PanelState.COLLAPSED);
                    }
                    break;
                }
                break;
            }
        }

        return wantTouchEvents;
    } catch (Exception ex) {
        ex.printStackTrace();
        return false;
    }
}

8) Add the following method:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // Identify if we want to handle the touch event in this class.
    // We do this here because we want to be able to handle the case
    // where a child begins handling a touch event, but then the
    // parent takes over. If we rely on onInterceptTouchEvent, we
    // lose control of the touch as soon as the child handles the event.
    if (mScrollView == null)
        return super.dispatchTouchEvent(ev);

    final int action = MotionEventCompat.getActionMasked(ev);

    final float x = ev.getX();
    final float y = ev.getY();

    if (action == MotionEvent.ACTION_DOWN) {
        // Go ahead and have the drag helper attempt to intercept
        // the touch event. If it won't be dragging, we'll cancel it later.
        mDragHelper.shouldInterceptTouchEvent(ev);

        mInitialMotionX = mPrevMotionX = x;
        mInitialMotionY = mPrevMotionY = y;

        isChildHandlingTouch = false;
    } else if (action == MotionEvent.ACTION_MOVE) {
        float dx = x - mPrevMotionX;
        float dy = y - mPrevMotionY;
        mPrevMotionX = x;
        mPrevMotionY = y;

        // If the scroll view isn't under the touch, pass the
        // event along to the dragView.
        if (!isScrollViewUnder((int) x, (int) y))
            return this.onTouchEvent(ev);

        // Which direction (up or down) is the drag moving?
        if (dy > 0) { // DOWN
            // Is the child less than fully scrolled?
            // Then let the child handle it.
            if (isScrollViewScrolling()) {
                isChildHandlingTouch = true;
                return super.dispatchTouchEvent(ev);
            }

            // Was the child handling the touch previously?
            // Then we need to rejigger things so that the
            // drag panel gets a proper down event.
            if (isChildHandlingTouch) {
                // Send an 'UP' event to the child.
                MotionEvent up = MotionEvent.obtain(ev);
                up.setAction(MotionEvent.ACTION_UP);
                super.dispatchTouchEvent(up);
                up.recycle();

                // Send a 'DOWN' event to the panel. (We'll cheat
                // and hijack this one)
                ev.setAction(MotionEvent.ACTION_DOWN);
            }

            isChildHandlingTouch = false;
            return this.onTouchEvent(ev);
        } else if (dy < 0) { // UP
            // Is the panel less than fully expanded?
            // Then we'll handle the drag here.
            if (mSlideOffset < 1.0f) {
                isChildHandlingTouch = false;
                return this.onTouchEvent(ev);
            }

            // Was the panel handling the touch previously?
            // Then we need to rejigger things so that the
            // child gets a proper down event.
            if (!isChildHandlingTouch) {
                mDragHelper.cancel();
                ev.setAction(MotionEvent.ACTION_DOWN);
            }

            isChildHandlingTouch = true;
            return super.dispatchTouchEvent(ev);
        }
    } else if ((action == MotionEvent.ACTION_CANCEL) ||
            (action == MotionEvent.ACTION_UP)) {
        if (!isChildHandlingTouch) {
            final float dx = x - mInitialMotionX;
            final float dy = y - mInitialMotionY;
            final int slop = mDragHelper.getTouchSlop();

            if ((mIsUsingDragViewTouchEvents) && (dx * dx + dy * dy < slop * slop))
                return super.dispatchTouchEvent(ev);

            return this.onTouchEvent(ev);
        }
    }

    // In all other cases, just let the default behavior take over.
    return super.dispatchTouchEvent(ev);
}

9) Add the following method to determine whether the scrollView is still scrolling. Handles cases for both ScrollView and ListView:

/**
 * Computes the scroll position of the the scrollView, if set.
 * @return
 */
private boolean isScrollViewScrolling() {
    if (mScrollView == null)
        return false;

    // ScrollViews are scrolling when getScrollY() is a value greater than 0.
    if (mScrollView instanceof ScrollView) {
        return (mScrollView.getScrollY() > 0);
    }
    // ListViews are scrolling if the first child is not displayed, or if the first child has an offset > 0
    else if (mScrollView instanceof ListView) {
        ListView lv = (ListView) mScrollView;

        if (lv.getFirstVisiblePosition() > 0)
            return true;

        View v = lv.getChildAt(0);
        int top = (v == null) ? (0) : (-v.getTop() + lv.getFirstVisiblePosition() * lv.getHeight());
        return top > 0;
    }

    return false;
}

10) (Optional) Add the following method to allow you to set the scrollView at runtime (i.e. You want to put a fragment in the panel, and the fragment's child has a ScrollView/ListView you want to scroll):

public void setScrollView(View scrollView) {
    mScrollView = scrollView;
}

We're now completely managing the handling of the touch event from within this class. If we're dragging the panel up and it slides fully into place, we cancel the drag and then spoof a new touch in the mScrollView child. If we're scrolling the child and reach the top, we spoof an "up" event in the child and spoof a new touch for the drag. This also allows tap events on other child widgets.

Known Issues The "up"/"down" events that we're spoofing can unintentionally trigger a click event on a child element of the scrollView.