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