I want to open a Bottom Sheet (Deep Linking way) but inside of it instead of share options or just a layout, I want to have an activity with its layout or a fragment with its layout.
Known libraries that open Bottom Sheet Like Flipboard/BottomSheet can open layout, not whole activity.
Is there any possibility to achieve that with a Coordinator Layout?
I found a Google's Photo on Bottom Sheet Component Page that shows what exactly I have in mind. Google's description says:
The app on the right displays a bottom sheet containing content from the app on the left. This allows the user to view content from another app without leaving their current app.
I am not an expert, but after some research, I've found a simple way to do this. In your activity_main.xml first, make sure that your root layout is the android.support.design.widget.CoordinatorLayout.
Just inside that CoodrdinatorLayout add an include to your Bottom Sheet Layout:
<include layout="#layout/bottom_sheet_main" />
Then, and this is probably the most important step, you need to specify the behavior of the Bottom Sheet layout, so here is a sample code:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="vertical"
android:background="#FFFFFF"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_hideable="true"
app:behavior_peekHeight="64dp" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="#style/TextAppearance.AppCompat.Title"
android:padding="16dp"
android:text="BOTTOM SHEET" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="#style/TextAppearance.AppCompat.Body1"
android:padding="16dp"
android:text="Here goes text" />
</LinearLayout>
Okay, so that was all the XML code. Notice that we applied an app:layout_behavior so that it has the properties we want. Another important thing is to specify app:behavior_hideable="true" if we want to have the option of hiding the whole layout. The attribute app:behavior_peekHeight="64dp" means that the view will be 64dp high when it is collapsed (but not hidden).
There are 3 main stages of this view:
Expanded (STATE_EXPANDED): when the Bottom Sheet is completely open.
Collapsed (STATE_COLLAPSED): when the user only sees a small part from the top of the view. The attribute app:behavior_peekHeight determines this height.
Hidden(STATE_HIDDEN): When it is completely hidden (SURPRISE HAHA!).
We also have STATE_SETTLING and STATE_DRAGGING which are transitory, but they are not that important.
Now, if you run your app (you don't have to write any JAVA code) you will see that if you swipe up the title that will appear at the bottom of your layout, the Sheet will expand, and the same in the other way.
But you may notice that if you click on the Bottom Sheet, nothing happens. You can play with some Java code to manage the state of the Bottom Sheet:
Declare the view: LinearLayout bottomSheet = (LinearLayout)findViewById(R.id.bottomSheet);
Declare the behavior "manager":
final BottomSheetBehavior bsb = BottomSheetBehavior.from(bottomSheet);
And then you can get state changes:
bsb.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
String strNewState = "";
switch(newState) {
case BottomSheetBehavior.STATE_COLLAPSED:
strNewState = "STATE_COLLAPSED";
break;
case BottomSheetBehavior.STATE_EXPANDED:
strNewState = "STATE_EXPANDED";
break;
case BottomSheetBehavior.STATE_HIDDEN:
strNewState = "STATE_HIDDEN";
break;
case BottomSheetBehavior.STATE_DRAGGING:
strNewState = "STATE_DRAGGING";
break;
case BottomSheetBehavior.STATE_SETTLING:
strNewState = "STATE_SETTLING";
break;
}
Log.i("BottomSheets", "New state: " + strNewState);
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
Log.i("BottomSheets", "Offset: " + slideOffset);
}});
And there it is!
You can also use a Modal Bottom Sheet, which lets you create a Bottom Sheet as if it was a fragment.
For create that you need to have a BottomSheetDialogFragment from com.google.android.material library like this :
public class FragmentBottomSheetDialogFull extends BottomSheetDialogFragment {
private BottomSheetBehavior mBehavior;
private AppBarLayout app_bar_layout;
#NonNull
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
final View view = View.inflate(getContext(), R.layout.fragment_bottom_sheet_dialog_full, null);
dialog.setContentView(view);
mBehavior = BottomSheetBehavior.from((View) view.getParent());
mBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO);
mBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (BottomSheetBehavior.STATE_EXPANDED == newState) {
// View is expended
}
if (BottomSheetBehavior.STATE_COLLAPSED == newState) {
// View is collapsed
}
if (BottomSheetBehavior.STATE_HIDDEN == newState) {
dismiss();
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
return dialog;
}
#Override
public void onStart() {
super.onStart();
mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
then in your activity call that to open
// display sheet
FragmentBottomSheetDialogFull fragment = new FragmentBottomSheetDialogFull();
fragment.show(getSupportFragmentManager(), fragment.getTag());
Related
I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar.
Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.
I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.
How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?
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.CustomCoordinatorLayout
xmlns: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.TabLayout
android: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.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 1" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 2" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 3" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 4" />
<com.google.android.material.tabs.TabItem
android: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.CoordinatorLayout
android: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
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(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);
}
private void iniList(final ViewGroup parent) {
#ColorInt int backgroundColor;
final int padding;
final int maxItems;
final float 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 = new ColorDrawable(Color.WHITE);
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = new NestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = new LinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i++) {
textView = new TextView(this);
textView.setText("Item " + (1 + i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
public class CustomCoordinatorLayout extends CoordinatorLayout {
private View proxyView;
public CustomCoordinatorLayout(#NonNull Context context) {
super(context);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean isPointInChildBounds(
#NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
return true;
}
// 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 result
if (proxyView != null) {
return super.isPointInChildBounds(proxyView, x, y);
}
return false;
}
// 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
// isPointInChildBounds
public void setProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<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 events
private int touchSlop;
private float initialY;
private boolean ignoreUntilClose;
public CustomBottomSheetBehavior(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
#Override
public boolean onInterceptTouchEvent(
#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 processing
if (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
return super.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 easier
case MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
return super.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 check
case 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 behavior
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
return super.onInterceptTouchEvent(parent, child, event);
}
return super.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.
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<FrameLayout id="+id/bottomSheet">
<View id="exploreNearby bottomMargin="buttonContainerHeight/>
<LinearLayout>
<Button id="explore"/>
<Button id="explore"/>
<Button id="explore"/>
</LinearLayout>
<View width="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).
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!
In my application I use a bottom sheet (from the support library) which works great. Now I would like to animate a layout change while the sheet is dragged up. For this I have created a subclass of BottomSheetCallback (this is normaly an inner class of a Fragment so not all objects used in this calss are initialized here):
public class MyBehavior extends BottomSheetBehavior.BottomSheetCallback {
Transition transition;
float lastOffset = 0;
Scene scene;
public PlayerBehavior() {
TransitionInflater inflater = TransitionInflater.from(getContext());
transition = inflater.inflateTransition(R.transition.player);
//transition.setDuration(300);
scene = fullLayout;
transition.setInterpolator(new Interpolator() {
#Override
public float getInterpolation(float v) {
return lastOffset;
}
});
}
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if(newState == BottomSheetBehavior.STATE_DRAGGING) {
TransitionManager.go(scene, transition);
}
}
#Override
public void onSlide(View bottomSheet, final float slideOffset) {
scene = (slideOffset > lastOffset) ? smallLayout : fullLayout;
lastOffset = slideOffset;
}
}
As you can see I also created two Scene from different layout files and a custom Transition to animate between the scenes with the TransitionManager. My problem is that the Transition should be based on the slideOffset parameter (in range of 0-1) but the TransitionManager uses the Animation class in the background which is normally time based in Android.
I tried to create the custom Intapolator but this does not work properly. So how can I create a Transition which is based on an external variable and not on time?
Based on your description, I think you are trying to achieve something like google maps bottom sheet behaviour. The layout changes as the bottomsheet is dragged up.
If that is what you are trying to achieve then you don't need to enforce custom animations, as the bottomsheetdialog itself has those animation behaviour when incorporated inside a parent Coordinator Layout.
Here is a sample code of how I'm implementing the same behaviour. It also makes the FloatingActionButton invisible when the bottomsheet is dragged up to full screen size :
Create a bottomsheetdialog that you want to use inside your main layout
public class CustomBottomDialog extends BottomSheetDialogFragment {
String mSomeName;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// if some arguments are passed from the calling activity
mSomeName = getArguments().getString("some_name");
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View bottomSheet = inflater.inflate(R.layout.bottomsheet_layout, container, false);
// initialise your bottomsheet_layout items here
TextView tvName = bottomSheet.findViewById(R.id.display_name);
tvName.setText(mSomeName);
tvName.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// do something here
((MainActivity)getActivity()).doSomething();
}
});
return bottomSheet;
}
}
bottomsheet_layout:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.FloatingActionButton
android:id="#+id/nav"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/navigation_tilt_grey"
app:backgroundTint="#color/colorAccent"
app:elevation="3dp"
app:fabSize="normal"
android:layout_marginEnd="#dimen/activity_horizontal_margin"
app:layout_anchor="#+id/live_dash"
app:layout_anchorGravity="top|right" />
<!--BottomSheet-->
<android.support.v4.widget.NestedScrollView
android:id="#+id/live_dash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#F3F3F3"
android:clipToPadding="true"
app:layout_behavior="android.support.design.widget.BottomSheetBe
havior"
tools:layout_editor_absoluteY="150dp">
<!--Include your items here, the height of all items combined
will take the main screen layout size with animation-->
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
Calling this BottomSheet from your activity:
public void notifyBottomSheet(String somename){
BottomSheetDialogFragment customDialogFragment = new CustomBottomDialog();
Bundle args = new Bundle();
args.putString("some_name", somename);
customDialogFragment.setArguments(args);
customDialogFragment.show(getSupportFragmentManager(), customDialogFragment.getTag());
customDialogFragment.setCancelable(false); // if you don't wish to hide
}
Hope this solves what you are trying to achieve.
To easily slide something off the bottom of the screen, you can use code such as:
final int activityHeight = findViewById(android.R.id.content).getHeight();
cardContainer.animate().yBy(activityHeight - cardContainer.getY()).setDuration(SLIDE_OUT_DURATION);
where cardContainer is the view you are trying to slide off the screen.
See this blog post for the complete example. Note that you can also use translationY instead of yBy. Another, more generic way of doing it is with this code:
public static ViewPropertyAnimator slideOutToBottom(Context ctx, View view) {
final int screenHeight = ctx.getResources().getDisplayMetrics().heightPixels;
int[] coords = new int[2];
view.getLocationOnScreen(coords);
return view.animate().translationY(screenHeight - coords[Y_INDEX]).setDuration(SLIDE_OUT_DURATION);
}
public static ViewPropertyAnimator slideInFromBottom(Context ctx, View view) {
final int screenHeight = ctx.getResources().getDisplayMetrics().heightPixels;
int[] coords = new int[2];
view.getLocationOnScreen(coords);
view.setTranslationY(screenHeight - coords[Y_INDEX]);
return view.animate().translationY(0).setDuration(SLIDE_IN_DURATION).setInterpolator(new OvershootInterpolator(1f));
}
## Translation Animation ##
<?xml version="1.0" encoding="utf-8"?>
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/accelerate_decelerate_interpolator"
android:fillAfter="true"
>
<translate
android:fromYDelta="100%p"
android:toYDelta="-30%p"
android:duration="900" />
</set>
## Main Activity ##
#Override
protected void onResume() {
super.onResume();
Animation am= AnimationUtils.loadAnimation(this,R.anim.fadeout);
tv5.startAnimation(am);
Animation myanim= AnimationUtils.loadAnimation(this,R.anim.translate);
tv1.startAnimation(myanim);
myanim.setStartOffset(500);
Animation animation= AnimationUtils.loadAnimation(this,R.anim.translate);
animation.setStartOffset(1000);
tv2.startAnimation(animation);
Animation an= AnimationUtils.loadAnimation(this,R.anim.translate);
an.setStartOffset(1500);
tv3.startAnimation(an);
Animation ab= AnimationUtils.loadAnimation(this,R.anim.translate);
ab.setStartOffset(2000);
tv4.startAnimation(ab);
Animation ac= AnimationUtils.loadAnimation(this,R.anim.fadein);
ac.setStartOffset(2500);
btn1.startAnimation(ac);
}
I'm not sure if that is what you want but maybe instead of using transition, you can use the function animate() since with this function, you can change all things about your animation (time, visibility etc.).
I have bottom sheet, and I want to change its behavior so it would work like on the main screen of Google Maps application, where you can expand it to any position and leave it there and it won't automatically stick to the bottom or to the top. Here's my layout with bottom sheet:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.gms.maps.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<View
android:id="#+id/shadow"
android:layout_width="match_parent"
android:layout_height="16dp"
android:background="#drawable/shape_gradient_top_shadow"
app:layout_anchor="#+id/map_bottom_sheet" />
<LinearLayout
android:id="#+id/map_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="300dp"
android:fillViewport="false"
android:orientation="vertical"
app:behavior_peekHeight="50dp"
android:background="#color/lightGray"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<include layout="#layout/bottom_sheet_top_buttons"/>
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottom_sheet_content_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/lightGray"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
What I need in essence is eliminate forcing of STATE_EXPANDED and STATE_COLLAPSED states when dragging is ended.
Here's a visual explanation of what I try to achieve:
As you can see, bottom sheet doesn't automatically anchor to the top or the bottom but stays at whatever position it was left.
Copy the code from android.support.design.widget.BottomSheetBehavior to make your own custom behavior. Then modify the onViewReleased() method which is responsible for the movement of the sheet after the drag ends. You also have to introduce a new state besides the existing ones - the state is helpful to restore the position and let others know in which state your sheet is at the moment with getState().
#Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
int top;
#State int targetState;
// Use the position where the drag ended as new top
top = releasedChild.getTop();
// You have to manage the states here, too (introduce a new one)
targetState = STATE_ANCHORED;
if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(releasedChild, new SettleRunnable(releasedChild, targetState));
} else {
setStateInternal(targetState);
}
}
I have created a proof of concept originating from the orginal source code from the design library. You can view it here. The problem with the original behavior is it doesn't allow flings, and most methods are private so extending the class and overriding some methods in an attempt to achieve it won't get you very far either. My implementation allows for optional snapping behavior, transient states (don't automatically snap after drag) and customizations around setting peek height and max height.
Hi Alex you can try this code for similar expected behaviour, it is not as optimised but it will help you to understand the concept.
final DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
bottomSheetBehavior.setPeekHeight(200);
// set callback for changes
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
Log.d(TAG, "onStateChanged: " + bottomSheet.getY() + "::" + bottomSheet.getMeasuredHeight() + " :: " + bottomSheet.getTop());
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
ViewGroup.LayoutParams params = bottomSheet.getLayoutParams();
params.height = Math.max(0, metrics.heightPixels - (int) bottomSheet.getTop());
bottomSheet.setLayoutParams(params);
}
});
I'm using Bottom Sheet from Android support library like this:
XML:
<LinearLayout
android:id="#+id/bottomSheetLinearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/fourth_white"
android:orientation="vertical"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />
I add child views to LinearLayout:
bottomSheet.addView(actionButtonView);
After I've finished adding child views, I initialize BottomSheetBehavior and expand it:
BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(bottomSheet);
sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
This doesn't work. Nothing shows. Even if I preset the LinearLayout height inside XML, it's just all white.
If I add all the child views inside LinearLayout in XML, then everything works fine. It just doesn't work when I try to dynamically add views programatically.
Anyone had any similar issues?
Troubles with dynamic content on BottomSheetBehavior related to implementation of it's expanded size calculation. BottomSheetBehavior calculates expanded size in onLayoutChild method. But when you change content of sheet layout process launches asynchronous. Even if you call RequestLayout or something similar. So consequence of calls is like this:
BottomSheetBehavior have old expanded size (in your case I think it is zero)
You add content to BottomSheet. Expanded size is still old.
You call SetState to EXPANDED. BottomSheetBehavior still remember old expanded size and launches animation to that size. State changed to STATE_SETTLING!
onLayoutChild called and BottomSheetBehavior calculates new expanded size. But animation is already in progress and state is STATE_SETTLING so BottomSheetBehavior do not change its size
Animation finished. Size of BottomSheet is old. State changed to EXPANDED but BottomSheetBehavior "forgot" that expanded size was changed during animation.
It is surely the bug of BottomSheetBehaviour implementation.
In my project I found such workaround:
private void showPanel(final View panelContent) {
if (panelBehavior.getState()!=BottomSheetBehavior.STATE_EXPANDED) {
panelBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(final View bottomSheet, int newState) {
if (newState==BottomSheetBehavior.STATE_EXPANDED) {
panelBehavior.setBottomSheetCallback(null);
contentView.removeAllViews();
contentView.addView(panelContent);
panelView.setVisibility(View.VISIBLE);
}
}
#Override
public void onSlide(View bottomSheet, float slideOffset) {
}
});
panelBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
return;
}
contentView.removeAllViews();
contentView.addView(panelContent);
panelView.setVisibility(View.VISIBLE);
}
private void hidePanel() {
panelBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
panelView.setVisibility(View.GONE);
contentView.removeAllViews();
}
So when you need to show BottomSheet with new content call ShowPanel. When you need to completely hide BottomSheet call hidePanel (if you need to hide it in your project. If not you could remove setVisibility from methods).
The idea of workaround is to never change content of BottomSheet when BottomSheetBehavior is not in expanded state. If state is not expanded just change it to expanded, wait until animation finished and only then change content.
Try to post runnable to view's message queue:
bottomSheet.post(new Runnable() {
#Override
public void run() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
Or with retrolambda:
bottomSheet.post(() -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED));
I am currently struggling to find a good way how to animate some views in a specific way.
Following screenshots should show what I want to achieve:
First state (HIDDEN):
Second state (COLLAPSED)
Third state (EXPANDED)
The change between these states should be animated.
Those views are not draggable or slideable at all.
I know that there is the SlidingUpPanel by umano but I think that would be kind of an overkill.
At the moment the way I achieve this behaviour is the following:
I wrap the 2 panels (top and bot) in a relative layout and use the property animator to animate a change of the height of the relative layout.
So when the state is COLLAPSED then the height of the relative layout will be animated from 0 to the height of the top panel.
This works fine but I think that this is a really bad way to do this.
I already tried out to create a custom ViewGroup but the animating part didnt work yet.
Any input is appreciated.
I would use FrameLayout here as follows:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/screen"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/top_panel"
android:layout_width="match_parent"
android:layout_height="#dimen/top_panel_height"
android:layout_gravity="bottom"/>
<FrameLayout
android:id="#+id/bottom_panel"
android:layout_width="match_parent"
android:layout_height="#dimen/bottom_panel_height"
android:layout_gravity="bottom"/>
</FrameLayout>
Then, create enum for states
enum State {
HIDDEN {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(topPanel.getHeight()).setDuration(animationDuration);
bottomPanel.animate().translationY(topPanel.getHeight() + bottomPanel.getHeight()).setDuration(animationDuration);
}
},
COLLAPSED {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(0).setDuration(animationDuration);
bottomPanel.animate().translationY(bottomPanel.getHeight()).setDuration(animationDuration);
}
},
EXPANDED {
#Override
public void moveTo(View topPanel, View bottomPanel, long animationDuration) {
topPanel.animate().translationY(-bottomPanel.getHeight()).setDuration(animationDuration);
bottomPanel.animate().translationY(0).setDuration(animationDuration);
}
};
public abstract void moveTo(View topPanel, View bottomPanel, long animationDuration);
}
Usage of this would be as follows:
State newState = State.EXPANDED;
newState.moveTo(topPanel, bottomPanel, 200);