Home | History | Annotate | Download | only in drawer
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.wear.widget.drawer;
     18 
     19 import static androidx.wear.widget.drawer.WearableDrawerView.STATE_IDLE;
     20 import static androidx.wear.widget.drawer.WearableDrawerView.STATE_SETTLING;
     21 
     22 import android.content.Context;
     23 import android.os.Handler;
     24 import android.os.Looper;
     25 import android.util.AttributeSet;
     26 import android.util.DisplayMetrics;
     27 import android.util.Log;
     28 import android.view.Gravity;
     29 import android.view.MotionEvent;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     33 import android.view.WindowInsets;
     34 import android.view.WindowManager;
     35 import android.view.accessibility.AccessibilityManager;
     36 import android.widget.FrameLayout;
     37 
     38 import androidx.annotation.NonNull;
     39 import androidx.annotation.Nullable;
     40 import androidx.annotation.VisibleForTesting;
     41 import androidx.core.view.NestedScrollingParent;
     42 import androidx.core.view.NestedScrollingParentHelper;
     43 import androidx.core.view.ViewCompat;
     44 import androidx.customview.widget.ViewDragHelper;
     45 import androidx.wear.widget.drawer.FlingWatcherFactory.FlingListener;
     46 import androidx.wear.widget.drawer.FlingWatcherFactory.FlingWatcher;
     47 import androidx.wear.widget.drawer.WearableDrawerView.DrawerState;
     48 
     49 /**
     50  * Top-level container that allows interactive drawers to be pulled from the top and bottom edge of
     51  * the window. For WearableDrawerLayout to work properly, scrolling children must send nested
     52  * scrolling events. Views that implement {@link androidx.core.view.NestedScrollingChild} do
     53  * this by default. To enable nested scrolling on frameworks views like {@link
     54  * android.widget.ListView}, set <code>android:nestedScrollingEnabled="true"</code> on the view in
     55  * the layout file, or call {@link View#setNestedScrollingEnabled} in code. This includes the main
     56  * content in a WearableDrawerLayout, as well as the content inside of the drawers.
     57  *
     58  * <p>To use WearableDrawerLayout with {@link WearableActionDrawerView} or {@link
     59  * WearableNavigationDrawerView}, place either drawer in a WearableDrawerLayout.
     60  *
     61  * <pre>
     62  * &lt;androidx.wear.widget.drawer.WearableDrawerLayout [...]&gt;
     63  *     &lt;FrameLayout android:id=@+id/content /&gt;
     64  *
     65  *     &lt;androidx.wear.widget.drawer.WearableNavigationDrawerView
     66  *         android:layout_width=match_parent
     67  *         android:layout_height=match_parent /&gt;
     68  *
     69  *     &lt;androidx.wear.widget.drawer.WearableActionDrawerView
     70  *         android:layout_width=match_parent
     71  *         android:layout_height=match_parent /&gt;
     72  *
     73  * &lt;/androidx.wear.widget.drawer.WearableDrawerLayout&gt;</pre>
     74  *
     75  * <p>To use custom content in a drawer, place {@link WearableDrawerView} in a WearableDrawerLayout
     76  * and specify the layout_gravity to pick the drawer location (the following example is for a top
     77  * drawer). <b>Note:</b> You must either call {@link WearableDrawerView#setDrawerContent} and pass
     78  * in your drawer content view, or specify it in the {@code app:drawerContent} XML attribute.
     79  *
     80  * <pre>
     81  * &lt;androidx.wear.widget.drawer.WearableDrawerLayout [...]&gt;
     82  *     &lt;FrameLayout
     83  *         android:id=@+id/content
     84  *         android:layout_width=match_parent
     85  *         android:layout_height=match_parent /&gt;
     86  *
     87  *     &lt;androidx.wear.widget.drawer.WearableDrawerView
     88  *         android:layout_width=match_parent
     89  *         android:layout_height=match_parent
     90  *         android:layout_gravity=top
     91  *         app:drawerContent="@+id/top_drawer_content" &gt;
     92  *
     93  *         &lt;FrameLayout
     94  *             android:id=@id/top_drawer_content
     95  *             android:layout_width=match_parent
     96  *             android:layout_height=match_parent /&gt;
     97  *
     98  *     &lt;/androidx.wear.widget.drawer.WearableDrawerView&gt;
     99  * &lt;/androidx.wear.widget.drawer.WearableDrawerLayout&gt;</pre>
    100  */
    101 public class WearableDrawerLayout extends FrameLayout
    102         implements View.OnLayoutChangeListener, NestedScrollingParent, FlingListener {
    103 
    104     private static final String TAG = "WearableDrawerLayout";
    105 
    106     /**
    107      * Undefined layout_gravity. This is different from {@link Gravity#NO_GRAVITY}. Follow up with
    108      * frameworks to find out why (b/27576632).
    109      */
    110     private static final int GRAVITY_UNDEFINED = -1;
    111 
    112     private static final int PEEK_FADE_DURATION_MS = 150;
    113 
    114     private static final int PEEK_AUTO_CLOSE_DELAY_MS = 1000;
    115 
    116     /**
    117      * The downward scroll direction for use as a parameter to canScrollVertically.
    118      */
    119     private static final int DOWN = 1;
    120 
    121     /**
    122      * The upward scroll direction for use as a parameter to canScrollVertically.
    123      */
    124     private static final int UP = -1;
    125 
    126     /**
    127      * The percent at which the drawer will be opened when the drawer is released mid-drag.
    128      */
    129     private static final float OPENED_PERCENT_THRESHOLD = 0.5f;
    130 
    131     /**
    132      * When a user lifts their finger off the screen, this may trigger a couple of small scroll
    133      * events. If the user is scrolling down and the final events from the user lifting their finger
    134      * are up, this will cause the bottom drawer to peek. To prevent this from happening, we prevent
    135      * the bottom drawer from peeking until this amount of scroll is exceeded. Note, scroll up
    136      * events are considered negative.
    137      */
    138     private static final int NESTED_SCROLL_SLOP_DP = 5;
    139     @VisibleForTesting final ViewDragHelper.Callback mTopDrawerDraggerCallback;
    140     @VisibleForTesting final ViewDragHelper.Callback mBottomDrawerDraggerCallback;
    141     private final int mNestedScrollSlopPx;
    142     private final NestedScrollingParentHelper mNestedScrollingParentHelper =
    143             new NestedScrollingParentHelper(this);
    144     /**
    145      * Helper for dragging the top drawer.
    146      */
    147     private final ViewDragHelper mTopDrawerDragger;
    148     /**
    149      * Helper for dragging the bottom drawer.
    150      */
    151     private final ViewDragHelper mBottomDrawerDragger;
    152     private final boolean mIsAccessibilityEnabled;
    153     private final FlingWatcherFactory mFlingWatcher;
    154     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    155     private final ClosePeekRunnable mCloseTopPeekRunnable = new ClosePeekRunnable(Gravity.TOP);
    156     private final ClosePeekRunnable mCloseBottomPeekRunnable = new ClosePeekRunnable(
    157             Gravity.BOTTOM);
    158     /**
    159      * Top drawer view.
    160      */
    161     @Nullable private WearableDrawerView mTopDrawerView;
    162     /**
    163      * Bottom drawer view.
    164      */
    165     @Nullable private WearableDrawerView mBottomDrawerView;
    166     /**
    167      * What we have inferred the scrolling content view to be, should one exist.
    168      */
    169     @Nullable private View mScrollingContentView;
    170     /**
    171      * Listens to drawer events.
    172      */
    173     private DrawerStateCallback mDrawerStateCallback;
    174     private int mSystemWindowInsetBottom;
    175     /**
    176      * Tracks the amount of nested scroll in the up direction. This is used with {@link
    177      * #NESTED_SCROLL_SLOP_DP} to prevent false drawer peeks.
    178      */
    179     private int mCurrentNestedScrollSlopTracker;
    180     /**
    181      * Tracks whether the top drawer should be opened after layout.
    182      */
    183     private boolean mShouldOpenTopDrawerAfterLayout;
    184     /**
    185      * Tracks whether the bottom drawer should be opened after layout.
    186      */
    187     private boolean mShouldOpenBottomDrawerAfterLayout;
    188     /**
    189      * Tracks whether the top drawer should be peeked after layout.
    190      */
    191     private boolean mShouldPeekTopDrawerAfterLayout;
    192     /**
    193      * Tracks whether the bottom drawer should be peeked after layout.
    194      */
    195     private boolean mShouldPeekBottomDrawerAfterLayout;
    196     /**
    197      * Tracks whether the top drawer is in a state where it can be closed. The content in the drawer
    198      * can scroll, and {@link #mTopDrawerDragger} should not intercept events unless the top drawer
    199      * is scrolled to the bottom of its content.
    200      */
    201     private boolean mCanTopDrawerBeClosed;
    202     /**
    203      * Tracks whether the bottom drawer is in a state where it can be closed. The content in the
    204      * drawer can scroll, and {@link #mBottomDrawerDragger} should not intercept events unless the
    205      * bottom drawer is scrolled to the top of its content.
    206      */
    207     private boolean mCanBottomDrawerBeClosed;
    208     /**
    209      * Tracks whether the last scroll resulted in a fling. Fling events do not contain the amount
    210      * scrolled, which makes it difficult to determine when to unlock an open drawer. To work around
    211      * this, if the last scroll was a fling and the next scroll unlocks the drawer, pass {@link
    212      * #mDrawerOpenLastInterceptedTouchEvent} to {@link #onTouchEvent} to start the drawer.
    213      */
    214     private boolean mLastScrollWasFling;
    215     /**
    216      * The last intercepted touch event. See {@link #mLastScrollWasFling} for more information.
    217      */
    218     private MotionEvent mDrawerOpenLastInterceptedTouchEvent;
    219 
    220     public WearableDrawerLayout(Context context) {
    221         this(context, null);
    222     }
    223 
    224     public WearableDrawerLayout(Context context, AttributeSet attrs) {
    225         this(context, attrs, 0);
    226     }
    227 
    228     public WearableDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    229         this(context, attrs, defStyleAttr, 0);
    230     }
    231 
    232     public WearableDrawerLayout(
    233             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    234         super(context, attrs, defStyleAttr, defStyleRes);
    235 
    236         mFlingWatcher = new FlingWatcherFactory(this);
    237         mTopDrawerDraggerCallback = new TopDrawerDraggerCallback();
    238         mTopDrawerDragger =
    239                 ViewDragHelper.create(this, 1f /* sensitivity */, mTopDrawerDraggerCallback);
    240         mTopDrawerDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP);
    241 
    242         mBottomDrawerDraggerCallback = new BottomDrawerDraggerCallback();
    243         mBottomDrawerDragger =
    244                 ViewDragHelper.create(this, 1f /* sensitivity */, mBottomDrawerDraggerCallback);
    245         mBottomDrawerDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
    246 
    247         WindowManager windowManager = (WindowManager) context
    248                 .getSystemService(Context.WINDOW_SERVICE);
    249         DisplayMetrics metrics = new DisplayMetrics();
    250         windowManager.getDefaultDisplay().getMetrics(metrics);
    251         mNestedScrollSlopPx = Math.round(metrics.density * NESTED_SCROLL_SLOP_DP);
    252 
    253         AccessibilityManager accessibilityManager =
    254                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    255         mIsAccessibilityEnabled = accessibilityManager.isEnabled();
    256     }
    257 
    258     private static void animatePeekVisibleAfterBeingClosed(WearableDrawerView drawer) {
    259         final View content = drawer.getDrawerContent();
    260         if (content != null) {
    261             content.animate()
    262                     .setDuration(PEEK_FADE_DURATION_MS)
    263                     .alpha(0)
    264                     .withEndAction(
    265                             new Runnable() {
    266                                 @Override
    267                                 public void run() {
    268                                     content.setVisibility(GONE);
    269                                 }
    270                             })
    271                     .start();
    272         }
    273 
    274         ViewGroup peek = drawer.getPeekContainer();
    275         peek.setVisibility(VISIBLE);
    276         peek.animate()
    277                 .setStartDelay(PEEK_FADE_DURATION_MS)
    278                 .setDuration(PEEK_FADE_DURATION_MS)
    279                 .alpha(1)
    280                 .scaleX(1)
    281                 .scaleY(1)
    282                 .start();
    283 
    284         drawer.setIsPeeking(true);
    285     }
    286 
    287     /**
    288      * Shows the drawer's contents. If the drawer is peeking, an animation is used to fade out the
    289      * peek view and fade in the drawer content.
    290      */
    291     private static void showDrawerContentMaybeAnimate(WearableDrawerView drawerView) {
    292         drawerView.bringToFront();
    293         final View contentView = drawerView.getDrawerContent();
    294         if (contentView != null) {
    295             contentView.setVisibility(VISIBLE);
    296         }
    297 
    298         if (drawerView.isPeeking()) {
    299             final View peekView = drawerView.getPeekContainer();
    300             peekView.animate().alpha(0).scaleX(0).scaleY(0).setDuration(PEEK_FADE_DURATION_MS)
    301                     .start();
    302 
    303             if (contentView != null) {
    304                 contentView.setAlpha(0);
    305                 contentView
    306                         .animate()
    307                         .setStartDelay(PEEK_FADE_DURATION_MS)
    308                         .alpha(1)
    309                         .setDuration(PEEK_FADE_DURATION_MS)
    310                         .start();
    311             }
    312         } else {
    313             drawerView.getPeekContainer().setAlpha(0);
    314             if (contentView != null) {
    315                 contentView.setAlpha(1);
    316             }
    317         }
    318     }
    319 
    320     @Override
    321     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    322         mSystemWindowInsetBottom = insets.getSystemWindowInsetBottom();
    323 
    324         if (mSystemWindowInsetBottom != 0) {
    325             MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
    326             layoutParams.bottomMargin = mSystemWindowInsetBottom;
    327             setLayoutParams(layoutParams);
    328         }
    329 
    330         return super.onApplyWindowInsets(insets);
    331     }
    332 
    333     /**
    334      * Closes drawer after {@code delayMs} milliseconds.
    335      */
    336     private void closeDrawerDelayed(final int gravity, long delayMs) {
    337         switch (gravity) {
    338             case Gravity.TOP:
    339                 mMainThreadHandler.removeCallbacks(mCloseTopPeekRunnable);
    340                 mMainThreadHandler.postDelayed(mCloseTopPeekRunnable, delayMs);
    341                 break;
    342             case Gravity.BOTTOM:
    343                 mMainThreadHandler.removeCallbacks(mCloseBottomPeekRunnable);
    344                 mMainThreadHandler.postDelayed(mCloseBottomPeekRunnable, delayMs);
    345                 break;
    346             default:
    347                 Log.w(TAG, "Invoked a delayed drawer close with an invalid gravity: " + gravity);
    348         }
    349     }
    350 
    351     /**
    352      * Close the specified drawer by animating it out of view.
    353      *
    354      * @param gravity Gravity.TOP to move the top drawer or Gravity.BOTTOM for the bottom.
    355      */
    356     void closeDrawer(int gravity) {
    357         closeDrawer(findDrawerWithGravity(gravity));
    358     }
    359 
    360     /**
    361      * Close the specified drawer by animating it out of view.
    362      *
    363      * @param drawer The drawer view to close.
    364      */
    365     void closeDrawer(WearableDrawerView drawer) {
    366         if (drawer == null) {
    367             return;
    368         }
    369         if (drawer == mTopDrawerView) {
    370             mTopDrawerDragger.smoothSlideViewTo(
    371                     mTopDrawerView, 0 /* finalLeft */, -mTopDrawerView.getHeight());
    372             invalidate();
    373         } else if (drawer == mBottomDrawerView) {
    374             mBottomDrawerDragger
    375                     .smoothSlideViewTo(mBottomDrawerView, 0 /* finalLeft */, getHeight());
    376             invalidate();
    377         } else {
    378             Log.w(TAG, "closeDrawer(View) should be passed in the top or bottom drawer");
    379         }
    380     }
    381 
    382     /**
    383      * Open the specified drawer by animating it into view.
    384      *
    385      * @param gravity Gravity.TOP to move the top drawer or Gravity.BOTTOM for the bottom.
    386      */
    387     void openDrawer(int gravity) {
    388         if (!isLaidOut()) {
    389             switch (gravity) {
    390                 case Gravity.TOP:
    391                     mShouldOpenTopDrawerAfterLayout = true;
    392                     break;
    393                 case Gravity.BOTTOM:
    394                     mShouldOpenBottomDrawerAfterLayout = true;
    395                     break;
    396                 default: // fall out
    397             }
    398             return;
    399         }
    400         openDrawer(findDrawerWithGravity(gravity));
    401     }
    402 
    403     /**
    404      * Open the specified drawer by animating it into view.
    405      *
    406      * @param drawer The drawer view to open.
    407      */
    408     void openDrawer(WearableDrawerView drawer) {
    409         if (drawer == null) {
    410             return;
    411         }
    412         if (!isLaidOut()) {
    413             if (drawer == mTopDrawerView) {
    414                 mShouldOpenTopDrawerAfterLayout = true;
    415             } else if (drawer == mBottomDrawerView) {
    416                 mShouldOpenBottomDrawerAfterLayout = true;
    417             }
    418             return;
    419         }
    420 
    421         if (drawer == mTopDrawerView) {
    422             mTopDrawerDragger
    423                     .smoothSlideViewTo(mTopDrawerView, 0 /* finalLeft */, 0 /* finalTop */);
    424             showDrawerContentMaybeAnimate(mTopDrawerView);
    425             invalidate();
    426         } else if (drawer == mBottomDrawerView) {
    427             mBottomDrawerDragger.smoothSlideViewTo(
    428                     mBottomDrawerView, 0 /* finalLeft */,
    429                     getHeight() - mBottomDrawerView.getHeight());
    430             showDrawerContentMaybeAnimate(mBottomDrawerView);
    431             invalidate();
    432         } else {
    433             Log.w(TAG, "openDrawer(View) should be passed in the top or bottom drawer");
    434         }
    435     }
    436 
    437     /**
    438      * Peek the drawer.
    439      *
    440      * @param gravity {@link Gravity#TOP} to peek the top drawer or {@link Gravity#BOTTOM} to peek
    441      * the bottom drawer.
    442      */
    443     void peekDrawer(final int gravity) {
    444         if (!isLaidOut()) {
    445             // If this view is not laid out yet, postpone the peek until onLayout is called.
    446             if (Log.isLoggable(TAG, Log.DEBUG)) {
    447                 Log.d(TAG, "WearableDrawerLayout not laid out yet. Postponing peek.");
    448             }
    449             switch (gravity) {
    450                 case Gravity.TOP:
    451                     mShouldPeekTopDrawerAfterLayout = true;
    452                     break;
    453                 case Gravity.BOTTOM:
    454                     mShouldPeekBottomDrawerAfterLayout = true;
    455                     break;
    456                 default: // fall out
    457             }
    458             return;
    459         }
    460         final WearableDrawerView drawerView = findDrawerWithGravity(gravity);
    461         maybePeekDrawer(drawerView);
    462     }
    463 
    464     /**
    465      * Peek the given {@link WearableDrawerView}, which may either be the top drawer or bottom
    466      * drawer. This should only be used after the drawer has been added as a child of the {@link
    467      * WearableDrawerLayout}.
    468      */
    469     void peekDrawer(WearableDrawerView drawer) {
    470         if (drawer == null) {
    471             throw new IllegalArgumentException(
    472                     "peekDrawer(WearableDrawerView) received a null drawer.");
    473         } else if (drawer != mTopDrawerView && drawer != mBottomDrawerView) {
    474             throw new IllegalArgumentException(
    475                     "peekDrawer(WearableDrawerView) received a drawer that isn't a child.");
    476         }
    477 
    478         if (!isLaidOut()) {
    479             // If this view is not laid out yet, postpone the peek until onLayout is called.
    480             if (Log.isLoggable(TAG, Log.DEBUG)) {
    481                 Log.d(TAG, "WearableDrawerLayout not laid out yet. Postponing peek.");
    482             }
    483             if (drawer == mTopDrawerView) {
    484                 mShouldPeekTopDrawerAfterLayout = true;
    485             } else if (drawer == mBottomDrawerView) {
    486                 mShouldPeekBottomDrawerAfterLayout = true;
    487             }
    488             return;
    489         }
    490 
    491         maybePeekDrawer(drawer);
    492     }
    493 
    494     @Override
    495     public boolean onInterceptTouchEvent(MotionEvent ev) {
    496         // Do not intercept touch events if a drawer is open. If the content in a drawer scrolls,
    497         // then the touch event can be intercepted if the content in the drawer is scrolled to
    498         // the maximum opposite of the drawer's gravity (ex: the touch event can be intercepted
    499         // if the top drawer is open and scrolling content is at the bottom.
    500         if ((mBottomDrawerView != null && mBottomDrawerView.isOpened() && !mCanBottomDrawerBeClosed)
    501                 || (mTopDrawerView != null && mTopDrawerView.isOpened()
    502                 && !mCanTopDrawerBeClosed)) {
    503             mDrawerOpenLastInterceptedTouchEvent = ev;
    504             return false;
    505         }
    506 
    507         // Delegate event to drawer draggers.
    508         final boolean shouldInterceptTop = mTopDrawerDragger.shouldInterceptTouchEvent(ev);
    509         final boolean shouldInterceptBottom = mBottomDrawerDragger.shouldInterceptTouchEvent(ev);
    510         return shouldInterceptTop || shouldInterceptBottom;
    511     }
    512 
    513     @Override
    514     public boolean onTouchEvent(MotionEvent ev) {
    515         if (ev == null) {
    516             Log.w(TAG, "null MotionEvent passed to onTouchEvent");
    517             return false;
    518         }
    519         // Delegate event to drawer draggers.
    520         mTopDrawerDragger.processTouchEvent(ev);
    521         mBottomDrawerDragger.processTouchEvent(ev);
    522         return true;
    523     }
    524 
    525     @Override
    526     public void computeScroll() {
    527         // For scrolling the drawers.
    528         final boolean topSettling = mTopDrawerDragger.continueSettling(true /* deferCallbacks */);
    529         final boolean bottomSettling = mBottomDrawerDragger.continueSettling(true /*
    530         deferCallbacks */);
    531         if (topSettling || bottomSettling) {
    532             ViewCompat.postInvalidateOnAnimation(this);
    533         }
    534     }
    535 
    536     @Override
    537     public void addView(View child, int index, ViewGroup.LayoutParams params) {
    538         super.addView(child, index, params);
    539 
    540         if (!(child instanceof WearableDrawerView)) {
    541             return;
    542         }
    543 
    544         WearableDrawerView drawerChild = (WearableDrawerView) child;
    545         drawerChild.setDrawerController(new WearableDrawerController(this, drawerChild));
    546         int childGravity = ((FrameLayout.LayoutParams) params).gravity;
    547         // Check for preferential gravity if no gravity is set in the layout.
    548         if (childGravity == Gravity.NO_GRAVITY || childGravity == GRAVITY_UNDEFINED) {
    549             ((FrameLayout.LayoutParams) params).gravity = drawerChild.preferGravity();
    550             childGravity = drawerChild.preferGravity();
    551             drawerChild.setLayoutParams(params);
    552         }
    553         WearableDrawerView drawerView;
    554         if (childGravity == Gravity.TOP) {
    555             mTopDrawerView = drawerChild;
    556             drawerView = mTopDrawerView;
    557         } else if (childGravity == Gravity.BOTTOM) {
    558             mBottomDrawerView = drawerChild;
    559             drawerView = mBottomDrawerView;
    560         } else {
    561             drawerView = null;
    562         }
    563 
    564         if (drawerView != null) {
    565             drawerView.addOnLayoutChangeListener(this);
    566         }
    567     }
    568 
    569     @Override
    570     public void onLayoutChange(
    571             View v,
    572             int left,
    573             int top,
    574             int right,
    575             int bottom,
    576             int oldLeft,
    577             int oldTop,
    578             int oldRight,
    579             int oldBottom) {
    580         if (v == mTopDrawerView) {
    581             // Layout the top drawer base on the openedPercent. It is initially hidden.
    582             final float openedPercent = mTopDrawerView.getOpenedPercent();
    583             final int height = v.getHeight();
    584             final int childTop = -height + (int) (height * openedPercent);
    585             v.layout(v.getLeft(), childTop, v.getRight(), childTop + height);
    586         } else if (v == mBottomDrawerView) {
    587             // Layout the bottom drawer base on the openedPercent. It is initially hidden.
    588             final float openedPercent = mBottomDrawerView.getOpenedPercent();
    589             final int height = v.getHeight();
    590             final int childTop = (int) (getHeight() - height * openedPercent);
    591             v.layout(v.getLeft(), childTop, v.getRight(), childTop + height);
    592         }
    593     }
    594 
    595     /**
    596      * Sets a listener to be notified of drawer events.
    597      */
    598     public void setDrawerStateCallback(DrawerStateCallback callback) {
    599         mDrawerStateCallback = callback;
    600     }
    601 
    602     @Override
    603     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    604         super.onLayout(changed, left, top, right, bottom);
    605         if (mShouldPeekBottomDrawerAfterLayout
    606                 || mShouldPeekTopDrawerAfterLayout
    607                 || mShouldOpenTopDrawerAfterLayout
    608                 || mShouldOpenBottomDrawerAfterLayout) {
    609             getViewTreeObserver()
    610                     .addOnGlobalLayoutListener(
    611                             new OnGlobalLayoutListener() {
    612                                 @Override
    613                                 public void onGlobalLayout() {
    614                                     getViewTreeObserver().removeOnGlobalLayoutListener(this);
    615                                     if (mShouldOpenBottomDrawerAfterLayout) {
    616                                         openDrawerWithoutAnimation(mBottomDrawerView);
    617                                         mShouldOpenBottomDrawerAfterLayout = false;
    618                                     } else if (mShouldPeekBottomDrawerAfterLayout) {
    619                                         peekDrawer(Gravity.BOTTOM);
    620                                         mShouldPeekBottomDrawerAfterLayout = false;
    621                                     }
    622 
    623                                     if (mShouldOpenTopDrawerAfterLayout) {
    624                                         openDrawerWithoutAnimation(mTopDrawerView);
    625                                         mShouldOpenTopDrawerAfterLayout = false;
    626                                     } else if (mShouldPeekTopDrawerAfterLayout) {
    627                                         peekDrawer(Gravity.TOP);
    628                                         mShouldPeekTopDrawerAfterLayout = false;
    629                                     }
    630                                 }
    631                             });
    632         }
    633     }
    634 
    635     @Override
    636     public void onFlingComplete(View view) {
    637         boolean canTopPeek = mTopDrawerView != null && mTopDrawerView.isAutoPeekEnabled();
    638         boolean canBottomPeek = mBottomDrawerView != null && mBottomDrawerView.isAutoPeekEnabled();
    639         boolean canScrollUp = view.canScrollVertically(UP);
    640         boolean canScrollDown = view.canScrollVertically(DOWN);
    641 
    642         if (canTopPeek && !canScrollUp && !mTopDrawerView.isPeeking()) {
    643             peekDrawer(Gravity.TOP);
    644         }
    645         if (canBottomPeek && (!canScrollUp || !canScrollDown) && !mBottomDrawerView.isPeeking()) {
    646             peekDrawer(Gravity.BOTTOM);
    647         }
    648     }
    649 
    650     @Override // NestedScrollingParent
    651     public int getNestedScrollAxes() {
    652         return mNestedScrollingParentHelper.getNestedScrollAxes();
    653     }
    654 
    655     @Override // NestedScrollingParent
    656     public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
    657             boolean consumed) {
    658         return false;
    659     }
    660 
    661     @Override // NestedScrollingParent
    662     public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
    663         maybeUpdateScrollingContentView(target);
    664         mLastScrollWasFling = true;
    665 
    666         if (target == mScrollingContentView) {
    667             FlingWatcher flingWatcher = mFlingWatcher.getFor(mScrollingContentView);
    668             if (flingWatcher != null) {
    669                 flingWatcher.watch();
    670             }
    671         }
    672         // We do not want to intercept the child from receiving the fling, so return false.
    673         return false;
    674     }
    675 
    676     @Override // NestedScrollingParent
    677     public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
    678         maybeUpdateScrollingContentView(target);
    679     }
    680 
    681     @Override // NestedScrollingParent
    682     public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
    683             int dxUnconsumed, int dyUnconsumed) {
    684 
    685         boolean scrolledUp = dyConsumed < 0;
    686         boolean scrolledDown = dyConsumed > 0;
    687         boolean overScrolledUp = dyUnconsumed < 0;
    688         boolean overScrolledDown = dyUnconsumed > 0;
    689 
    690         // When the top drawer is open, we need to track whether it can be closed.
    691         if (mTopDrawerView != null && mTopDrawerView.isOpened()) {
    692             // When the top drawer is overscrolled down or cannot scroll down, we consider it to be
    693             // at the bottom of its content, so it can be closed.
    694             mCanTopDrawerBeClosed =
    695                     overScrolledDown || !mTopDrawerView.getDrawerContent()
    696                             .canScrollVertically(DOWN);
    697             // If the last scroll was a fling and the drawer can be closed, pass along the last
    698             // touch event to start closing the drawer. See the javadocs on mLastScrollWasFling
    699             // for more information.
    700             if (mCanTopDrawerBeClosed && mLastScrollWasFling) {
    701                 onTouchEvent(mDrawerOpenLastInterceptedTouchEvent);
    702             }
    703             mLastScrollWasFling = false;
    704             return;
    705         }
    706 
    707         // When the bottom drawer is open, we need to track whether it can be closed.
    708         if (mBottomDrawerView != null && mBottomDrawerView.isOpened()) {
    709             // When the bottom drawer is scrolled to the top of its content, it can be closed.
    710             mCanBottomDrawerBeClosed = overScrolledUp;
    711             // If the last scroll was a fling and the drawer can be closed, pass along the last
    712             // touch event to start closing the drawer. See the javadocs on mLastScrollWasFling
    713             // for more information.
    714             if (mCanBottomDrawerBeClosed && mLastScrollWasFling) {
    715                 onTouchEvent(mDrawerOpenLastInterceptedTouchEvent);
    716             }
    717             mLastScrollWasFling = false;
    718             return;
    719         }
    720 
    721         mLastScrollWasFling = false;
    722 
    723         // The following code assumes that neither drawer is open.
    724 
    725         // The bottom and top drawer are not open. Look at the scroll events to figure out whether
    726         // a drawer should peek, close it's peek, or do nothing.
    727         boolean canTopAutoPeek = mTopDrawerView != null && mTopDrawerView.isAutoPeekEnabled();
    728         boolean canBottomAutoPeek =
    729                 mBottomDrawerView != null && mBottomDrawerView.isAutoPeekEnabled();
    730         boolean isTopDrawerPeeking = mTopDrawerView != null && mTopDrawerView.isPeeking();
    731         boolean isBottomDrawerPeeking = mBottomDrawerView != null && mBottomDrawerView.isPeeking();
    732         boolean scrolledDownPastSlop = false;
    733         boolean shouldPeekOnScrollDown =
    734                 mBottomDrawerView != null && mBottomDrawerView.isPeekOnScrollDownEnabled();
    735         if (scrolledDown) {
    736             mCurrentNestedScrollSlopTracker += dyConsumed;
    737             scrolledDownPastSlop = mCurrentNestedScrollSlopTracker > mNestedScrollSlopPx;
    738         }
    739 
    740         if (canTopAutoPeek) {
    741             if (overScrolledUp && !isTopDrawerPeeking) {
    742                 peekDrawer(Gravity.TOP);
    743             } else if (scrolledDown && isTopDrawerPeeking && !isClosingPeek(mTopDrawerView)) {
    744                 closeDrawer(Gravity.TOP);
    745             }
    746         }
    747 
    748         if (canBottomAutoPeek) {
    749             if ((overScrolledDown || overScrolledUp) && !isBottomDrawerPeeking) {
    750                 peekDrawer(Gravity.BOTTOM);
    751             } else if (shouldPeekOnScrollDown && scrolledDownPastSlop && !isBottomDrawerPeeking) {
    752                 peekDrawer(Gravity.BOTTOM);
    753             } else if ((scrolledUp || (!shouldPeekOnScrollDown && scrolledDown))
    754                     && isBottomDrawerPeeking
    755                     && !isClosingPeek(mBottomDrawerView)) {
    756                 closeDrawer(mBottomDrawerView);
    757             }
    758         }
    759     }
    760 
    761     /**
    762      * Peeks the given drawer if it is not {@code null} and has a peek view.
    763      */
    764     private void maybePeekDrawer(WearableDrawerView drawerView) {
    765         if (drawerView == null) {
    766             return;
    767         }
    768         View peekView = drawerView.getPeekContainer();
    769         if (peekView == null) {
    770             return;
    771         }
    772 
    773         View drawerContent = drawerView.getDrawerContent();
    774         int layoutGravity = ((FrameLayout.LayoutParams) drawerView.getLayoutParams()).gravity;
    775         int gravity =
    776                 layoutGravity == Gravity.NO_GRAVITY ? drawerView.preferGravity() : layoutGravity;
    777 
    778         drawerView.setIsPeeking(true);
    779         peekView.setAlpha(1);
    780         peekView.setScaleX(1);
    781         peekView.setScaleY(1);
    782         peekView.setVisibility(VISIBLE);
    783         if (drawerContent != null) {
    784             drawerContent.setAlpha(0);
    785             drawerContent.setVisibility(GONE);
    786         }
    787 
    788         if (gravity == Gravity.BOTTOM) {
    789             mBottomDrawerDragger.smoothSlideViewTo(
    790                     drawerView, 0 /* finalLeft */, getHeight() - peekView.getHeight());
    791         } else if (gravity == Gravity.TOP) {
    792             mTopDrawerDragger.smoothSlideViewTo(
    793                     drawerView, 0 /* finalLeft */,
    794                     -(drawerView.getHeight() - peekView.getHeight()));
    795             if (!mIsAccessibilityEnabled) {
    796                 // Don't automatically close the top drawer when in accessibility mode.
    797                 closeDrawerDelayed(gravity, PEEK_AUTO_CLOSE_DELAY_MS);
    798             }
    799         }
    800 
    801         invalidate();
    802     }
    803 
    804     private void openDrawerWithoutAnimation(WearableDrawerView drawer) {
    805         if (drawer == null) {
    806             return;
    807         }
    808 
    809         int offset;
    810         if (drawer == mTopDrawerView) {
    811             offset = mTopDrawerView.getHeight();
    812         } else if (drawer == mBottomDrawerView) {
    813             offset = -mBottomDrawerView.getHeight();
    814         } else {
    815             Log.w(TAG, "openDrawer(View) should be passed in the top or bottom drawer");
    816             return;
    817         }
    818 
    819         drawer.offsetTopAndBottom(offset);
    820         drawer.setOpenedPercent(1f);
    821         drawer.onDrawerOpened();
    822         if (mDrawerStateCallback != null) {
    823             mDrawerStateCallback.onDrawerOpened(this, drawer);
    824         }
    825         showDrawerContentMaybeAnimate(drawer);
    826         invalidate();
    827     }
    828 
    829     /**
    830      * @param gravity the gravity of the child to return.
    831      * @return the drawer with the specified gravity
    832      */
    833     @Nullable
    834     private WearableDrawerView findDrawerWithGravity(int gravity) {
    835         switch (gravity) {
    836             case Gravity.TOP:
    837                 return mTopDrawerView;
    838             case Gravity.BOTTOM:
    839                 return mBottomDrawerView;
    840             default:
    841                 Log.w(TAG, "Invalid drawer gravity: " + gravity);
    842                 return null;
    843         }
    844     }
    845 
    846     /**
    847      * Updates {@link #mScrollingContentView} if {@code view} is not a descendant of a {@link
    848      * WearableDrawerView}.
    849      */
    850     private void maybeUpdateScrollingContentView(View view) {
    851         if (view != mScrollingContentView && !isDrawerOrChildOfDrawer(view)) {
    852             mScrollingContentView = view;
    853         }
    854     }
    855 
    856     /**
    857      * Returns {@code true} if {@code view} is a descendant of a {@link WearableDrawerView}.
    858      */
    859     private boolean isDrawerOrChildOfDrawer(View view) {
    860         while (view != null && view != this) {
    861             if (view instanceof WearableDrawerView) {
    862                 return true;
    863             }
    864 
    865             view = (View) view.getParent();
    866         }
    867 
    868         return false;
    869     }
    870 
    871     private boolean isClosingPeek(WearableDrawerView drawerView) {
    872         return drawerView != null && drawerView.getDrawerState() == STATE_SETTLING;
    873     }
    874 
    875     @Override // NestedScrollingParent
    876     public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
    877             int nestedScrollAxes) {
    878         mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    879     }
    880 
    881     @Override // NestedScrollingParent
    882     public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
    883             int nestedScrollAxes) {
    884         mCurrentNestedScrollSlopTracker = 0;
    885         return true;
    886     }
    887 
    888     @Override // NestedScrollingParent
    889     public void onStopNestedScroll(@NonNull View target) {
    890         mNestedScrollingParentHelper.onStopNestedScroll(target);
    891     }
    892 
    893     private boolean canDrawerContentScrollVertically(
    894             @Nullable WearableDrawerView drawerView, int direction) {
    895         if (drawerView == null) {
    896             return false;
    897         }
    898 
    899         View drawerContent = drawerView.getDrawerContent();
    900         if (drawerContent == null) {
    901             return false;
    902         }
    903 
    904         return drawerContent.canScrollVertically(direction);
    905     }
    906 
    907     /**
    908      * Listener for monitoring events about drawers.
    909      */
    910     public static class DrawerStateCallback {
    911 
    912         /**
    913          * Called when a drawer has settled in a completely open state. The drawer is interactive at
    914          * this point.
    915          */
    916         public void onDrawerOpened(WearableDrawerLayout layout, WearableDrawerView drawerView) {
    917         }
    918 
    919         /**
    920          * Called when a drawer has settled in a completely closed state.
    921          */
    922         public void onDrawerClosed(WearableDrawerLayout layout, WearableDrawerView drawerView) {
    923         }
    924 
    925         /**
    926          * Called when the drawer motion state changes. The new state will be one of {@link
    927          * WearableDrawerView#STATE_IDLE}, {@link WearableDrawerView#STATE_DRAGGING} or {@link
    928          * WearableDrawerView#STATE_SETTLING}.
    929          */
    930         public void onDrawerStateChanged(WearableDrawerLayout layout, @DrawerState int newState) {
    931         }
    932     }
    933 
    934     private void allowAccessibilityFocusOnAllChildren() {
    935         if (!mIsAccessibilityEnabled) {
    936             return;
    937         }
    938 
    939         for (int i = 0; i < getChildCount(); i++) {
    940             getChildAt(i).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
    941         }
    942     }
    943 
    944     private void allowAccessibilityFocusOnOnly(WearableDrawerView drawer) {
    945         if (!mIsAccessibilityEnabled) {
    946             return;
    947         }
    948 
    949         for (int i = 0; i < getChildCount(); i++) {
    950             View child = getChildAt(i);
    951             if (child != drawer) {
    952                 child.setImportantForAccessibility(
    953                         View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    954             }
    955         }
    956     }
    957 
    958     /**
    959      * Base class for top and bottom drawer dragger callbacks.
    960      */
    961     private abstract class DrawerDraggerCallback extends ViewDragHelper.Callback {
    962 
    963         public abstract WearableDrawerView getDrawerView();
    964 
    965         @Override
    966         public boolean tryCaptureView(@NonNull View child, int pointerId) {
    967             WearableDrawerView drawerView = getDrawerView();
    968             // Returns true if the dragger is dragging the drawer.
    969             return child == drawerView && !drawerView.isLocked()
    970                     && drawerView.getDrawerContent() != null;
    971         }
    972 
    973         @Override
    974         public int getViewVerticalDragRange(@NonNull View child) {
    975             // Defines the vertical drag range of the drawer.
    976             return child == getDrawerView() ? child.getHeight() : 0;
    977         }
    978 
    979         @Override
    980         public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
    981             showDrawerContentMaybeAnimate((WearableDrawerView) capturedChild);
    982         }
    983 
    984         @Override
    985         public void onViewDragStateChanged(int state) {
    986             final WearableDrawerView drawerView = getDrawerView();
    987             switch (state) {
    988                 case ViewDragHelper.STATE_IDLE:
    989                     boolean openedOrClosed = false;
    990                     if (drawerView.isOpened()) {
    991                         openedOrClosed = true;
    992                         drawerView.onDrawerOpened();
    993                         allowAccessibilityFocusOnOnly(drawerView);
    994                         if (mDrawerStateCallback != null) {
    995                             mDrawerStateCallback
    996                                     .onDrawerOpened(WearableDrawerLayout.this, drawerView);
    997                         }
    998 
    999                         // Drawers can be closed if a drag to close them will not cause a scroll.
   1000                         mCanTopDrawerBeClosed = !canDrawerContentScrollVertically(mTopDrawerView,
   1001                                 DOWN);
   1002                         mCanBottomDrawerBeClosed = !canDrawerContentScrollVertically(
   1003                                 mBottomDrawerView, UP);
   1004                     } else if (drawerView.isClosed()) {
   1005                         openedOrClosed = true;
   1006                         drawerView.onDrawerClosed();
   1007                         allowAccessibilityFocusOnAllChildren();
   1008                         if (mDrawerStateCallback != null) {
   1009                             mDrawerStateCallback
   1010                                     .onDrawerClosed(WearableDrawerLayout.this, drawerView);
   1011                         }
   1012                     } else { // drawerView is peeking
   1013                         allowAccessibilityFocusOnAllChildren();
   1014                     }
   1015 
   1016                     // If the drawer is fully opened or closed, change it to non-peeking mode.
   1017                     if (openedOrClosed && drawerView.isPeeking()) {
   1018                         drawerView.setIsPeeking(false);
   1019                         drawerView.getPeekContainer().setVisibility(INVISIBLE);
   1020                     }
   1021                     break;
   1022                 default: // fall out
   1023             }
   1024 
   1025             if (drawerView.getDrawerState() != state) {
   1026                 drawerView.setDrawerState(state);
   1027                 drawerView.onDrawerStateChanged(state);
   1028                 if (mDrawerStateCallback != null) {
   1029                     mDrawerStateCallback.onDrawerStateChanged(WearableDrawerLayout.this, state);
   1030                 }
   1031             }
   1032         }
   1033     }
   1034 
   1035     /**
   1036      * For communicating with top drawer view dragger.
   1037      */
   1038     private class TopDrawerDraggerCallback extends DrawerDraggerCallback {
   1039 
   1040         @Override
   1041         public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
   1042             if (mTopDrawerView == child) {
   1043                 int peekHeight = mTopDrawerView.getPeekContainer().getHeight();
   1044                 // The top drawer can be dragged vertically from peekHeight - height to 0.
   1045                 return Math.max(peekHeight - child.getHeight(), Math.min(top, 0));
   1046             }
   1047             return 0;
   1048         }
   1049 
   1050         @Override
   1051         public void onEdgeDragStarted(int edgeFlags, int pointerId) {
   1052             if (mTopDrawerView != null
   1053                     && edgeFlags == ViewDragHelper.EDGE_TOP
   1054                     && !mTopDrawerView.isLocked()
   1055                     && (mBottomDrawerView == null || !mBottomDrawerView.isOpened())
   1056                     && mTopDrawerView.getDrawerContent() != null) {
   1057 
   1058                 boolean atTop =
   1059                         mScrollingContentView == null || !mScrollingContentView
   1060                                 .canScrollVertically(UP);
   1061                 if (!mTopDrawerView.isOpenOnlyAtTopEnabled() || atTop) {
   1062                     mTopDrawerDragger.captureChildView(mTopDrawerView, pointerId);
   1063                 }
   1064             }
   1065         }
   1066 
   1067         @Override
   1068         public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
   1069             if (releasedChild == mTopDrawerView) {
   1070                 // Settle to final position. Either swipe open or close.
   1071                 final float openedPercent = mTopDrawerView.getOpenedPercent();
   1072 
   1073                 final int finalTop;
   1074                 if (yvel > 0 || (yvel == 0 && openedPercent > OPENED_PERCENT_THRESHOLD)) {
   1075                     // Drawer was being flung open or drawer is mostly open, so finish opening.
   1076                     finalTop = 0;
   1077                 } else {
   1078                     // Drawer should be closed to its peek state.
   1079                     animatePeekVisibleAfterBeingClosed(mTopDrawerView);
   1080                     finalTop = mTopDrawerView.getPeekContainer().getHeight() - releasedChild
   1081                             .getHeight();
   1082                 }
   1083 
   1084                 mTopDrawerDragger.settleCapturedViewAt(0 /* finalLeft */, finalTop);
   1085                 invalidate();
   1086             }
   1087         }
   1088 
   1089         @Override
   1090         public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
   1091                 int dy) {
   1092             if (changedView == mTopDrawerView) {
   1093                 // Compute the offset and invalidate will move the drawer during layout.
   1094                 final int height = changedView.getHeight();
   1095                 mTopDrawerView.setOpenedPercent((float) (top + height) / height);
   1096                 invalidate();
   1097             }
   1098         }
   1099 
   1100         @Override
   1101         public WearableDrawerView getDrawerView() {
   1102             return mTopDrawerView;
   1103         }
   1104     }
   1105 
   1106     /**
   1107      * For communicating with bottom drawer view dragger.
   1108      */
   1109     private class BottomDrawerDraggerCallback extends DrawerDraggerCallback {
   1110 
   1111         @Override
   1112         public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
   1113             if (mBottomDrawerView == child) {
   1114                 // The bottom drawer can be dragged vertically from (parentHeight - height) to
   1115                 // (parentHeight - peekHeight).
   1116                 int parentHeight = getHeight();
   1117                 int peekHeight = mBottomDrawerView.getPeekContainer().getHeight();
   1118                 return Math.max(parentHeight - child.getHeight(),
   1119                         Math.min(top, parentHeight - peekHeight));
   1120             }
   1121             return 0;
   1122         }
   1123 
   1124         @Override
   1125         public void onEdgeDragStarted(int edgeFlags, int pointerId) {
   1126             if (mBottomDrawerView != null
   1127                     && edgeFlags == ViewDragHelper.EDGE_BOTTOM
   1128                     && !mBottomDrawerView.isLocked()
   1129                     && (mTopDrawerView == null || !mTopDrawerView.isOpened())
   1130                     && mBottomDrawerView.getDrawerContent() != null) {
   1131                 // Tells the dragger which view to start dragging.
   1132                 mBottomDrawerDragger.captureChildView(mBottomDrawerView, pointerId);
   1133             }
   1134         }
   1135 
   1136         @Override
   1137         public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
   1138             if (releasedChild == mBottomDrawerView) {
   1139                 // Settle to final position. Either swipe open or close.
   1140                 final int parentHeight = getHeight();
   1141                 final float openedPercent = mBottomDrawerView.getOpenedPercent();
   1142                 final int finalTop;
   1143                 if (yvel < 0 || (yvel == 0 && openedPercent > OPENED_PERCENT_THRESHOLD)) {
   1144                     // Drawer was being flung open or drawer is mostly open, so finish opening it.
   1145                     finalTop = parentHeight - releasedChild.getHeight();
   1146                 } else {
   1147                     // Drawer should be closed to its peek state.
   1148                     animatePeekVisibleAfterBeingClosed(mBottomDrawerView);
   1149                     finalTop = getHeight() - mBottomDrawerView.getPeekContainer().getHeight();
   1150                 }
   1151                 mBottomDrawerDragger.settleCapturedViewAt(0 /* finalLeft */, finalTop);
   1152                 invalidate();
   1153             }
   1154         }
   1155 
   1156         @Override
   1157         public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
   1158                 int dy) {
   1159             if (changedView == mBottomDrawerView) {
   1160                 // Compute the offset and invalidate will move the drawer during layout.
   1161                 final int height = changedView.getHeight();
   1162                 final int parentHeight = getHeight();
   1163 
   1164                 mBottomDrawerView.setOpenedPercent((float) (parentHeight - top) / height);
   1165                 invalidate();
   1166             }
   1167         }
   1168 
   1169         @Override
   1170         public WearableDrawerView getDrawerView() {
   1171             return mBottomDrawerView;
   1172         }
   1173     }
   1174 
   1175     /**
   1176      * Runnable that closes the given drawer if it is just peeking.
   1177      */
   1178     private class ClosePeekRunnable implements Runnable {
   1179 
   1180         private final int mGravity;
   1181 
   1182         private ClosePeekRunnable(int gravity) {
   1183             mGravity = gravity;
   1184         }
   1185 
   1186         @Override
   1187         public void run() {
   1188             WearableDrawerView drawer = findDrawerWithGravity(mGravity);
   1189             if (drawer != null
   1190                     && !drawer.isOpened()
   1191                     && drawer.getDrawerState() == STATE_IDLE) {
   1192                 closeDrawer(mGravity);
   1193             }
   1194         }
   1195     }
   1196 }
   1197