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