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.