Allow Bottomsheet To Slide Up After Threshold Is Reached On An Area Outside
Solution 1:
Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
The basic logic is to handle touch events in onInterceptTouchEvent
in a custom BottomSheetBehavior
and check in a CoordinatorLayout
if the given view (from now on named proxy view
) is of interest for the rest of the touch delegation in isPointInChildBounds
.
This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.
Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><com.example.tabsheet.CustomCoordinatorLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/customCoordinatorLayout"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><com.google.android.material.tabs.TabLayoutandroid:id="@+id/tabLayout"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="bottom"android:background="@android:color/darker_gray"><com.google.android.material.tabs.TabItemandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:icon="@drawable/ic_launcher_background"android:text="Tab 1" /><com.google.android.material.tabs.TabItemandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:icon="@drawable/ic_launcher_background"android:text="Tab 2" /><com.google.android.material.tabs.TabItemandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:icon="@drawable/ic_launcher_background"android:text="Tab 3" /><com.google.android.material.tabs.TabItemandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:icon="@drawable/ic_launcher_background"android:text="Tab 4" /><com.google.android.material.tabs.TabItemandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:icon="@drawable/ic_launcher_background"android:text="Tab 5" /></com.google.android.material.tabs.TabLayout><androidx.coordinatorlayout.widget.CoordinatorLayoutandroid:id="@+id/bottomSheet"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#3F51B5"android:clipToPadding="false"app:behavior_peekHeight="0dp"app:layout_behavior=".CustomBottomSheetBehavior" /></com.example.tabsheet.CustomCoordinatorLayout>
MainActivity.java
publicclassMainActivityextendsAppCompatActivity {
@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {
final CustomCoordinatorLayout customCoordinatorLayout;
final CoordinatorLayout bottomSheet;
final TabLayout tabLayout;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
bottomSheet = findViewById(R.id.bottomSheet);
tabLayout = findViewById(R.id.tabLayout);
iniList(bottomSheet);
customCoordinatorLayout.setProxyView(tabLayout);
}
privatevoidiniList(final ViewGroup parent) {
@ColorIntint backgroundColor;
finalint padding;
finalint maxItems;
finalfloat density;
final NestedScrollView nestedScrollView;
final LinearLayout linearLayout;
final ColorDrawable dividerDrawable;
int i;
TextView textView;
ViewGroup.LayoutParams layoutParams;
density = Resources.getSystem().getDisplayMetrics().density;
padding = (int) (20 * density);
maxItems = 50;
backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
dividerDrawable = newColorDrawable(Color.WHITE);
layoutParams = newViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = newNestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = newLinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i++) {
textView = newTextView(this);
textView.setText("Item " + (1 + i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
publicclassCustomCoordinatorLayoutextendsCoordinatorLayout {
private View proxyView;
publicCustomCoordinatorLayout(@NonNull Context context) {
super(context);
}
publicCustomCoordinatorLayout(
@NonNull Context context,
@Nullable AttributeSet attrs
) {
super(context, attrs);
}
publicCustomCoordinatorLayout(
@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
@OverridepublicbooleanisPointInChildBounds(
@NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
returntrue;
}
// we want to intercept touch events if they are// within the proxy view bounds, for this reason// we instruct the coordinator layout to check// if this is true and let the touch delegation// respond to that resultif (proxyView != null) {
returnsuper.isPointInChildBounds(proxyView, x, y);
}
returnfalse;
}
// for this example we are only interested in intercepting// touch events for a single view, if more are needed use// a List<View> viewList instead and iterate in // isPointInChildBoundspublicvoidsetProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
publicclassCustomBottomSheetBehavior<V extendsView> extendsBottomSheetBehavior<V> {
// we'll use the device's touch slop value to find out when a tap// becomes a scroll by checking how far the finger moved to be// considered a scroll. if the finger moves more than the touch// slop then it's a scroll, otherwise it is just a tap and we// ignore the touch eventsprivateint touchSlop;
privatefloat initialY;
privateboolean ignoreUntilClose;
publicCustomBottomSheetBehavior(
@NonNull Context context,
@Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@OverridepublicbooleanonInterceptTouchEvent(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull MotionEvent event
) {
// touch events are ignored if the bottom sheet is already// open and we save that state for further processingif (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
returnsuper.onInterceptTouchEvent(parent, child, event);
}
switch (event.getAction()) {
// this is the first event we want to begin observing// so we set the initial value for further processing// as a positive value to make things easiercase MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
returnsuper.onInterceptTouchEvent(parent, child, event);
// if the last bottom sheet state was not open then// we check if the current finger movement has exceed// the touch slop in which case we return true to tell// the system we are consuming the touch event// otherwise we let the default handling behavior// since we don't care about the direction of the// movement we ensure its difference is a positive// integer to simplify the condition checkcase MotionEvent.ACTION_MOVE:
return !ignoreUntilClose
&& Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
|| super.onInterceptTouchEvent(parent, child, event);
// once the tap or movement is completed we reset// the initial values to restore normal behaviorcase MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
returnsuper.onInterceptTouchEvent(parent, child, event);
}
returnsuper.onInterceptTouchEvent(parent, child, event);
}
}
Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.
Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView
for example) that can be used as is by the CoordinatorLayout
, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.
Solution 2:
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<FrameLayoutid="+id/bottomSheet"><Viewid="exploreNearby bottomMargin="buttonContainerHeight/><LinearLayout><Buttonid="explore"/><Buttonid="explore"/><Buttonid="explore"/></LinearLayout><Viewwidth="match"height="match"id="+id/touchCatcher"
</FrameLayout>
Add a gesture detector on the bottomSheet view on override onTouch(). which uses SimpleOnGestureListener
to wait for a "scroll" events - everything but a scroll event you can replicate down through to the view as normal.
On a scroll event you can grow your exploreNearby as a delta (make sure it doesn't recurse or go to high or too low).
Solution 3:
The Bottom sheet class will already do this for you. Just set it's peek height to 0 and it should already listen for the slide up gesture.
However, I'm not positive it will work with a peek height of 0. So if that doesn't work, simply put a peek height of 20dp and make the top portion of the bottom sheet layout transparent so it is not visible.
That should do the trick for ya, unless I'm misunderstanding your question. If your goal is to simply be able to tap at the bottom and slide upwards bringing up the bottom sheet that should be pretty straight forward.
The one possible issue that you "could" encounter is if the bottom sheet doesn't receive the touch events due to the button already consuming it. If this happens you will need to create a touch handler for the whole screen and return "true" that you are handling it each time, then simply forward the touch events to the underlying view, so when you get above the threshold of your bottom tab bar you start sending the touch events to the bottom sheet layout instead of the tab bar.
It sounds harder than it is. Most classes have an onTouch and you just forward it on. However, only go that route, if it doesn't work for you out of the box the way I described in the first two scenarios.
Lastly, one other option that might work is to create your tab buttons as part of the bottomSheetLayout and make the peek height equivalent of the tab bar. Then make sure the tab bar is constrained to bottomsheet parent bottom, so that when you swipe up it simply stays at the bottom. This would enable you to click the buttons or get the free bottom sheet behavior.
Happy Coding!
Post a Comment for "Allow Bottomsheet To Slide Up After Threshold Is Reached On An Area Outside"