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 android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.drawable.Drawable; 22 import android.util.AttributeSet; 23 import android.view.Gravity; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.FrameLayout; 28 import android.widget.ImageView; 29 30 import androidx.annotation.IdRes; 31 import androidx.annotation.IntDef; 32 import androidx.annotation.RestrictTo; 33 import androidx.annotation.RestrictTo.Scope; 34 import androidx.annotation.StyleableRes; 35 import androidx.core.view.ViewCompat; 36 import androidx.customview.widget.ViewDragHelper; 37 import androidx.wear.R; 38 39 import org.jspecify.annotations.Nullable; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 44 /** 45 * View that contains drawer content and a peeking view for use with {@link WearableDrawerLayout}. 46 * 47 * <p>This view provides the ability to set its main content as well as a view shown while peeking. 48 * Specifying the peek view is entirely optional; a default is used if none are set. However, the 49 * content must be provided. 50 * 51 * <p>There are two ways to specify the content and peek views: by invoking {@code setter} methods 52 * on the {@code WearableDrawerView}, or by specifying the {@code app:drawerContent} and {@code 53 * app:peekView} attributes. Examples: 54 * 55 * <pre> 56 * // From Java: 57 * drawerView.setDrawerContent(drawerContentView); 58 * drawerView.setPeekContent(peekContentView); 59 * 60 * <!-- From XML: --> 61 * <androidx.wear.widget.drawer.WearableDrawerView 62 * android:layout_width="match_parent" 63 * android:layout_height="match_parent" 64 * android:layout_gravity="bottom" 65 * android:background="@color/red" 66 * app:drawerContent="@+id/drawer_content" 67 * app:peekView="@+id/peek_view"> 68 * 69 * <FrameLayout 70 * android:id="@id/drawer_content" 71 * android:layout_width="match_parent" 72 * android:layout_height="match_parent" /> 73 * 74 * <LinearLayout 75 * android:id="@id/peek_view" 76 * android:layout_width="wrap_content" 77 * android:layout_height="wrap_content" 78 * android:layout_gravity="center_horizontal" 79 * android:orientation="horizontal"> 80 * <ImageView 81 * android:layout_width="wrap_content" 82 * android:layout_height="wrap_content" 83 * android:src="@android:drawable/ic_media_play" /> 84 * <ImageView 85 * android:layout_width="wrap_content" 86 * android:layout_height="wrap_content" 87 * android:src="@android:drawable/ic_media_pause" /> 88 * </LinearLayout> 89 * </androidx.wear.widget.drawer.WearableDrawerView></pre> 90 */ 91 public class WearableDrawerView extends FrameLayout { 92 /** 93 * Indicates that the drawer is in an idle, settled state. No animation is in progress. 94 */ 95 public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; 96 97 /** 98 * Indicates that the drawer is currently being dragged by the user. 99 */ 100 public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; 101 102 /** 103 * Indicates that the drawer is in the process of settling to a final position. 104 */ 105 public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; 106 107 /** 108 * Enumeration of possible drawer states. 109 */ 110 @Retention(RetentionPolicy.SOURCE) 111 @RestrictTo(Scope.LIBRARY) 112 @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) 113 public @interface DrawerState {} 114 115 private final ViewGroup mPeekContainer; 116 private final ImageView mPeekIcon; 117 private View mContent; 118 private WearableDrawerController mController; 119 /** 120 * Vertical offset of the drawer. Ranges from 0 (closed) to 1 (opened) 121 */ 122 private float mOpenedPercent; 123 /** 124 * True if the drawer's position cannot be modified by the user. This includes edge dragging, 125 * view dragging, and scroll based auto-peeking. 126 */ 127 private boolean mIsLocked = false; 128 private boolean mCanAutoPeek = true; 129 private boolean mLockWhenClosed = false; 130 private boolean mOpenOnlyAtTop = false; 131 private boolean mPeekOnScrollDown = false; 132 private boolean mIsPeeking; 133 @DrawerState private int mDrawerState; 134 @IdRes private int mPeekResId = 0; 135 @IdRes private int mContentResId = 0; WearableDrawerView(Context context)136 public WearableDrawerView(Context context) { 137 this(context, null); 138 } 139 WearableDrawerView(Context context, AttributeSet attrs)140 public WearableDrawerView(Context context, AttributeSet attrs) { 141 this(context, attrs, 0); 142 } 143 WearableDrawerView(Context context, AttributeSet attrs, int defStyleAttr)144 public WearableDrawerView(Context context, AttributeSet attrs, int defStyleAttr) { 145 this(context, attrs, defStyleAttr, 0); 146 } 147 WearableDrawerView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)148 public WearableDrawerView( 149 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 150 super(context, attrs, defStyleAttr, defStyleRes); 151 LayoutInflater.from(context).inflate(R.layout.ws_wearable_drawer_view, this, true); 152 153 setClickable(true); 154 setElevation(context.getResources() 155 .getDimension(R.dimen.ws_wearable_drawer_view_elevation)); 156 157 mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container); 158 mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon); 159 160 mPeekContainer.setOnClickListener( 161 new OnClickListener() { 162 @Override 163 public void onClick(View v) { 164 onPeekContainerClicked(v); 165 } 166 }); 167 168 parseAttributes(context, attrs, defStyleAttr); 169 } 170 getDrawable( Context context, TypedArray typedArray, @StyleableRes int index)171 private static Drawable getDrawable( 172 Context context, TypedArray typedArray, @StyleableRes int index) { 173 Drawable background; 174 int backgroundResId = 175 typedArray.getResourceId(index, 0); 176 if (backgroundResId == 0) { 177 background = typedArray.getDrawable(index); 178 } else { 179 background = context.getDrawable(backgroundResId); 180 } 181 return background; 182 } 183 184 @Override onFinishInflate()185 protected void onFinishInflate() { 186 super.onFinishInflate(); 187 188 // Drawer content is added after the peek view, so we need to bring the peek view 189 // to the front so it shows on top of the content. 190 mPeekContainer.bringToFront(); 191 } 192 193 /** 194 * Called when anything within the peek container is clicked. However, if a custom peek view is 195 * supplied and it handles the click, then this may not be called. The default behavior is to 196 * open the drawer. 197 */ onPeekContainerClicked(View v)198 public void onPeekContainerClicked(View v) { 199 mController.openDrawer(); 200 } 201 202 @Override onAttachedToWindow()203 protected void onAttachedToWindow() { 204 super.onAttachedToWindow(); 205 206 // The peek view has a layout gravity of bottom for the top drawer, and a layout gravity 207 // of top for the bottom drawer. This is required so that the peek view shows. On the top 208 // drawer, the bottom peeks from the top, and on the bottom drawer, the top peeks. 209 // LayoutParams are not guaranteed to return a non-null value until a child is attached to 210 // the window. 211 LayoutParams peekParams = (LayoutParams) mPeekContainer.getLayoutParams(); 212 if (!Gravity.isVertical(peekParams.gravity)) { 213 final boolean isTopDrawer = 214 (((LayoutParams) getLayoutParams()).gravity & Gravity.VERTICAL_GRAVITY_MASK) 215 == Gravity.TOP; 216 if (isTopDrawer) { 217 peekParams.gravity = Gravity.BOTTOM; 218 mPeekIcon.setImageResource(R.drawable.ws_ic_more_horiz_24dp_wht); 219 } else { 220 peekParams.gravity = Gravity.TOP; 221 mPeekIcon.setImageResource(R.drawable.ws_ic_more_vert_24dp_wht); 222 } 223 mPeekContainer.setLayoutParams(peekParams); 224 } 225 } 226 227 @Override addView(View child, int index, ViewGroup.LayoutParams params)228 public void addView(View child, int index, ViewGroup.LayoutParams params) { 229 @IdRes int childId = child.getId(); 230 if (childId != 0) { 231 if (childId == mPeekResId) { 232 setPeekContent(child, index, params); 233 return; 234 } 235 if (childId == mContentResId && !setDrawerContentWithoutAdding(child)) { 236 return; 237 } 238 } 239 240 super.addView(child, index, params); 241 } 242 preferGravity()243 int preferGravity() { 244 return Gravity.NO_GRAVITY; 245 } 246 getPeekContainer()247 ViewGroup getPeekContainer() { 248 return mPeekContainer; 249 } 250 setDrawerController(WearableDrawerController controller)251 void setDrawerController(WearableDrawerController controller) { 252 mController = controller; 253 } 254 255 /** 256 * Returns the drawer content view. 257 */ getDrawerContent()258 public @Nullable View getDrawerContent() { 259 return mContent; 260 } 261 262 /** 263 * Set the drawer content view. 264 * 265 * @param content The view to show when the drawer is open, or {@code null} if it should not 266 * open. 267 */ setDrawerContent(@ullable View content)268 public void setDrawerContent(@Nullable View content) { 269 if (setDrawerContentWithoutAdding(content)) { 270 addView(content); 271 } 272 } 273 274 /** 275 * Set the peek content view. 276 * 277 * @param content The view to show when the drawer peeks. 278 */ setPeekContent(View content)279 public void setPeekContent(View content) { 280 ViewGroup.LayoutParams layoutParams = content.getLayoutParams(); 281 setPeekContent( 282 content, 283 -1 /* index */, 284 layoutParams != null ? layoutParams : generateDefaultLayoutParams()); 285 } 286 287 /** 288 * Called when the drawer has settled in a completely open state. The drawer is interactive at 289 * this point. This is analogous to {@link 290 * WearableDrawerLayout.DrawerStateCallback#onDrawerOpened}. 291 */ onDrawerOpened()292 public void onDrawerOpened() {} 293 294 /** 295 * Called when the drawer has settled in a completely closed state. This is analogous to {@link 296 * WearableDrawerLayout.DrawerStateCallback#onDrawerClosed}. 297 */ onDrawerClosed()298 public void onDrawerClosed() {} 299 300 /** 301 * Called when the drawer state changes. This is analogous to {@link 302 * WearableDrawerLayout.DrawerStateCallback#onDrawerStateChanged}. 303 * 304 * @param state one of {@link #STATE_DRAGGING}, {@link #STATE_SETTLING}, or {@link #STATE_IDLE} 305 */ onDrawerStateChanged(@rawerState int state)306 public void onDrawerStateChanged(@DrawerState int state) {} 307 308 /** 309 * Only allow the user to open this drawer when at the top of the scrolling content. If there is 310 * no scrolling content, then this has no effect. Defaults to {@code false}. 311 */ setOpenOnlyAtTopEnabled(boolean openOnlyAtTop)312 public void setOpenOnlyAtTopEnabled(boolean openOnlyAtTop) { 313 mOpenOnlyAtTop = openOnlyAtTop; 314 } 315 316 /** 317 * Returns whether this drawer may only be opened by the user when at the top of the scrolling 318 * content. If there is no scrolling content, then this has no effect. Defaults to {@code 319 * false}. 320 */ isOpenOnlyAtTopEnabled()321 public boolean isOpenOnlyAtTopEnabled() { 322 return mOpenOnlyAtTop; 323 } 324 325 /** 326 * Sets whether or not this drawer should peek while scrolling down. This is currently only 327 * supported for bottom drawers. Defaults to {@code false}. 328 */ setPeekOnScrollDownEnabled(boolean peekOnScrollDown)329 public void setPeekOnScrollDownEnabled(boolean peekOnScrollDown) { 330 mPeekOnScrollDown = peekOnScrollDown; 331 } 332 333 /** 334 * Gets whether or not this drawer should peek while scrolling down. This is currently only 335 * supported for bottom drawers. Defaults to {@code false}. 336 */ isPeekOnScrollDownEnabled()337 public boolean isPeekOnScrollDownEnabled() { 338 return mPeekOnScrollDown; 339 } 340 341 /** 342 * Sets whether this drawer should be locked when the user cannot see it. 343 * @see #isLocked 344 */ setLockedWhenClosed(boolean locked)345 public void setLockedWhenClosed(boolean locked) { 346 mLockWhenClosed = locked; 347 } 348 349 /** 350 * Returns true if this drawer should be locked when the user cannot see it. 351 * @see #isLocked 352 */ isLockedWhenClosed()353 public boolean isLockedWhenClosed() { 354 return mLockWhenClosed; 355 } 356 357 /** 358 * Returns the current drawer state, which will be one of {@link #STATE_DRAGGING}, {@link 359 * #STATE_SETTLING}, or {@link #STATE_IDLE} 360 */ 361 @DrawerState getDrawerState()362 public int getDrawerState() { 363 return mDrawerState; 364 } 365 366 /** 367 * Sets the {@link DrawerState}. 368 */ setDrawerState(@rawerState int drawerState)369 void setDrawerState(@DrawerState int drawerState) { 370 mDrawerState = drawerState; 371 } 372 373 /** 374 * Returns whether the drawer is either peeking or the peek view is animating open. 375 */ isPeeking()376 public boolean isPeeking() { 377 return mIsPeeking; 378 } 379 380 /** 381 * Returns true if this drawer has auto-peeking enabled. This will always return {@code false} 382 * for a locked drawer. 383 */ isAutoPeekEnabled()384 public boolean isAutoPeekEnabled() { 385 return mCanAutoPeek && !mIsLocked; 386 } 387 388 /** 389 * Sets whether or not the drawer can automatically adjust its peek state. Note that locked 390 * drawers will never auto-peek, but their {@code isAutoPeekEnabled} state will be maintained 391 * through a lock/unlock cycle. 392 */ setIsAutoPeekEnabled(boolean canAutoPeek)393 public void setIsAutoPeekEnabled(boolean canAutoPeek) { 394 mCanAutoPeek = canAutoPeek; 395 } 396 397 /** 398 * Returns true if the position of the drawer cannot be modified by user interaction. 399 * Specifically, a drawer cannot be opened, closed, or automatically peeked by {@link 400 * WearableDrawerLayout}. However, it can be explicitly opened, closed, and peeked by the 401 * developer. A drawer may be considered locked if the drawer is locked open, locked closed, or 402 * is closed and {@link #isLockedWhenClosed} returns true. 403 */ isLocked()404 public boolean isLocked() { 405 return mIsLocked || (isLockedWhenClosed() && mOpenedPercent <= 0); 406 } 407 408 /** 409 * Sets whether or not the position of the drawer can be modified by user interaction. 410 * @see #isLocked 411 */ setIsLocked(boolean locked)412 public void setIsLocked(boolean locked) { 413 mIsLocked = locked; 414 } 415 416 /** 417 * Returns true if the drawer is fully open. 418 */ isOpened()419 public boolean isOpened() { 420 return mOpenedPercent == 1; 421 } 422 423 /** 424 * Returns true if the drawer is fully closed. 425 */ isClosed()426 public boolean isClosed() { 427 return mOpenedPercent == 0; 428 } 429 430 /** 431 * Returns the {@link WearableDrawerController} associated with this {@link WearableDrawerView}. 432 * This will only be valid after this {@code View} has been added to its parent. 433 */ getController()434 public WearableDrawerController getController() { 435 return mController; 436 } 437 438 /** 439 * Sets whether the drawer is either peeking or the peek view is animating open. 440 */ setIsPeeking(boolean isPeeking)441 void setIsPeeking(boolean isPeeking) { 442 mIsPeeking = isPeeking; 443 } 444 445 /** 446 * Returns the percent the drawer is open, from 0 (fully closed) to 1 (fully open). 447 */ getOpenedPercent()448 float getOpenedPercent() { 449 return mOpenedPercent; 450 } 451 452 /** 453 * Sets the percent the drawer is open, from 0 (fully closed) to 1 (fully open). 454 */ setOpenedPercent(float openedPercent)455 void setOpenedPercent(float openedPercent) { 456 mOpenedPercent = openedPercent; 457 } 458 parseAttributes( Context context, AttributeSet attrs, int defStyleAttr)459 private void parseAttributes( 460 Context context, AttributeSet attrs, int defStyleAttr) { 461 if (attrs == null) { 462 return; 463 } 464 465 TypedArray typedArray = 466 context.obtainStyledAttributes( 467 attrs, R.styleable.WearableDrawerView, defStyleAttr, 468 R.style.Widget_Wear_WearableDrawerView); 469 ViewCompat.saveAttributeDataForStyleable( 470 this, context, R.styleable.WearableDrawerView, attrs, typedArray, defStyleAttr, 471 R.style.Widget_Wear_WearableDrawerView); 472 473 Drawable background = 474 getDrawable(context, typedArray, R.styleable.WearableDrawerView_android_background); 475 int elevation = typedArray 476 .getDimensionPixelSize(R.styleable.WearableDrawerView_android_elevation, 0); 477 setBackground(background); 478 setElevation(elevation); 479 480 mContentResId = typedArray.getResourceId(R.styleable.WearableDrawerView_drawerContent, 0); 481 mPeekResId = typedArray.getResourceId(R.styleable.WearableDrawerView_peekView, 0); 482 mCanAutoPeek = 483 typedArray.getBoolean(R.styleable.WearableDrawerView_enableAutoPeek, mCanAutoPeek); 484 typedArray.recycle(); 485 } 486 setPeekContent(View content, int index, ViewGroup.LayoutParams params)487 private void setPeekContent(View content, int index, ViewGroup.LayoutParams params) { 488 if (content == null) { 489 return; 490 } 491 if (mPeekContainer.getChildCount() > 0) { 492 mPeekContainer.removeAllViews(); 493 } 494 mPeekContainer.addView(content, index, params); 495 } 496 497 /** 498 * @return {@code true} if this is a new and valid {@code content}. 499 */ setDrawerContentWithoutAdding(View content)500 private boolean setDrawerContentWithoutAdding(View content) { 501 if (content == mContent) { 502 return false; 503 } 504 if (mContent != null) { 505 removeView(mContent); 506 } 507 508 mContent = content; 509 return mContent != null; 510 } 511 } 512