/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.wear.widget.drawer; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.annotation.StyleableRes; import androidx.customview.widget.ViewDragHelper; import androidx.wear.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * View that contains drawer content and a peeking view for use with {@link WearableDrawerLayout}. * *

This view provides the ability to set its main content as well as a view shown while peeking. * Specifying the peek view is entirely optional; a default is used if none are set. However, the * content must be provided. * *

There are two ways to specify the content and peek views: by invoking {@code setter} methods * on the {@code WearableDrawerView}, or by specifying the {@code app:drawerContent} and {@code * app:peekView} attributes. Examples: * *

 * // From Java:
 * drawerView.setDrawerContent(drawerContentView);
 * drawerView.setPeekContent(peekContentView);
 *
 * <!-- From XML: -->
 * <androidx.wear.widget.drawer.WearableDrawerView
 *     android:layout_width="match_parent"
 *     android:layout_height="match_parent"
 *     android:layout_gravity="bottom"
 *     android:background="@color/red"
 *     app:drawerContent="@+id/drawer_content"
 *     app:peekView="@+id/peek_view">
 *
 *     <FrameLayout
 *         android:id="@id/drawer_content"
 *         android:layout_width="match_parent"
 *         android:layout_height="match_parent" />
 *
 *     <LinearLayout
 *         android:id="@id/peek_view"
 *         android:layout_width="wrap_content"
 *         android:layout_height="wrap_content"
 *         android:layout_gravity="center_horizontal"
 *         android:orientation="horizontal">
 *         <ImageView
 *             android:layout_width="wrap_content"
 *             android:layout_height="wrap_content"
 *             android:src="@android:drawable/ic_media_play" />
 *         <ImageView
 *             android:layout_width="wrap_content"
 *             android:layout_height="wrap_content"
 *             android:src="@android:drawable/ic_media_pause" />
 *     </LinearLayout>
 * </androidx.wear.widget.drawer.WearableDrawerView>
*/ public class WearableDrawerView extends FrameLayout { /** * Indicates that the drawer is in an idle, settled state. No animation is in progress. */ public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; /** * Indicates that the drawer is currently being dragged by the user. */ public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; /** * Indicates that the drawer is in the process of settling to a final position. */ public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; /** * Enumeration of possible drawer states. * @hide */ @Retention(RetentionPolicy.SOURCE) @RestrictTo(Scope.LIBRARY) @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) public @interface DrawerState {} private final ViewGroup mPeekContainer; private final ImageView mPeekIcon; private View mContent; private WearableDrawerController mController; /** * Vertical offset of the drawer. Ranges from 0 (closed) to 1 (opened) */ private float mOpenedPercent; /** * True if the drawer's position cannot be modified by the user. This includes edge dragging, * view dragging, and scroll based auto-peeking. */ private boolean mIsLocked = false; private boolean mCanAutoPeek = true; private boolean mLockWhenClosed = false; private boolean mOpenOnlyAtTop = false; private boolean mPeekOnScrollDown = false; private boolean mIsPeeking; @DrawerState private int mDrawerState; @IdRes private int mPeekResId = 0; @IdRes private int mContentResId = 0; public WearableDrawerView(Context context) { this(context, null); } public WearableDrawerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public WearableDrawerView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public WearableDrawerView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); LayoutInflater.from(context).inflate(R.layout.ws_wearable_drawer_view, this, true); setClickable(true); setElevation(context.getResources() .getDimension(R.dimen.ws_wearable_drawer_view_elevation)); mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container); mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon); mPeekContainer.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { onPeekContainerClicked(v); } }); parseAttributes(context, attrs, defStyleAttr, defStyleRes); } private static Drawable getDrawable( Context context, TypedArray typedArray, @StyleableRes int index) { Drawable background; int backgroundResId = typedArray.getResourceId(index, 0); if (backgroundResId == 0) { background = typedArray.getDrawable(index); } else { background = context.getDrawable(backgroundResId); } return background; } @Override protected void onFinishInflate() { super.onFinishInflate(); // Drawer content is added after the peek view, so we need to bring the peek view // to the front so it shows on top of the content. mPeekContainer.bringToFront(); } /** * Called when anything within the peek container is clicked. However, if a custom peek view is * supplied and it handles the click, then this may not be called. The default behavior is to * open the drawer. */ public void onPeekContainerClicked(View v) { mController.openDrawer(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // The peek view has a layout gravity of bottom for the top drawer, and a layout gravity // of top for the bottom drawer. This is required so that the peek view shows. On the top // drawer, the bottom peeks from the top, and on the bottom drawer, the top peeks. // LayoutParams are not guaranteed to return a non-null value until a child is attached to // the window. LayoutParams peekParams = (LayoutParams) mPeekContainer.getLayoutParams(); if (!Gravity.isVertical(peekParams.gravity)) { final boolean isTopDrawer = (((LayoutParams) getLayoutParams()).gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP; if (isTopDrawer) { peekParams.gravity = Gravity.BOTTOM; mPeekIcon.setImageResource(R.drawable.ws_ic_more_horiz_24dp_wht); } else { peekParams.gravity = Gravity.TOP; mPeekIcon.setImageResource(R.drawable.ws_ic_more_vert_24dp_wht); } mPeekContainer.setLayoutParams(peekParams); } } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { @IdRes int childId = child.getId(); if (childId != 0) { if (childId == mPeekResId) { setPeekContent(child, index, params); return; } if (childId == mContentResId && !setDrawerContentWithoutAdding(child)) { return; } } super.addView(child, index, params); } int preferGravity() { return Gravity.NO_GRAVITY; } ViewGroup getPeekContainer() { return mPeekContainer; } void setDrawerController(WearableDrawerController controller) { mController = controller; } /** * Returns the drawer content view. */ @Nullable public View getDrawerContent() { return mContent; } /** * Set the drawer content view. * * @param content The view to show when the drawer is open, or {@code null} if it should not * open. */ public void setDrawerContent(@Nullable View content) { if (setDrawerContentWithoutAdding(content)) { addView(content); } } /** * Set the peek content view. * * @param content The view to show when the drawer peeks. */ public void setPeekContent(View content) { ViewGroup.LayoutParams layoutParams = content.getLayoutParams(); setPeekContent( content, -1 /* index */, layoutParams != null ? layoutParams : generateDefaultLayoutParams()); } /** * Called when the drawer has settled in a completely open state. The drawer is interactive at * this point. This is analogous to {@link * WearableDrawerLayout.DrawerStateCallback#onDrawerOpened}. */ public void onDrawerOpened() {} /** * Called when the drawer has settled in a completely closed state. This is analogous to {@link * WearableDrawerLayout.DrawerStateCallback#onDrawerClosed}. */ public void onDrawerClosed() {} /** * Called when the drawer state changes. This is analogous to {@link * WearableDrawerLayout.DrawerStateCallback#onDrawerStateChanged}. * * @param state one of {@link #STATE_DRAGGING}, {@link #STATE_SETTLING}, or {@link #STATE_IDLE} */ public void onDrawerStateChanged(@DrawerState int state) {} /** * Only allow the user to open this drawer when at the top of the scrolling content. If there is * no scrolling content, then this has no effect. Defaults to {@code false}. */ public void setOpenOnlyAtTopEnabled(boolean openOnlyAtTop) { mOpenOnlyAtTop = openOnlyAtTop; } /** * Returns whether this drawer may only be opened by the user when at the top of the scrolling * content. If there is no scrolling content, then this has no effect. Defaults to {@code * false}. */ public boolean isOpenOnlyAtTopEnabled() { return mOpenOnlyAtTop; } /** * Sets whether or not this drawer should peek while scrolling down. This is currently only * supported for bottom drawers. Defaults to {@code false}. */ public void setPeekOnScrollDownEnabled(boolean peekOnScrollDown) { mPeekOnScrollDown = peekOnScrollDown; } /** * Gets whether or not this drawer should peek while scrolling down. This is currently only * supported for bottom drawers. Defaults to {@code false}. */ public boolean isPeekOnScrollDownEnabled() { return mPeekOnScrollDown; } /** * Sets whether this drawer should be locked when the user cannot see it. * @see #isLocked */ public void setLockedWhenClosed(boolean locked) { mLockWhenClosed = locked; } /** * Returns true if this drawer should be locked when the user cannot see it. * @see #isLocked */ public boolean isLockedWhenClosed() { return mLockWhenClosed; } /** * Returns the current drawer state, which will be one of {@link #STATE_DRAGGING}, {@link * #STATE_SETTLING}, or {@link #STATE_IDLE} */ @DrawerState public int getDrawerState() { return mDrawerState; } /** * Sets the {@link DrawerState}. */ void setDrawerState(@DrawerState int drawerState) { mDrawerState = drawerState; } /** * Returns whether the drawer is either peeking or the peek view is animating open. */ public boolean isPeeking() { return mIsPeeking; } /** * Returns true if this drawer has auto-peeking enabled. This will always return {@code false} * for a locked drawer. */ public boolean isAutoPeekEnabled() { return mCanAutoPeek && !mIsLocked; } /** * Sets whether or not the drawer can automatically adjust its peek state. Note that locked * drawers will never auto-peek, but their {@code isAutoPeekEnabled} state will be maintained * through a lock/unlock cycle. */ public void setIsAutoPeekEnabled(boolean canAutoPeek) { mCanAutoPeek = canAutoPeek; } /** * Returns true if the position of the drawer cannot be modified by user interaction. * Specifically, a drawer cannot be opened, closed, or automatically peeked by {@link * WearableDrawerLayout}. However, it can be explicitly opened, closed, and peeked by the * developer. A drawer may be considered locked if the drawer is locked open, locked closed, or * is closed and {@link #isLockedWhenClosed} returns true. */ public boolean isLocked() { return mIsLocked || (isLockedWhenClosed() && mOpenedPercent <= 0); } /** * Sets whether or not the position of the drawer can be modified by user interaction. * @see #isLocked */ public void setIsLocked(boolean locked) { mIsLocked = locked; } /** * Returns true if the drawer is fully open. */ public boolean isOpened() { return mOpenedPercent == 1; } /** * Returns true if the drawer is fully closed. */ public boolean isClosed() { return mOpenedPercent == 0; } /** * Returns the {@link WearableDrawerController} associated with this {@link WearableDrawerView}. * This will only be valid after this {@code View} has been added to its parent. */ public WearableDrawerController getController() { return mController; } /** * Sets whether the drawer is either peeking or the peek view is animating open. */ void setIsPeeking(boolean isPeeking) { mIsPeeking = isPeeking; } /** * Returns the percent the drawer is open, from 0 (fully closed) to 1 (fully open). */ float getOpenedPercent() { return mOpenedPercent; } /** * Sets the percent the drawer is open, from 0 (fully closed) to 1 (fully open). */ void setOpenedPercent(float openedPercent) { mOpenedPercent = openedPercent; } private void parseAttributes( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { if (attrs == null) { return; } TypedArray typedArray = context.obtainStyledAttributes( attrs, R.styleable.WearableDrawerView, defStyleAttr, R.style.Widget_Wear_WearableDrawerView); Drawable background = getDrawable(context, typedArray, R.styleable.WearableDrawerView_android_background); int elevation = typedArray .getDimensionPixelSize(R.styleable.WearableDrawerView_android_elevation, 0); setBackground(background); setElevation(elevation); mContentResId = typedArray.getResourceId(R.styleable.WearableDrawerView_drawerContent, 0); mPeekResId = typedArray.getResourceId(R.styleable.WearableDrawerView_peekView, 0); mCanAutoPeek = typedArray.getBoolean(R.styleable.WearableDrawerView_enableAutoPeek, mCanAutoPeek); typedArray.recycle(); } private void setPeekContent(View content, int index, ViewGroup.LayoutParams params) { if (content == null) { return; } if (mPeekContainer.getChildCount() > 0) { mPeekContainer.removeAllViews(); } mPeekContainer.addView(content, index, params); } /** * @return {@code true} if this is a new and valid {@code content}. */ private boolean setDrawerContentWithoutAdding(View content) { if (content == mContent) { return false; } if (mContent != null) { removeView(mContent); } mContent = content; return mContent != null; } }