Automatic peekHeight for bottom sheets

Apps can specify app:behavior_peekHeight="auto" to let the
BottomSheetBehavior automatically calculate the 16:9 ratio keyline and
use it as the peek height while keeping it at least as tall as 64dp.

This is now the default parameter of BottomSheetBehavior and
BottomSheetDialog. The old default was 0, so this change will not affect
existing apps which should already have some value specified for the
peek height.

Bug: 27260758
Change-Id: Ibbf725aa89352512f4b54fa9fe297c1675f76939
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index f132d7a..26f75fc 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -405,7 +405,10 @@
 
     <declare-styleable name="BottomSheetBehavior_Layout">
         <!-- The height of the bottom sheet when it is collapsed. -->
-        <attr name="behavior_peekHeight" format="dimension"/>
+        <attr name="behavior_peekHeight" format="dimension">
+            <!-- Peek at the 16:9 ratio keyline of its parent -->
+            <enum name="auto" value="-1"/>
+        </attr>
         <!-- Whether this bottom sheet can be hidden by dragging it further downwards -->
         <attr name="behavior_hideable" format="boolean"/>
         <!-- Skip the collapsed state once expanded; no effect unless it is hideable -->
diff --git a/design/res/values/dimens.xml b/design/res/values/dimens.xml
index 6439b57e3..e7e1f4c 100644
--- a/design/res/values/dimens.xml
+++ b/design/res/values/dimens.xml
@@ -56,7 +56,7 @@
     <dimen name="design_appbar_elevation">4dp</dimen>
 
     <dimen name="design_bottom_sheet_modal_elevation">16dp</dimen>
-    <dimen name="design_bottom_sheet_modal_peek_height">256dp</dimen>
+    <dimen name="design_bottom_sheet_peek_height_min">64dp</dimen>
 
     <dimen name="design_bottom_navigation_height">56dp</dimen>
     <dimen name="design_bottom_navigation_text_size">12sp</dimen>
diff --git a/design/res/values/styles.xml b/design/res/values/styles.xml
index c5c6051..31eddbb 100644
--- a/design/res/values/styles.xml
+++ b/design/res/values/styles.xml
@@ -130,7 +130,7 @@
     <style name="Widget.Design.BottomSheet.Modal" parent="android:Widget">
         <item name="android:background">?android:attr/colorBackground</item>
         <item name="android:elevation">@dimen/design_bottom_sheet_modal_elevation</item>
-        <item name="behavior_peekHeight">@dimen/design_bottom_sheet_modal_peek_height</item>
+        <item name="behavior_peekHeight">auto</item>
         <item name="behavior_hideable">true</item>
         <item name="behavior_skipCollapsed">false</item>
     </style>
diff --git a/design/src/android/support/design/widget/BottomSheetBehavior.java b/design/src/android/support/design/widget/BottomSheetBehavior.java
index 68d8e52..f4f8de2 100644
--- a/design/src/android/support/design/widget/BottomSheetBehavior.java
+++ b/design/src/android/support/design/widget/BottomSheetBehavior.java
@@ -32,6 +32,7 @@
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.ViewDragHelper;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
@@ -106,6 +107,11 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface State {}
 
+    /**
+     * Peek at the 16:9 ratio keyline of its parent.
+     */
+    public static final int PEEK_HEIGHT_AUTO = -1;
+
     private static final float HIDE_THRESHOLD = 0.5f;
 
     private static final float HIDE_FRICTION = 0.1f;
@@ -114,6 +120,10 @@
 
     private int mPeekHeight;
 
+    private boolean mPeekHeightAuto;
+
+    private int mPeekHeightMin;
+
     private int mMinOffset;
 
     private int mMaxOffset;
@@ -165,8 +175,13 @@
         super(context, attrs);
         TypedArray a = context.obtainStyledAttributes(attrs,
                 R.styleable.BottomSheetBehavior_Layout);
-        setPeekHeight(a.getDimensionPixelSize(
-                R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, 0));
+        TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight);
+        if (value.data == PEEK_HEIGHT_AUTO) {
+            setPeekHeight(value.data);
+        } else {
+            setPeekHeight(a.getDimensionPixelSize(
+                    R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
+        }
         setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false));
         setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
                 false));
@@ -202,8 +217,18 @@
         parent.onLayoutChild(child, layoutDirection);
         // Offset the bottom sheet
         mParentHeight = parent.getHeight();
+        int peekHeight;
+        if (mPeekHeightAuto) {
+            if (mPeekHeightMin == 0) {
+                mPeekHeightMin = parent.getResources().getDimensionPixelSize(
+                        R.dimen.design_bottom_sheet_peek_height_min);
+            }
+            peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);
+        } else {
+            peekHeight = mPeekHeight;
+        }
         mMinOffset = Math.max(0, mParentHeight - child.getHeight());
-        mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
+        mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
         if (mState == STATE_EXPANDED) {
             ViewCompat.offsetTopAndBottom(child, mMinOffset);
         } else if (mHideable && mState == STATE_HIDDEN) {
@@ -395,12 +420,29 @@
     /**
      * Sets the height of the bottom sheet when it is collapsed.
      *
-     * @param peekHeight The height of the collapsed bottom sheet in pixels.
+     * @param peekHeight The height of the collapsed bottom sheet in pixels, or
+     *                   {@link #PEEK_HEIGHT_AUTO}.
      * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
      */
     public final void setPeekHeight(int peekHeight) {
-        mPeekHeight = Math.max(0, peekHeight);
-        mMaxOffset = mParentHeight - peekHeight;
+        boolean layout = false;
+        if (peekHeight == PEEK_HEIGHT_AUTO) {
+            if (!mPeekHeightAuto) {
+                mPeekHeightAuto = true;
+                layout = true;
+            }
+        } else if (mPeekHeightAuto || mPeekHeight != peekHeight) {
+            mPeekHeightAuto = false;
+            mPeekHeight = Math.max(0, peekHeight);
+            mMaxOffset = mParentHeight - peekHeight;
+            layout = true;
+        }
+        if (layout && mState == STATE_COLLAPSED && mViewRef != null) {
+            V view = mViewRef.get();
+            if (view != null) {
+                view.requestLayout();
+            }
+        }
     }
 
     /**
diff --git a/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java b/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
index 98cfbe8..932bc55 100644
--- a/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
+++ b/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
@@ -607,6 +607,23 @@
         });
     }
 
+    @Test
+    public void testAutoPeekHeight() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getBehavior().setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO);
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                assertThat(getBottomSheet().getTop(),
+                        is(getCoordinatorLayout().getWidth() * 9 / 16));
+            }
+        });
+    }
+
     private void checkSetState(final int state, Matcher<View> matcher) {
         registerIdlingResourceCallback();
         try {
diff --git a/samples/SupportDesignDemos/res/layout/design_bottom_sheet_modal.xml b/samples/SupportDesignDemos/res/layout/design_bottom_sheet_modal.xml
index cbd3b27..d8e7f75 100644
--- a/samples/SupportDesignDemos/res/layout/design_bottom_sheet_modal.xml
+++ b/samples/SupportDesignDemos/res/layout/design_bottom_sheet_modal.xml
@@ -28,10 +28,17 @@
             style="@style/TextAppearance.AppCompat.Headline"/>
 
     <Button
-            android:id="@+id/show"
+            android:id="@+id/show_short"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="16dp"
-            android:text="@string/bottomsheet_show"/>
+            android:text="@string/bottomsheet_show_short"/>
+
+    <Button
+            android:id="@+id/show_long"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:text="@string/bottomsheet_show_long"/>
 
 </LinearLayout>
diff --git a/samples/SupportDesignDemos/res/values/strings.xml b/samples/SupportDesignDemos/res/values/strings.xml
index ed725ba..848dbfb 100644
--- a/samples/SupportDesignDemos/res/values/strings.xml
+++ b/samples/SupportDesignDemos/res/values/strings.xml
@@ -108,6 +108,8 @@
     <string name="bottomsheet_modal">Modal</string>
     <string name="bottomsheet_hide">Hide</string>
     <string name="bottomsheet_show">Show</string>
+    <string name="bottomsheet_show_short">Show short</string>
+    <string name="bottomsheet_show_long">Show long</string>
     <string name="item_n">Item %d</string>
 
     <string name="design_bottom_navigation_view">Bottom navigation view</string>
diff --git a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomSheetModalBase.java b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomSheetModalBase.java
index 84c260a..1eb4dfc 100644
--- a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomSheetModalBase.java
+++ b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomSheetModalBase.java
@@ -38,15 +38,21 @@
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.design_bottom_sheet_modal);
-        findViewById(R.id.show).setOnClickListener(mOnClickListener);
+        findViewById(R.id.show_short).setOnClickListener(mOnClickListener);
+        findViewById(R.id.show_long).setOnClickListener(mOnClickListener);
     }
 
     private View.OnClickListener mOnClickListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             switch (v.getId()) {
-                case R.id.show:
-                    new ModalFragment().show(getSupportFragmentManager(), FRAGMENT_MODAL);
+                case R.id.show_short:
+                    ModalFragment.newInstance(5)
+                            .show(getSupportFragmentManager(), FRAGMENT_MODAL);
+                    break;
+                case R.id.show_long:
+                    ModalFragment.newInstance(ModalFragment.LENGTH_ALL)
+                            .show(getSupportFragmentManager(), FRAGMENT_MODAL);
                     break;
             }
         }
@@ -57,6 +63,18 @@
      */
     public static class ModalFragment extends BottomSheetDialogFragment {
 
+        private static final String ARG_LENGTH = "length";
+
+        public static final int LENGTH_ALL = Cheeses.sCheeseStrings.length;
+
+        public static ModalFragment newInstance(int length) {
+            ModalFragment fragment = new ModalFragment();
+            Bundle args = new Bundle();
+            args.putInt(ARG_LENGTH, Math.min(LENGTH_ALL, length));
+            fragment.setArguments(args);
+            return fragment;
+        }
+
         @Nullable
         @Override
         public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -72,8 +90,10 @@
                     (RecyclerView) view.findViewById(R.id.bottom_sheet_recyclerview);
             Context context = recyclerView.getContext();
             recyclerView.setLayoutManager(new LinearLayoutManager(context));
-            recyclerView.setAdapter(new SimpleStringRecyclerViewAdapter(context,
-                    Cheeses.sCheeseStrings));
+            int length = getArguments().getInt(ARG_LENGTH);
+            String[] array = new String[length];
+            System.arraycopy(Cheeses.sCheeseStrings, 0, array, 0, length);
+            recyclerView.setAdapter(new SimpleStringRecyclerViewAdapter(context, array));
         }
 
     }