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
17package android.support.wear.widget.drawer;
18
19import static android.support.wear.widget.drawer.WearableDrawerView.STATE_IDLE;
20import static android.support.wear.widget.drawer.WearableDrawerView.STATE_SETTLING;
21
22import android.annotation.TargetApi;
23import android.content.Context;
24import android.os.Build;
25import android.os.Handler;
26import android.os.Looper;
27import android.support.annotation.Nullable;
28import android.support.annotation.VisibleForTesting;
29import android.support.v4.view.NestedScrollingParent;
30import android.support.v4.view.NestedScrollingParentHelper;
31import android.support.v4.view.ViewCompat;
32import android.support.v4.widget.ViewDragHelper;
33import android.support.wear.widget.drawer.FlingWatcherFactory.FlingListener;
34import android.support.wear.widget.drawer.FlingWatcherFactory.FlingWatcher;
35import android.support.wear.widget.drawer.WearableDrawerView.DrawerState;
36import android.util.AttributeSet;
37import android.util.DisplayMetrics;
38import android.util.Log;
39import android.view.Gravity;
40import android.view.MotionEvent;
41import android.view.View;
42import android.view.ViewGroup;
43import android.view.ViewTreeObserver.OnGlobalLayoutListener;
44import android.view.WindowInsets;
45import android.view.WindowManager;
46import android.view.accessibility.AccessibilityManager;
47import android.widget.FrameLayout;
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 android.support.v4.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;android.support.wear.widget.drawer.WearableDrawerLayout [...]&gt;
63 *     &lt;FrameLayout android:id=”@+id/content” /&gt;
64 *
65 *     &lt;android.support.wear.widget.drawer.WearableNavigationDrawerView
66 *         android:layout_width=”match_parent”
67 *         android:layout_height=”match_parent” /&gt;
68 *
69 *     &lt;android.support.wear.widget.drawer.WearableActionDrawerView
70 *         android:layout_width=”match_parent”
71 *         android:layout_height=”match_parent” /&gt;
72 *
73 * &lt;/android.support.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;android.support.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;android.support.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;/android.support.wear.widget.drawer.WearableDrawerView&gt;
99 * &lt;/android.support.wear.widget.drawer.WearableDrawerLayout&gt;</pre>
100 */
101@TargetApi(Build.VERSION_CODES.M)
102public 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    private final ViewDragHelper mTopDrawerDragger;
149    /**
150     * Helper for dragging the bottom drawer.
151     */
152    private 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 private WearableDrawerView mTopDrawerView;
163    /**
164     * Bottom drawer view.
165     */
166    @Nullable private WearableDrawerView mBottomDrawerView;
167    /**
168     * What we have inferred the scrolling content view to be, should one exist.
169     */
170    @Nullable private View mScrollingContentView;
171    /**
172     * Listens to drawer events.
173     */
174    private 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    private boolean mShouldOpenTopDrawerAfterLayout;
185    /**
186     * Tracks whether the bottom drawer should be opened after layout.
187     */
188    private boolean mShouldOpenBottomDrawerAfterLayout;
189    /**
190     * Tracks whether the top drawer should be peeked after layout.
191     */
192    private boolean mShouldPeekTopDrawerAfterLayout;
193    /**
194     * Tracks whether the bottom drawer should be peeked after layout.
195     */
196    private 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    private 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    private 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
221    public WearableDrawerLayout(Context context) {
222        this(context, null);
223    }
224
225    public WearableDrawerLayout(Context context, AttributeSet attrs) {
226        this(context, attrs, 0);
227    }
228
229    public WearableDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
230        this(context, attrs, defStyleAttr, 0);
231    }
232
233    public WearableDrawerLayout(
234            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
235        super(context, attrs, defStyleAttr, defStyleRes);
236
237        mFlingWatcher = new FlingWatcherFactory(this);
238        mTopDrawerDraggerCallback = new TopDrawerDraggerCallback();
239        mTopDrawerDragger =
240                ViewDragHelper.create(this, 1f /* sensitivity */, mTopDrawerDraggerCallback);
241        mTopDrawerDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP);
242
243        mBottomDrawerDraggerCallback = new BottomDrawerDraggerCallback();
244        mBottomDrawerDragger =
245                ViewDragHelper.create(this, 1f /* sensitivity */, mBottomDrawerDraggerCallback);
246        mBottomDrawerDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_BOTTOM);
247
248        WindowManager windowManager = (WindowManager) context
249                .getSystemService(Context.WINDOW_SERVICE);
250        DisplayMetrics metrics = new DisplayMetrics();
251        windowManager.getDefaultDisplay().getMetrics(metrics);
252        mNestedScrollSlopPx = Math.round(metrics.density * NESTED_SCROLL_SLOP_DP);
253
254        AccessibilityManager accessibilityManager =
255                (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
256        mIsAccessibilityEnabled = accessibilityManager.isEnabled();
257    }
258
259    private static void animatePeekVisibleAfterBeingClosed(WearableDrawerView drawer) {
260        final View content = drawer.getDrawerContent();
261        if (content != null) {
262            content.animate()
263                    .setDuration(PEEK_FADE_DURATION_MS)
264                    .alpha(0)
265                    .withEndAction(
266                            new Runnable() {
267                                @Override
268                                public void run() {
269                                    content.setVisibility(GONE);
270                                }
271                            })
272                    .start();
273        }
274
275        ViewGroup peek = drawer.getPeekContainer();
276        peek.setVisibility(VISIBLE);
277        peek.animate()
278                .setStartDelay(PEEK_FADE_DURATION_MS)
279                .setDuration(PEEK_FADE_DURATION_MS)
280                .alpha(1)
281                .scaleX(1)
282                .scaleY(1)
283                .start();
284
285        drawer.setIsPeeking(true);
286    }
287
288    /**
289     * Shows the drawer's contents. If the drawer is peeking, an animation is used to fade out the
290     * peek view and fade in the drawer content.
291     */
292    private static void showDrawerContentMaybeAnimate(WearableDrawerView drawerView) {
293        drawerView.bringToFront();
294        final View contentView = drawerView.getDrawerContent();
295        if (contentView != null) {
296            contentView.setVisibility(VISIBLE);
297        }
298
299        if (drawerView.isPeeking()) {
300            final View peekView = drawerView.getPeekContainer();
301            peekView.animate().alpha(0).scaleX(0).scaleY(0).setDuration(PEEK_FADE_DURATION_MS)
302                    .start();
303
304            if (contentView != null) {
305                contentView.setAlpha(0);
306                contentView
307                        .animate()
308                        .setStartDelay(PEEK_FADE_DURATION_MS)
309                        .alpha(1)
310                        .setDuration(PEEK_FADE_DURATION_MS)
311                        .start();
312            }
313        } else {
314            drawerView.getPeekContainer().setAlpha(0);
315            if (contentView != null) {
316                contentView.setAlpha(1);
317            }
318        }
319    }
320
321    @Override
322    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
323        mSystemWindowInsetBottom = insets.getSystemWindowInsetBottom();
324
325        if (mSystemWindowInsetBottom != 0) {
326            MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
327            layoutParams.bottomMargin = mSystemWindowInsetBottom;
328            setLayoutParams(layoutParams);
329        }
330
331        return super.onApplyWindowInsets(insets);
332    }
333
334    /**
335     * Closes drawer after {@code delayMs} milliseconds.
336     */
337    private void closeDrawerDelayed(final int gravity, long delayMs) {
338        switch (gravity) {
339            case Gravity.TOP:
340                mMainThreadHandler.removeCallbacks(mCloseTopPeekRunnable);
341                mMainThreadHandler.postDelayed(mCloseTopPeekRunnable, delayMs);
342                break;
343            case Gravity.BOTTOM:
344                mMainThreadHandler.removeCallbacks(mCloseBottomPeekRunnable);
345                mMainThreadHandler.postDelayed(mCloseBottomPeekRunnable, delayMs);
346                break;
347            default:
348                Log.w(TAG, "Invoked a delayed drawer close with an invalid gravity: " + gravity);
349        }
350    }
351
352    /**
353     * Close the specified drawer by animating it out of view.
354     *
355     * @param gravity Gravity.TOP to move the top drawer or Gravity.BOTTOM for the bottom.
356     */
357    void closeDrawer(int gravity) {
358        closeDrawer(findDrawerWithGravity(gravity));
359    }
360
361    /**
362     * Close the specified drawer by animating it out of view.
363     *
364     * @param drawer The drawer view to close.
365     */
366    void closeDrawer(WearableDrawerView drawer) {
367        if (drawer == null) {
368            return;
369        }
370        if (drawer == mTopDrawerView) {
371            mTopDrawerDragger.smoothSlideViewTo(
372                    mTopDrawerView, 0 /* finalLeft */, -mTopDrawerView.getHeight());
373            invalidate();
374        } else if (drawer == mBottomDrawerView) {
375            mBottomDrawerDragger
376                    .smoothSlideViewTo(mBottomDrawerView, 0 /* finalLeft */, getHeight());
377            invalidate();
378        } else {
379            Log.w(TAG, "closeDrawer(View) should be passed in the top or bottom drawer");
380        }
381    }
382
383    /**
384     * Open the specified drawer by animating it into view.
385     *
386     * @param gravity Gravity.TOP to move the top drawer or Gravity.BOTTOM for the bottom.
387     */
388    void openDrawer(int gravity) {
389        if (!isLaidOut()) {
390            switch (gravity) {
391                case Gravity.TOP:
392                    mShouldOpenTopDrawerAfterLayout = true;
393                    break;
394                case Gravity.BOTTOM:
395                    mShouldOpenBottomDrawerAfterLayout = true;
396                    break;
397                default: // fall out
398            }
399            return;
400        }
401        openDrawer(findDrawerWithGravity(gravity));
402    }
403
404    /**
405     * Open the specified drawer by animating it into view.
406     *
407     * @param drawer The drawer view to open.
408     */
409    void openDrawer(WearableDrawerView drawer) {
410        if (drawer == null) {
411            return;
412        }
413        if (!isLaidOut()) {
414            if (drawer == mTopDrawerView) {
415                mShouldOpenTopDrawerAfterLayout = true;
416            } else if (drawer == mBottomDrawerView) {
417                mShouldOpenBottomDrawerAfterLayout = true;
418            }
419            return;
420        }
421
422        if (drawer == mTopDrawerView) {
423            mTopDrawerDragger
424                    .smoothSlideViewTo(mTopDrawerView, 0 /* finalLeft */, 0 /* finalTop */);
425            showDrawerContentMaybeAnimate(mTopDrawerView);
426            invalidate();
427        } else if (drawer == mBottomDrawerView) {
428            mBottomDrawerDragger.smoothSlideViewTo(
429                    mBottomDrawerView, 0 /* finalLeft */,
430                    getHeight() - mBottomDrawerView.getHeight());
431            showDrawerContentMaybeAnimate(mBottomDrawerView);
432            invalidate();
433        } else {
434            Log.w(TAG, "openDrawer(View) should be passed in the top or bottom drawer");
435        }
436    }
437
438    /**
439     * Peek the drawer.
440     *
441     * @param gravity {@link Gravity#TOP} to peek the top drawer or {@link Gravity#BOTTOM} to peek
442     * the bottom drawer.
443     */
444    void peekDrawer(final int gravity) {
445        if (!isLaidOut()) {
446            // If this view is not laid out yet, postpone the peek until onLayout is called.
447            if (Log.isLoggable(TAG, Log.DEBUG)) {
448                Log.d(TAG, "WearableDrawerLayout not laid out yet. Postponing peek.");
449            }
450            switch (gravity) {
451                case Gravity.TOP:
452                    mShouldPeekTopDrawerAfterLayout = true;
453                    break;
454                case Gravity.BOTTOM:
455                    mShouldPeekBottomDrawerAfterLayout = true;
456                    break;
457                default: // fall out
458            }
459            return;
460        }
461        final WearableDrawerView drawerView = findDrawerWithGravity(gravity);
462        maybePeekDrawer(drawerView);
463    }
464
465    /**
466     * Peek the given {@link WearableDrawerView}, which may either be the top drawer or bottom
467     * drawer. This should only be used after the drawer has been added as a child of the {@link
468     * WearableDrawerLayout}.
469     */
470    void peekDrawer(WearableDrawerView drawer) {
471        if (drawer == null) {
472            throw new IllegalArgumentException(
473                    "peekDrawer(WearableDrawerView) received a null drawer.");
474        } else if (drawer != mTopDrawerView && drawer != mBottomDrawerView) {
475            throw new IllegalArgumentException(
476                    "peekDrawer(WearableDrawerView) received a drawer that isn't a child.");
477        }
478
479        if (!isLaidOut()) {
480            // If this view is not laid out yet, postpone the peek until onLayout is called.
481            if (Log.isLoggable(TAG, Log.DEBUG)) {
482                Log.d(TAG, "WearableDrawerLayout not laid out yet. Postponing peek.");
483            }
484            if (drawer == mTopDrawerView) {
485                mShouldPeekTopDrawerAfterLayout = true;
486            } else if (drawer == mBottomDrawerView) {
487                mShouldPeekBottomDrawerAfterLayout = true;
488            }
489            return;
490        }
491
492        maybePeekDrawer(drawer);
493    }
494
495    @Override
496    public boolean onInterceptTouchEvent(MotionEvent ev) {
497        // Do not intercept touch events if a drawer is open. If the content in a drawer scrolls,
498        // then the touch event can be intercepted if the content in the drawer is scrolled to
499        // the maximum opposite of the drawer's gravity (ex: the touch event can be intercepted
500        // if the top drawer is open and scrolling content is at the bottom.
501        if ((mBottomDrawerView != null && mBottomDrawerView.isOpened() && !mCanBottomDrawerBeClosed)
502                || (mTopDrawerView != null && mTopDrawerView.isOpened()
503                && !mCanTopDrawerBeClosed)) {
504            mDrawerOpenLastInterceptedTouchEvent = ev;
505            return false;
506        }
507
508        // Delegate event to drawer draggers.
509        final boolean shouldInterceptTop = mTopDrawerDragger.shouldInterceptTouchEvent(ev);
510        final boolean shouldInterceptBottom = mBottomDrawerDragger.shouldInterceptTouchEvent(ev);
511        return shouldInterceptTop || shouldInterceptBottom;
512    }
513
514    @Override
515    public boolean onTouchEvent(MotionEvent ev) {
516        if (ev == null) {
517            Log.w(TAG, "null MotionEvent passed to onTouchEvent");
518            return false;
519        }
520        // Delegate event to drawer draggers.
521        mTopDrawerDragger.processTouchEvent(ev);
522        mBottomDrawerDragger.processTouchEvent(ev);
523        return true;
524    }
525
526    @Override
527    public void computeScroll() {
528        // For scrolling the drawers.
529        final boolean topSettling = mTopDrawerDragger.continueSettling(true /* deferCallbacks */);
530        final boolean bottomSettling = mBottomDrawerDragger.continueSettling(true /*
531        deferCallbacks */);
532        if (topSettling || bottomSettling) {
533            ViewCompat.postInvalidateOnAnimation(this);
534        }
535    }
536
537    @Override
538    public void addView(View child, int index, ViewGroup.LayoutParams params) {
539        super.addView(child, index, params);
540
541        if (!(child instanceof WearableDrawerView)) {
542            return;
543        }
544
545        WearableDrawerView drawerChild = (WearableDrawerView) child;
546        drawerChild.setDrawerController(new WearableDrawerController(this, drawerChild));
547        int childGravity = ((FrameLayout.LayoutParams) params).gravity;
548        // Check for preferential gravity if no gravity is set in the layout.
549        if (childGravity == Gravity.NO_GRAVITY || childGravity == GRAVITY_UNDEFINED) {
550            ((FrameLayout.LayoutParams) params).gravity = drawerChild.preferGravity();
551            childGravity = drawerChild.preferGravity();
552            drawerChild.setLayoutParams(params);
553        }
554        WearableDrawerView drawerView;
555        if (childGravity == Gravity.TOP) {
556            mTopDrawerView = drawerChild;
557            drawerView = mTopDrawerView;
558        } else if (childGravity == Gravity.BOTTOM) {
559            mBottomDrawerView = drawerChild;
560            drawerView = mBottomDrawerView;
561        } else {
562            drawerView = null;
563        }
564
565        if (drawerView != null) {
566            drawerView.addOnLayoutChangeListener(this);
567        }
568    }
569
570    @Override
571    public void onLayoutChange(
572            View v,
573            int left,
574            int top,
575            int right,
576            int bottom,
577            int oldLeft,
578            int oldTop,
579            int oldRight,
580            int oldBottom) {
581        if (v == mTopDrawerView) {
582            // Layout the top drawer base on the openedPercent. It is initially hidden.
583            final float openedPercent = mTopDrawerView.getOpenedPercent();
584            final int height = v.getHeight();
585            final int childTop = -height + (int) (height * openedPercent);
586            v.layout(v.getLeft(), childTop, v.getRight(), childTop + height);
587        } else if (v == mBottomDrawerView) {
588            // Layout the bottom drawer base on the openedPercent. It is initially hidden.
589            final float openedPercent = mBottomDrawerView.getOpenedPercent();
590            final int height = v.getHeight();
591            final int childTop = (int) (getHeight() - height * openedPercent);
592            v.layout(v.getLeft(), childTop, v.getRight(), childTop + height);
593        }
594    }
595
596    /**
597     * Sets a listener to be notified of drawer events.
598     */
599    public void setDrawerStateCallback(DrawerStateCallback callback) {
600        mDrawerStateCallback = callback;
601    }
602
603    @Override
604    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
605        super.onLayout(changed, left, top, right, bottom);
606        if (mShouldPeekBottomDrawerAfterLayout
607                || mShouldPeekTopDrawerAfterLayout
608                || mShouldOpenTopDrawerAfterLayout
609                || mShouldOpenBottomDrawerAfterLayout) {
610            getViewTreeObserver()
611                    .addOnGlobalLayoutListener(
612                            new OnGlobalLayoutListener() {
613                                @Override
614                                public void onGlobalLayout() {
615                                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
616                                    if (mShouldOpenBottomDrawerAfterLayout) {
617                                        openDrawerWithoutAnimation(mBottomDrawerView);
618                                        mShouldOpenBottomDrawerAfterLayout = false;
619                                    } else if (mShouldPeekBottomDrawerAfterLayout) {
620                                        peekDrawer(Gravity.BOTTOM);
621                                        mShouldPeekBottomDrawerAfterLayout = false;
622                                    }
623
624                                    if (mShouldOpenTopDrawerAfterLayout) {
625                                        openDrawerWithoutAnimation(mTopDrawerView);
626                                        mShouldOpenTopDrawerAfterLayout = false;
627                                    } else if (mShouldPeekTopDrawerAfterLayout) {
628                                        peekDrawer(Gravity.TOP);
629                                        mShouldPeekTopDrawerAfterLayout = false;
630                                    }
631                                }
632                            });
633        }
634    }
635
636    @Override
637    public void onFlingComplete(View view) {
638        boolean canTopPeek = mTopDrawerView != null && mTopDrawerView.isAutoPeekEnabled();
639        boolean canBottomPeek = mBottomDrawerView != null && mBottomDrawerView.isAutoPeekEnabled();
640        boolean canScrollUp = view.canScrollVertically(UP);
641        boolean canScrollDown = view.canScrollVertically(DOWN);
642
643        if (canTopPeek && !canScrollUp && !mTopDrawerView.isPeeking()) {
644            peekDrawer(Gravity.TOP);
645        }
646        if (canBottomPeek && (!canScrollUp || !canScrollDown) && !mBottomDrawerView.isPeeking()) {
647            peekDrawer(Gravity.BOTTOM);
648        }
649    }
650
651    @Override // NestedScrollingParent
652    public int getNestedScrollAxes() {
653        return mNestedScrollingParentHelper.getNestedScrollAxes();
654    }
655
656    @Override // NestedScrollingParent
657    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
658        return false;
659    }
660
661    @Override // NestedScrollingParent
662    public boolean onNestedPreFling(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(View target, int dx, int dy, int[] consumed) {
678        maybeUpdateScrollingContentView(target);
679    }
680
681    @Override // NestedScrollingParent
682    public void onNestedScroll(
683            View target, int dxConsumed, int dyConsumed, 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(View child, View target, int nestedScrollAxes) {
877        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
878    }
879
880    @Override // NestedScrollingParent
881    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
882        mCurrentNestedScrollSlopTracker = 0;
883        return true;
884    }
885
886    @Override // NestedScrollingParent
887    public void onStopNestedScroll(View target) {
888        mNestedScrollingParentHelper.onStopNestedScroll(target);
889    }
890
891    private boolean canDrawerContentScrollVertically(
892            @Nullable WearableDrawerView drawerView, int direction) {
893        if (drawerView == null) {
894            return false;
895        }
896
897        View drawerContent = drawerView.getDrawerContent();
898        if (drawerContent == null) {
899            return false;
900        }
901
902        return drawerContent.canScrollVertically(direction);
903    }
904
905    /**
906     * Listener for monitoring events about drawers.
907     */
908    public static class DrawerStateCallback {
909
910        /**
911         * Called when a drawer has settled in a completely open state. The drawer is interactive at
912         * this point.
913         */
914        public void onDrawerOpened(WearableDrawerLayout layout, WearableDrawerView drawerView) {
915        }
916
917        /**
918         * Called when a drawer has settled in a completely closed state.
919         */
920        public void onDrawerClosed(WearableDrawerLayout layout, WearableDrawerView drawerView) {
921        }
922
923        /**
924         * Called when the drawer motion state changes. The new state will be one of {@link
925         * WearableDrawerView#STATE_IDLE}, {@link WearableDrawerView#STATE_DRAGGING} or {@link
926         * WearableDrawerView#STATE_SETTLING}.
927         */
928        public void onDrawerStateChanged(WearableDrawerLayout layout, @DrawerState int newState) {
929        }
930    }
931
932    private void allowAccessibilityFocusOnAllChildren() {
933        if (!mIsAccessibilityEnabled) {
934            return;
935        }
936
937        for (int i = 0; i < getChildCount(); i++) {
938            getChildAt(i).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
939        }
940    }
941
942    private void allowAccessibilityFocusOnOnly(WearableDrawerView drawer) {
943        if (!mIsAccessibilityEnabled) {
944            return;
945        }
946
947        for (int i = 0; i < getChildCount(); i++) {
948            View child = getChildAt(i);
949            if (child != drawer) {
950                child.setImportantForAccessibility(
951                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
952            }
953        }
954    }
955
956    /**
957     * Base class for top and bottom drawer dragger callbacks.
958     */
959    private abstract class DrawerDraggerCallback extends ViewDragHelper.Callback {
960
961        public abstract WearableDrawerView getDrawerView();
962
963        @Override
964        public boolean tryCaptureView(View child, int pointerId) {
965            WearableDrawerView drawerView = getDrawerView();
966            // Returns true if the dragger is dragging the drawer.
967            return child == drawerView && !drawerView.isLocked()
968                    && drawerView.getDrawerContent() != null;
969        }
970
971        @Override
972        public int getViewVerticalDragRange(View child) {
973            // Defines the vertical drag range of the drawer.
974            return child == getDrawerView() ? child.getHeight() : 0;
975        }
976
977        @Override
978        public void onViewCaptured(View capturedChild, int activePointerId) {
979            showDrawerContentMaybeAnimate((WearableDrawerView) capturedChild);
980        }
981
982        @Override
983        public void onViewDragStateChanged(int state) {
984            final WearableDrawerView drawerView = getDrawerView();
985            switch (state) {
986                case ViewDragHelper.STATE_IDLE:
987                    boolean openedOrClosed = false;
988                    if (drawerView.isOpened()) {
989                        openedOrClosed = true;
990                        drawerView.onDrawerOpened();
991                        allowAccessibilityFocusOnOnly(drawerView);
992                        if (mDrawerStateCallback != null) {
993                            mDrawerStateCallback
994                                    .onDrawerOpened(WearableDrawerLayout.this, drawerView);
995                        }
996
997                        // Drawers can be closed if a drag to close them will not cause a scroll.
998                        mCanTopDrawerBeClosed = !canDrawerContentScrollVertically(mTopDrawerView,
999                                DOWN);
1000                        mCanBottomDrawerBeClosed = !canDrawerContentScrollVertically(
1001                                mBottomDrawerView, UP);
1002                    } else if (drawerView.isClosed()) {
1003                        openedOrClosed = true;
1004                        drawerView.onDrawerClosed();
1005                        allowAccessibilityFocusOnAllChildren();
1006                        if (mDrawerStateCallback != null) {
1007                            mDrawerStateCallback
1008                                    .onDrawerClosed(WearableDrawerLayout.this, drawerView);
1009                        }
1010                    } else { // drawerView is peeking
1011                        allowAccessibilityFocusOnAllChildren();
1012                    }
1013
1014                    // If the drawer is fully opened or closed, change it to non-peeking mode.
1015                    if (openedOrClosed && drawerView.isPeeking()) {
1016                        drawerView.setIsPeeking(false);
1017                        drawerView.getPeekContainer().setVisibility(INVISIBLE);
1018                    }
1019                    break;
1020                default: // fall out
1021            }
1022
1023            if (drawerView.getDrawerState() != state) {
1024                drawerView.setDrawerState(state);
1025                drawerView.onDrawerStateChanged(state);
1026                if (mDrawerStateCallback != null) {
1027                    mDrawerStateCallback.onDrawerStateChanged(WearableDrawerLayout.this, state);
1028                }
1029            }
1030        }
1031    }
1032
1033    /**
1034     * For communicating with top drawer view dragger.
1035     */
1036    private class TopDrawerDraggerCallback extends DrawerDraggerCallback {
1037
1038        @Override
1039        public int clampViewPositionVertical(View child, int top, int dy) {
1040            if (mTopDrawerView == child) {
1041                int peekHeight = mTopDrawerView.getPeekContainer().getHeight();
1042                // The top drawer can be dragged vertically from peekHeight - height to 0.
1043                return Math.max(peekHeight - child.getHeight(), Math.min(top, 0));
1044            }
1045            return 0;
1046        }
1047
1048        @Override
1049        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
1050            if (mTopDrawerView != null
1051                    && edgeFlags == ViewDragHelper.EDGE_TOP
1052                    && !mTopDrawerView.isLocked()
1053                    && (mBottomDrawerView == null || !mBottomDrawerView.isOpened())
1054                    && mTopDrawerView.getDrawerContent() != null) {
1055
1056                boolean atTop =
1057                        mScrollingContentView == null || !mScrollingContentView
1058                                .canScrollVertically(UP);
1059                if (!mTopDrawerView.isOpenOnlyAtTopEnabled() || atTop) {
1060                    mTopDrawerDragger.captureChildView(mTopDrawerView, pointerId);
1061                }
1062            }
1063        }
1064
1065        @Override
1066        public void onViewReleased(View releasedChild, float xvel, float yvel) {
1067            if (releasedChild == mTopDrawerView) {
1068                // Settle to final position. Either swipe open or close.
1069                final float openedPercent = mTopDrawerView.getOpenedPercent();
1070
1071                final int finalTop;
1072                if (yvel > 0 || (yvel == 0 && openedPercent > OPENED_PERCENT_THRESHOLD)) {
1073                    // Drawer was being flung open or drawer is mostly open, so finish opening.
1074                    finalTop = 0;
1075                } else {
1076                    // Drawer should be closed to its peek state.
1077                    animatePeekVisibleAfterBeingClosed(mTopDrawerView);
1078                    finalTop = mTopDrawerView.getPeekContainer().getHeight() - releasedChild
1079                            .getHeight();
1080                }
1081
1082                mTopDrawerDragger.settleCapturedViewAt(0 /* finalLeft */, finalTop);
1083                invalidate();
1084            }
1085        }
1086
1087        @Override
1088        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1089            if (changedView == mTopDrawerView) {
1090                // Compute the offset and invalidate will move the drawer during layout.
1091                final int height = changedView.getHeight();
1092                mTopDrawerView.setOpenedPercent((float) (top + height) / height);
1093                invalidate();
1094            }
1095        }
1096
1097        @Override
1098        public WearableDrawerView getDrawerView() {
1099            return mTopDrawerView;
1100        }
1101    }
1102
1103    /**
1104     * For communicating with bottom drawer view dragger.
1105     */
1106    private class BottomDrawerDraggerCallback extends DrawerDraggerCallback {
1107
1108        @Override
1109        public int clampViewPositionVertical(View child, int top, int dy) {
1110            if (mBottomDrawerView == child) {
1111                // The bottom drawer can be dragged vertically from (parentHeight - height) to
1112                // (parentHeight - peekHeight).
1113                int parentHeight = getHeight();
1114                int peekHeight = mBottomDrawerView.getPeekContainer().getHeight();
1115                return Math.max(parentHeight - child.getHeight(),
1116                        Math.min(top, parentHeight - peekHeight));
1117            }
1118            return 0;
1119        }
1120
1121        @Override
1122        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
1123            if (mBottomDrawerView != null
1124                    && edgeFlags == ViewDragHelper.EDGE_BOTTOM
1125                    && !mBottomDrawerView.isLocked()
1126                    && (mTopDrawerView == null || !mTopDrawerView.isOpened())
1127                    && mBottomDrawerView.getDrawerContent() != null) {
1128                // Tells the dragger which view to start dragging.
1129                mBottomDrawerDragger.captureChildView(mBottomDrawerView, pointerId);
1130            }
1131        }
1132
1133        @Override
1134        public void onViewReleased(View releasedChild, float xvel, float yvel) {
1135            if (releasedChild == mBottomDrawerView) {
1136                // Settle to final position. Either swipe open or close.
1137                final int parentHeight = getHeight();
1138                final float openedPercent = mBottomDrawerView.getOpenedPercent();
1139                final int finalTop;
1140                if (yvel < 0 || (yvel == 0 && openedPercent > OPENED_PERCENT_THRESHOLD)) {
1141                    // Drawer was being flung open or drawer is mostly open, so finish opening it.
1142                    finalTop = parentHeight - releasedChild.getHeight();
1143                } else {
1144                    // Drawer should be closed to its peek state.
1145                    animatePeekVisibleAfterBeingClosed(mBottomDrawerView);
1146                    finalTop = getHeight() - mBottomDrawerView.getPeekContainer().getHeight();
1147                }
1148                mBottomDrawerDragger.settleCapturedViewAt(0 /* finalLeft */, finalTop);
1149                invalidate();
1150            }
1151        }
1152
1153        @Override
1154        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
1155            if (changedView == mBottomDrawerView) {
1156                // Compute the offset and invalidate will move the drawer during layout.
1157                final int height = changedView.getHeight();
1158                final int parentHeight = getHeight();
1159
1160                mBottomDrawerView.setOpenedPercent((float) (parentHeight - top) / height);
1161                invalidate();
1162            }
1163        }
1164
1165        @Override
1166        public WearableDrawerView getDrawerView() {
1167            return mBottomDrawerView;
1168        }
1169    }
1170
1171    /**
1172     * Runnable that closes the given drawer if it is just peeking.
1173     */
1174    private class ClosePeekRunnable implements Runnable {
1175
1176        private final int mGravity;
1177
1178        private ClosePeekRunnable(int gravity) {
1179            mGravity = gravity;
1180        }
1181
1182        @Override
1183        public void run() {
1184            WearableDrawerView drawer = findDrawerWithGravity(mGravity);
1185            if (drawer != null
1186                    && !drawer.isOpened()
1187                    && drawer.getDrawerState() == STATE_IDLE) {
1188                closeDrawer(mGravity);
1189            }
1190        }
1191    }
1192}
1193