SlidingUpPanelLayout and ScrollView

2019-01-14 04:50发布

问题:

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.

回答1:

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.



回答2:

I had the same issue but at my app there is ListView instead of ScrollView. I couldn't apply themarshal's answer to work for my problem. But I have found solution on the basis of themarshal's, Chris's answers and Maria Sakharova's comments

First I couldn't find variables mCanSlide and mIsSlidingEnabled and methods expandPane(mAnchorPoint) and collapsePane() so I use next code:

@Override
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 (!isExpanded() && !isAnchored()) {
                        //expandPane(mAnchorPoint);
                        setPanelState(PanelState.ANCHORED);
                    } else {
                        //collapsePane();
                        setPanelState(PanelState.COLLAPSED);
                    }
                    break;
                }
                break;
            }
        }

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

try/catch is needed because of exception raises when apply two fingers.

Second Chris's answers is obligatory to fulfill.

And then because of ListView's method getScrollY() always returns zero I change slightly code at method dispatchTouchEvent(MotionEvent ev):

this:

if (mScrollView.getScrollY() > 0) {
   isChildHandlingTouch = true;
   return super.dispatchTouchEvent(ev);
}

to:

if (((ListView)mScrollView).getFirstVisiblePosition() > 0 ||             getFirstChildTopOffset((ListView) mScrollView) > 0){
   isChildHandlingTouch = true;
   return super.dispatchTouchEvent(ev);
} 

//at some other place in class SlidingUpPanelLayout 
public int getFirstChildTopOffset(ListView list){
    View v = list.getChildAt(0);
    int top = (v == null) ? 0 : (list.getPaddingTop() - v.getTop());
    return top;
}

Also my app has Google Map as main content and it also must get MotionEvent so as Maria Sakharova said we must return this.onTouchEvent(ev) || super.dispatchTouchEvent(ev) instead of this.onTouchEvent(ev) at two places. We must change this code:

if (!isScrollViewUnder((int) x, (int) y))
   return this.onTouchEvent(ev);

to:

if (!isScrollViewUnder((int) x, (int) y))
   return this.onTouchEvent(ev) || super.dispatchTouchEvent(ev);

in this case super.dispatchTouchEvent(ev) is needed if main content must get MotionEvent.

And second code:

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

to:

} 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) || super.dispatchTouchEvent(ev);
    }
}

in this case super.dispatchTouchEvent(ev) is needed to able to expand panel.

In summary method dispatchTouchEvent(MotionEvent ev) will be the next:

@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);
            return this.onTouchEvent(ev) || super.dispatchTouchEvent(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 (mScrollView.getScrollY() > 0) {
            if (((ListView)mScrollView).getFirstVisiblePosition() > 0 || getFirstChildTopOffset((ListView) mScrollView) > 0){
                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 > 0.0f) {
            if (mSlideOffset < 1.0f) {
                isChildHandlingTouch = false;
                return this.onTouchEvent(ev);
                //return this.onTouchEvent(ev) || super.dispatchTouchEvent(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);
            return this.onTouchEvent(ev) || super.dispatchTouchEvent(ev);
        }
    }

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


回答3:

Since 3.1.0, Umano SlidingUpPanelLayout support nested scrolling with ScrollView, ListView and RecyclerView out of the box.

In most of the cases, simply add the sothree:umanoScrollableView attribute in your XML layout file, as following :

<com.sothree.slidinguppanel.SlidingUpPanelLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:sothree="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sliding_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    sothree:umanoScrollableView="@+id/my_scrollable_view"
    android:gravity="bottom"
    sothree:umanoAnchorPoint="0.3"
    sothree:umanoPanelHeight="@dimen/bottom_playlist_height"
    sothree:umanoShadowHeight="4dp"
    android:paddingTop="?attr/actionBarSize">

For further information, look at this link : https://github.com/umano/AndroidSlidingUpPanel#scrollable-sliding-views



回答4:

In order for JJD's answer to work you need to add another step

8) add this method mScrollViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_scrollView, -1); in the constructor of the SlidingPanelLayout

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

      ...

    if (attrs != null) {
            ...

        if (ta != null) {

                   ...

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

                   ...
        }

        ta.recycle();
    }

}


回答5:

Just use setScrollableView!

Example:

public View onCreateView(
    @NonNull LayoutInflater inflater, 
    ViewGroup container,
    Bundle savedInstanceState) {
    ((SlidingUpPanelLayout)findViewById(R.id.view))
        .setScrollableView(findViewById(R.id.scrollable));
}

Scrollable view can be is RecyclerView, ListView, ScrollView etc.



回答6:

Wow! 4 years have passed! But the question is still relevant. At least for me. I found a solution in a small section of the code and without modifying the library. It works fine with ScrollView.

public class MySlidingUpPanelLayout extends SlidingUpPanelLayout {
    public void setScrollViewInside(final ScrollViewInsideSlidingUpPanelLayout scroll){
        this.addPanelSlideListener(new PanelSlideListener() {
            @Override public void onPanelSlide(View panel, float slideOffset) {}

            @Override
            public void onPanelStateChanged(View panel, PanelState previousState, PanelState newState) {
                if(scroll!=null) {
                    scroll.setScrollable(getPanelState() == PanelState.EXPANDED);
                }
            }
        });
    }
}

public class ScrollViewInsideSlidingUpPanelLayout extends ScrollView {
    private boolean scrollable = false;
    public void setScrollable(boolean scrollable){
        this.scrollable = scrollable;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        if (getScrollY() == 0) {
            scrollable = false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(scrollable)
            return super.onTouchEvent(event);
        else
            return false;
    }
}

usage:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    sliding_layout.setScrollViewInside(scroll);
}