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