Skip to content

Instantly share code, notes, and snippets.

@davidliu
Last active January 25, 2020 19:33
Show Gist options
  • Save davidliu/c246a717f00494a6ad237a592a3cea4f to your computer and use it in GitHub Desktop.
Save davidliu/c246a717f00494a6ad237a592a3cea4f to your computer and use it in GitHub Desktop.
public class DelegatingLayout extends FrameLayout {
private boolean mIsDelegating;
private ViewGroup mDelegateView;
private int[] mOriginalOffset = new int[2];
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Clear delegating flag on touch start/end
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDelegating = false;
mOriginalOffset[0] = 0;
mOriginalOffset[1] = 0;
}
// If we're delegating, send all events to the delegate
if (mIsDelegating) {
// Offset location in case the delegate view has shifted since we last fed it a motion event.
ev.offsetLocation(mOriginalOffset[0] - mDelegateView.getLeft(), mOriginalOffset[1] - mDelegateView.getTop());
return mDelegateView.dispatchTouchEvent(ev);
}
// Check if the delegate wants to steal touches.
mIsDelegating = mDelegateView.onInterceptTouchEvent(ev);
if (mIsDelegating) {
// If delegate has stolen, we should cancel any touch handling in our own view.
MotionEvent cancel = MotionEvent.obtain(ev);
cancel.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(cancel);
cancel.recycle();
// May be more complicated to handle other edge cases
// (i.e. non shared parent view descendants, translation, etc.)
//
// I would use getLocationInWindow to cover more cases, but
// the current issue only touches getTop() so this simple efficient solution is fine.
mOriginalOffset[0] = mDelegateView.getLeft();
mOriginalOffset[1] = mDelegateView.getTop();
// Send the touch event to the delegate
return mDelegateView.onTouchEvent(ev);
}
// No delegation, handle like usual.
return super.dispatchTouchEvent(ev);
}
}
@ParticleCore
Copy link

Yeah, I was just pointing out the impossibility for me to diagnose your issue without knowing your exact setup. A CoordinatorLayout-based bottom sheet approach works completely different from something that uses a RecyclerView as a bottom sheet (and I'm not meaning attaching a BottomSheetBehavior to a RecyclerView, but rather using a RecyclerView set up to look like a bottom sheet, with no CoordinatorLayout involved).

With a CoordinatorLayout and BottomSheetBehavior on a FrameLayout, the FrameLayout isn't involved at all in terms of the touches. The scrolling behavior is all handled at the CoordinatorLayout level (which speaks directly with the BottomSheetBehavior), and the touch event never reaches the FrameLayout. Delegating touches to the FrameLayout won't do anything, because the FrameLayout doesn't know anything about how to scroll, which means the delegating touches approach won't work at all here.

On the flip side, the only real problem is that BottomSheetBehavior determines whether to intercept scrolling by whether the touch was initiated inside the target view's bounds.

Subclassing CoordinatorLayout and overriding isPointInChildBounds to always return true for the target view will do the trick, though to really mimic DelegateLayout's intentions (allow user to tap within the DelegateLayout, but scrolls will delegate to the target view), you'd have to override BottomSheetBehavior to also only intercept after scrolling a certain amount. If you don't need this second part, and your only intention is to be able to scroll off the bottom sheet from anywhere, I'd suggest not bothering with any of this and use the more appropriate BottomSheetDialogFragment.

Much appreciate it. The isPointInChildBounds suggestion (along with the other requirements) is doing the trick just fine, the rest will be a matter of adjusting to each need. Thanks a lot for the great information, this was extremely useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment