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  * &lt;!-- From XML: --&gt;
61  * &lt;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"&gt;
68  *
69  *     &lt;FrameLayout
70  *         android:id="@id/drawer_content"
71  *         android:layout_width="match_parent"
72  *         android:layout_height="match_parent" /&gt;
73  *
74  *     &lt;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"&gt;
80  *         &lt;ImageView
81  *             android:layout_width="wrap_content"
82  *             android:layout_height="wrap_content"
83  *             android:src="@android:drawable/ic_media_play" /&gt;
84  *         &lt;ImageView
85  *             android:layout_width="wrap_content"
86  *             android:layout_height="wrap_content"
87  *             android:src="@android:drawable/ic_media_pause" /&gt;
88  *     &lt;/LinearLayout&gt;
89  * &lt;/androidx.wear.widget.drawer.WearableDrawerView&gt;</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