Skip to content

Instantly share code, notes, and snippets.

@davidliu
Last active January 25, 2020 19:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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

This appears to only work when the delegate view is a RecyclerView, otherwise L30 always returns false.
Works perfectly just for that case, but if I want to use a FrameLayout - for example - then it will not work.
Do you know any work around for this?

@davidliu
Copy link
Author

Have your delegate view subclass FrameLayout and override onInterceptTouch. Or whatever switch you want to turn it on. It's not that delicate, modify as needed.

@ParticleCore
Copy link

ParticleCore commented Jan 24, 2020

Have your delegate view subclass FrameLayout and override onInterceptTouch. Or whatever switch you want to turn it on. It's not that delicate, modify as needed.

Oh I see, I thought this was using each view's delegation intentionally, but it just happened to work with RecyclerView directly, however if other views are used then they need to be customized in order for this to work. Thanks for the quick explanation.

PS: If you happen to have any working example available somewhere I'd really appreciate if you could share.

@davidliu
Copy link
Author

What's the specific usecase you're thinking of? This example specifically works with RecyclerView (and any scrolling type view) because:

  1. They actually implement onInterceptTouch.
  2. Those views don't rely on the touches actually being inside their bounds to make sense (i.e. scrolls only care about relative movement, as opposed to directly tapping on a specific button or something).

This view was pretty specifically custom made to fit a specific use case, so unless I know exactly what you're doing, I can't recommend using this gist as anything other than something to learn from.

@ParticleCore
Copy link

ParticleCore commented Jan 25, 2020

What's the specific usecase you're thinking of? This example specifically works with RecyclerView (and any scrolling type view) because:

  1. They actually implement onInterceptTouch.

  2. Those views don't rely on the touches actually being inside their bounds to make sense (i.e. scrolls only care about relative movement, as opposed to directly tapping on a specific button or something).

This view was pretty specifically custom made to fit a specific use case, so unless I know exactly what you're doing, I can't recommend using this gist as anything other than something to learn from.

The specific use case is that of a BottomSheet not containing a scrollable list structure - or at least not scrollable lik a RecyclerView -, just plain data that may or may not occupy more than the available vertical space, a LinearLayout for example. Maybe even a CoordinatorLayout containing a sort of header at the top and a RecyclerView right below it.

I did try using a ScrollView in place of the RecyclerView, but it did not work. In fact, so far no ViewGroup layout I tried worked except for the RecyclerView.

Been struggling trying to subclass each one in order to get it to work with their onInterceptTouch to no avail, it is always ignored.

@davidliu
Copy link
Author

Which BottomSheet? There isn't a specific BottomSheet class AFAIK, so you're going to have to specify the exact class you're talking about.

@ParticleCore
Copy link

Which BottomSheet? There isn't a specific BottomSheet class AFAIK, so you're going to have to specify the exact class you're talking about.

Apologies, I was referring to "A" bottom sheet that is either a linear layout or a coordinator layout or a frame layout, I was not referring to a BottomSheet view specifically.

@davidliu
Copy link
Author

The thing is that the bottom sheet design inherently requires something scrollable in concept (for you to pull it up/down). The sheet itself might be static, but there will be something scrollable. The key is to have the delegate layout delegate to the thing that's scrollable. For my use case, that was a RecyclerView.

@ParticleCore
Copy link

The thing is that the bottom sheet design inherently requires something scrollable in concept (for you to pull it up/down). The sheet itself might be static, but there will be something scrollable. The key is to have the delegate layout delegate to the thing that's scrollable. For my use case, that was a RecyclerView.

Not really, you can use an empty FrameLayout, set it to peek to about 60dp and you can easily slide it up and down, as long as the touch is initiated on that view already. It has nothing scrollable inside and it still pulls up and down as usual. I'll have to see how to do it for my case since it differs from yours, which is a shame because yours is very elegant and works really well. Thanks for the explanations

@davidliu
Copy link
Author

A FrameLayout by itself doesn't scroll. A CoordinatorLayout with a child FrameLayout with a BottomSheetBehavior is a wholly different beast.

This is why I'm trying to get you to be very specific with what your setup is, as the solution will vary, and I still don't know what approach you want.

@ParticleCore
Copy link

A FrameLayout by itself doesn't scroll. A CoordinatorLayout with a child FrameLayout with a BottomSheetBehavior is a wholly different beast.

This is why I'm trying to get you to be very specific with what your setup is, as the solution will vary, and I still don't know what approach you want.

I understand the frame layout does not scroll by itself, but it can be used as a bottom sheet and works correctly as one, as long as the touch interactions are initiated inside it, same goes for any view used as a bottom sheet that is not a recycler view. Those were all examples of possible ways to create a bottom sheet with a simple hierarchy and a complex hierarchy. I am just trying to get this to work with a simple frame layout.

Something as simple as this:

<CoordinatorLayout...>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/red"
        app:behavior_peekHeight="0dp"
        app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />
    <DelegatingLayout...>
        <some views here>
    </DelegatingLayout>
</CoordinatorLayout>

@davidliu
Copy link
Author

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.

@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