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