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 * <android.support.wear.widget.drawer.WearableDrawerLayout [...]> 63 * <FrameLayout android:id=”@+id/content” /> 64 * 65 * <android.support.wear.widget.drawer.WearableNavigationDrawerView 66 * android:layout_width=”match_parent” 67 * android:layout_height=”match_parent” /> 68 * 69 * <android.support.wear.widget.drawer.WearableActionDrawerView 70 * android:layout_width=”match_parent” 71 * android:layout_height=”match_parent” /> 72 * 73 * </android.support.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 * <android.support.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 * <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" > 92 * 93 * <FrameLayout 94 * android:id=”@id/top_drawer_content” 95 * android:layout_width=”match_parent” 96 * android:layout_height=”match_parent” /> 97 * 98 * </android.support.wear.widget.drawer.WearableDrawerView> 99 * </android.support.wear.widget.drawer.WearableDrawerLayout></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