20

I am wanting to implement a "Bottom Sheet" type of layout, but with a twist where the "bottom" sheet will be a MapFragment, which won't work very well as an up/down draggable view.

I had a probably naive thought to "flip" the logic to a "Top Sheet" design, where you drag the Top Sheet up/down to show more/less of the bottom MapFragment.

ie: From this...
Bottom Sheet Example

...to [something like] this...
Top Sheet Example

Is this possible given the Support Design Tools, or will I have to roll something like this on my own?

swooby
  • 3,005
  • 2
  • 36
  • 43
  • 3
    I think you are going to sign up to do a substantial amount of heavy lifting to do two things. First, to create your custom implementation. Second, to identify how your implementation conflicts with the android implementation and to defend against those scenarios. My personal opinion, the Material Design Language has been put in place to visually communicate the ways your users can expect to interact with the app. Something like this may be great as a personal endeavour for learning, but once you hit the market - you need to expect that every single person won't understand how to use your app. – apelsoczi Aug 22 '16 at 20:44
  • 1
    I ended up just adding a bottomsheet layout that has a Toolbar at its top that allows the user to drag the toolbar up. The trick is then to resize the Map as the user drags the toolbar. I can post my code if anyone is interested. – swooby Sep 09 '16 at 21:08
  • I'd like to see it @swooby, I'm trying to do something similar – odiggity Nov 18 '16 at 17:26
  • @odiggity you can check my answer, I finally made it working exactly as the BottomSheetBehavior, but from the top. – xarlymg89 Nov 21 '18 at 11:17

2 Answers2

12

I found a TopSheetBehavior implementation and have tried to keep it up-to-date: https://github.com/carlos-mg89/TopSheetBehavior

It has proved to work pretty well in my case. I found many other TopSheetBehavior that were incomplete or that crashed, but this one doesn't crash and works out of the box by only replacing the behavior parameter:

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:behavior_hideable="true"
        app:behavior_peekHeight="56dp"
        app:layout_behavior="your.package.components.TopSheetBehavior">

        <!-- Your content goes here -->

    </LinearLayout>
xarlymg89
  • 2,552
  • 2
  • 27
  • 41
  • That's the perfect way to do that. It's working smoothly and fine. – Dheeraj Rijhwani Nov 20 '18 at 18:05
  • Can I use this exact solution in closed source project? Or should I provide some references? – Viktor Vostrikov Dec 23 '18 at 15:08
  • @ViktorVostrikov yes, you can. It's under the Apache license. Here you have a few questions answered about the usage of code under the Apache license: https://resources.whitesourcesoftware.com/blog-whitesource/top-10-apache-license-questions-answered – xarlymg89 Dec 24 '18 at 11:01
  • @CarlosAlbertoMartínezGadea however it is stated that I need to still write somehow the owner of the library.. Don't know there should I provide credentials.. – Viktor Vostrikov Dec 24 '18 at 11:17
  • @ViktorVostrikov to me, the phrase "If you redistribute software with any Apache licensed components, you must include a copy of the license, provide a clear Apache License attribution, and add modification notices to all the files that you modify." explains the situation quite clearly. You don't need to contact the author of the code but you must show that you are using the Apache License, with a link to that license. – xarlymg89 Dec 24 '18 at 12:32
  • @TuanDao did you save the TopSheetBehavior class in the directory your.package.components ? If you don't it's normal that your app crashes. Share your error log anyway through a pastebin.com URL or similar. – xarlymg89 Dec 31 '19 at 09:49
  • 2
    Also, I recently made a RightSheetBehavior https://github.com/OKatrych/RightSheetBehavior. – Warble Feb 28 '20 at 15:02
  • I found that if the content of the topsheet changes while it is collapsed, the sheet will expand over the app ui and is visible to the user unexpectedly, whereas a bottomsheet would expand downwards so it is invisible to the user. Is there a way to combat this? – behelit Jan 14 '21 at 21:31
  • @behelit did you try the latest commit's version? If so, it could be that somehow I messed it up with recent changes related with deprecated code. You could try an earlier version and check if the same happens please? – xarlymg89 Jan 15 '21 at 08:27
  • @xarlymg89 thanks for the reply, it turned out to be due to animating layout changes. It must be false for it to behave correctly. I had it on. – behelit Jan 20 '21 at 05:27
  • @behelit great! I'm happy it works great for you ;) – xarlymg89 Jan 20 '21 at 07:20
2

Here is the basis of my solution that I commented about above. I will come back and flesh it out later.

@Override
protected void onCreate(
        @Nullable
                Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    if (isFinishing())
    {
        return;
    }


    setContentView(R.layout.activity_home);

    ...

    mGroupBottomSheetFiller = (ViewGroup) findViewById(R.id.groupBottomSheetFiller);

    final NestedScrollView bottomSheetMap = (NestedScrollView) findViewById(R.id.bottomSheetMap);
    mBottomSheetMapBehavior = BottomSheetBehavior.from(bottomSheetMap);
    mBottomSheetMapBehavior.setBottomSheetCallback(new BottomSheetCallback()
    {
        @Override
        public void onStateChanged(
                @NonNull
                        View bottomSheet,
                int newState)
        {
            //Log.e(TAG, "mBottomSheetMapBehavior.onStateChanged(bottomSheet, newState=" +
            //             bottomSheetBehaviorStateToString(newState) + ')');
            int visibility = isBottomSheetExpanded(mBottomSheetMapBehavior) ? View.VISIBLE : View.GONE;
            mImageBottomSheetMapClose.setVisibility(visibility);
        }

        @Override
        public void onSlide(
                @NonNull
                        View bottomSheet,
                float slideOffset)
        {
            //Log.e(TAG, "mBottomSheetMapBehavior.onStateChanged(bottomSheet, slideOffset=" + slideOffset + ')');
            resizeMap();
        }
    });
    bottomSheetMap.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener()
    {
        @Override
        public void onGlobalLayout()
        {
            //Log.e(TAG, "onGlobalLayout()");
            bottomSheetMap.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            resizeMap();
        }
    });

    ...
}

private void resizeMap()
{
    int screenHeightPixels = PbPlatformUtils.getScreenHeightPixels();
    //Log.e(TAG, "resizeMap: screenHeightPixels=" + screenHeightPixels);

    int[] location = new int[2];
    mGroupMap.getLocationInWindow(location);
    //Log.e(TAG, "resizeMap: getLocationInWindow=" + Arrays.toString(location));

    LayoutParams groupMapLayoutParams = mGroupMap.getLayoutParams();
    groupMapLayoutParams.height = screenHeightPixels - location[1];
    mGroupMap.requestLayout();
}

public static String bottomSheetBehaviorStateToString(int state)
{
    String s;
    switch (state)
    {
        case BottomSheetBehavior.STATE_COLLAPSED:
            s = "STATE_COLLAPSED";
            break;
        case BottomSheetBehavior.STATE_DRAGGING:
            s = "STATE_DRAGGING";
            break;
        case BottomSheetBehavior.STATE_EXPANDED:
            s = "STATE_EXPANDED";
            break;
        case BottomSheetBehavior.STATE_HIDDEN:
            s = "STATE_HIDDEN";
            break;
        case BottomSheetBehavior.STATE_SETTLING:
            s = "STATE_SETTLING";
            break;
        default:
            s = "UNKNOWN";
            break;
    }
    return s + '(' + state + ')';
}

private static boolean isBottomSheetExpanded(
        @NonNull
                BottomSheetBehavior bottomSheetBehavior)
{
    return bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED;
}

private void bottomSheetMapExpand()
{
    mGroupBottomSheetFiller.setVisibility(View.VISIBLE);
    int peekHeightPx = getResources().getDimensionPixelSize(R.dimen.home_bottom_sheet_map_peek_height);
    mBottomSheetMapBehavior.setPeekHeight(peekHeightPx);
    mBottomSheetMapBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
    mBottomSheetMapBehavior.setHideable(false);
}

private void bottomSheetMapCollapse()
{
    mGroupBottomSheetFiller.setVisibility(View.VISIBLE);
    int peekHeightPx = getResources().getDimensionPixelSize(R.dimen.home_bottom_sheet_map_peek_height);
    mBottomSheetMapBehavior.setPeekHeight(peekHeightPx);
    mBottomSheetMapBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
    mBottomSheetMapBehavior.setHideable(false);
}

private void bottomSheetMapHide()
{
    mBottomSheetMapBehavior.setHideable(true);
    mBottomSheetMapBehavior.setPeekHeight(0);
    mBottomSheetMapBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
    mGroupBottomSheetFiller.setVisibility(View.GONE);
}
swooby
  • 3,005
  • 2
  • 36
  • 43
  • 1
    did you encounter compatibility issues with this solution? in case you have a working app in the playstore using this functionality it'd be great to check it out :) – carlosavoy May 29 '17 at 15:33
  • You can check my answer @swooby , perhaps it's nearer to what you were looking for. At least, it behaves exactly as I imagined a TopSheet to be equivalent to a ```BottomSheetBehavior```. – xarlymg89 Nov 21 '18 at 11:18
  • Looks good @CarlosAlbertoMartínezGadea, but the implementation in my answer current fits my needs for an existing production app, so I haven't experimented with porting over to yours. If I do one day I'll mark yours as the preferred answer! Thanks! – swooby Nov 27 '18 at 21:07
  • do you have a complete solution...i am doing something similar and i am unable to find useful material for this... – unownsp Oct 27 '21 at 11:52