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 androidx.wear.widget.drawer;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.drawable.Drawable;
22import android.util.AttributeSet;
23import android.view.Gravity;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.FrameLayout;
28import android.widget.ImageView;
29
30import androidx.annotation.IdRes;
31import androidx.annotation.IntDef;
32import androidx.annotation.Nullable;
33import androidx.annotation.RestrictTo;
34import androidx.annotation.RestrictTo.Scope;
35import androidx.annotation.StyleableRes;
36import androidx.customview.widget.ViewDragHelper;
37import androidx.wear.R;
38
39import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
41
42/**
43 * View that contains drawer content and a peeking view for use with {@link WearableDrawerLayout}.
44 *
45 * <p>This view provides the ability to set its main content as well as a view shown while peeking.
46 * Specifying the peek view is entirely optional; a default is used if none are set. However, the
47 * content must be provided.
48 *
49 * <p>There are two ways to specify the content and peek views: by invoking {@code setter} methods
50 * on the {@code WearableDrawerView}, or by specifying the {@code app:drawerContent} and {@code
51 * app:peekView} attributes. Examples:
52 *
53 * <pre>
54 * // From Java:
55 * drawerView.setDrawerContent(drawerContentView);
56 * drawerView.setPeekContent(peekContentView);
57 *
58 * &lt;!-- From XML: --&gt;
59 * &lt;androidx.wear.widget.drawer.WearableDrawerView
60 *     android:layout_width="match_parent"
61 *     android:layout_height="match_parent"
62 *     android:layout_gravity="bottom"
63 *     android:background="@color/red"
64 *     app:drawerContent="@+id/drawer_content"
65 *     app:peekView="@+id/peek_view"&gt;
66 *
67 *     &lt;FrameLayout
68 *         android:id="@id/drawer_content"
69 *         android:layout_width="match_parent"
70 *         android:layout_height="match_parent" /&gt;
71 *
72 *     &lt;LinearLayout
73 *         android:id="@id/peek_view"
74 *         android:layout_width="wrap_content"
75 *         android:layout_height="wrap_content"
76 *         android:layout_gravity="center_horizontal"
77 *         android:orientation="horizontal"&gt;
78 *         &lt;ImageView
79 *             android:layout_width="wrap_content"
80 *             android:layout_height="wrap_content"
81 *             android:src="@android:drawable/ic_media_play" /&gt;
82 *         &lt;ImageView
83 *             android:layout_width="wrap_content"
84 *             android:layout_height="wrap_content"
85 *             android:src="@android:drawable/ic_media_pause" /&gt;
86 *     &lt;/LinearLayout&gt;
87 * &lt;/androidx.wear.widget.drawer.WearableDrawerView&gt;</pre>
88 */
89public class WearableDrawerView extends FrameLayout {
90    /**
91     * Indicates that the drawer is in an idle, settled state. No animation is in progress.
92     */
93    public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
94
95    /**
96     * Indicates that the drawer is currently being dragged by the user.
97     */
98    public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
99
100    /**
101     * Indicates that the drawer is in the process of settling to a final position.
102     */
103    public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
104
105    /**
106     * Enumeration of possible drawer states.
107     * @hide
108     */
109    @Retention(RetentionPolicy.SOURCE)
110    @RestrictTo(Scope.LIBRARY)
111    @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING})
112    public @interface DrawerState {}
113
114    private final ViewGroup mPeekContainer;
115    private final ImageView mPeekIcon;
116    private View mContent;
117    private WearableDrawerController mController;
118    /**
119     * Vertical offset of the drawer. Ranges from 0 (closed) to 1 (opened)
120     */
121    private float mOpenedPercent;
122    /**
123     * True if the drawer's position cannot be modified by the user. This includes edge dragging,
124     * view dragging, and scroll based auto-peeking.
125     */
126    private boolean mIsLocked = false;
127    private boolean mCanAutoPeek = true;
128    private boolean mLockWhenClosed = false;
129    private boolean mOpenOnlyAtTop = false;
130    private boolean mPeekOnScrollDown = false;
131    private boolean mIsPeeking;
132    @DrawerState private int mDrawerState;
133    @IdRes private int mPeekResId = 0;
134    @IdRes private int mContentResId = 0;
135    public WearableDrawerView(Context context) {
136        this(context, null);
137    }
138
139    public WearableDrawerView(Context context, AttributeSet attrs) {
140        this(context, attrs, 0);
141    }
142
143    public WearableDrawerView(Context context, AttributeSet attrs, int defStyleAttr) {
144        this(context, attrs, defStyleAttr, 0);
145    }
146
147    public WearableDrawerView(
148            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
149        super(context, attrs, defStyleAttr, defStyleRes);
150        LayoutInflater.from(context).inflate(R.layout.ws_wearable_drawer_view, this, true);
151
152        setClickable(true);
153        setElevation(context.getResources()
154                .getDimension(R.dimen.ws_wearable_drawer_view_elevation));
155
156        mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container);
157        mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon);
158
159        mPeekContainer.setOnClickListener(
160                new OnClickListener() {
161                    @Override
162                    public void onClick(View v) {
163                        onPeekContainerClicked(v);
164                    }
165                });
166
167        parseAttributes(context, attrs, defStyleAttr, defStyleRes);
168    }
169
170    private static Drawable getDrawable(
171            Context context, TypedArray typedArray, @StyleableRes int index) {
172        Drawable background;
173        int backgroundResId =
174                typedArray.getResourceId(index, 0);
175        if (backgroundResId == 0) {
176            background = typedArray.getDrawable(index);
177        } else {
178            background = context.getDrawable(backgroundResId);
179        }
180        return background;
181    }
182
183    @Override
184    protected void onFinishInflate() {
185        super.onFinishInflate();
186
187        // Drawer content is added after the peek view, so we need to bring the peek view
188        // to the front so it shows on top of the content.
189        mPeekContainer.bringToFront();
190    }
191
192    /**
193     * Called when anything within the peek container is clicked. However, if a custom peek view is
194     * supplied and it handles the click, then this may not be called. The default behavior is to
195     * open the drawer.
196     */
197    public void onPeekContainerClicked(View v) {
198        mController.openDrawer();
199    }
200
201    @Override
202    protected void onAttachedToWindow() {
203        super.onAttachedToWindow();
204
205        // The peek view has a layout gravity of bottom for the top drawer, and a layout gravity
206        // of top for the bottom drawer. This is required so that the peek view shows. On the top
207        // drawer, the bottom peeks from the top, and on the bottom drawer, the top peeks.
208        // LayoutParams are not guaranteed to return a non-null value until a child is attached to
209        // the window.
210        LayoutParams peekParams = (LayoutParams) mPeekContainer.getLayoutParams();
211        if (!Gravity.isVertical(peekParams.gravity)) {
212            final boolean isTopDrawer =
213                    (((LayoutParams) getLayoutParams()).gravity & Gravity.VERTICAL_GRAVITY_MASK)
214                            == Gravity.TOP;
215            if (isTopDrawer) {
216                peekParams.gravity = Gravity.BOTTOM;
217                mPeekIcon.setImageResource(R.drawable.ws_ic_more_horiz_24dp_wht);
218            } else {
219                peekParams.gravity = Gravity.TOP;
220                mPeekIcon.setImageResource(R.drawable.ws_ic_more_vert_24dp_wht);
221            }
222            mPeekContainer.setLayoutParams(peekParams);
223        }
224    }
225
226    @Override
227    public void addView(View child, int index, ViewGroup.LayoutParams params) {
228        @IdRes int childId = child.getId();
229        if (childId != 0) {
230            if (childId == mPeekResId) {
231                setPeekContent(child, index, params);
232                return;
233            }
234            if (childId == mContentResId && !setDrawerContentWithoutAdding(child)) {
235                return;
236            }
237        }
238
239        super.addView(child, index, params);
240    }
241
242    int preferGravity() {
243        return Gravity.NO_GRAVITY;
244    }
245
246    ViewGroup getPeekContainer() {
247        return mPeekContainer;
248    }
249
250    void setDrawerController(WearableDrawerController controller) {
251        mController = controller;
252    }
253
254    /**
255     * Returns the drawer content view.
256     */
257    @Nullable
258    public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
362    public int getDrawerState() {
363        return mDrawerState;
364    }
365
366    /**
367     * Sets the {@link DrawerState}.
368     */
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     */
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     */
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     */
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     */
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     */
412    public void setIsLocked(boolean locked) {
413        mIsLocked = locked;
414    }
415
416    /**
417     * Returns true if the drawer is fully open.
418     */
419    public boolean isOpened() {
420        return mOpenedPercent == 1;
421    }
422
423    /**
424     * Returns true if the drawer is fully closed.
425     */
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     */
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     */
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     */
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     */
455    void setOpenedPercent(float openedPercent) {
456        mOpenedPercent = openedPercent;
457    }
458
459    private void parseAttributes(
460            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
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
470        Drawable background =
471                getDrawable(context, typedArray, R.styleable.WearableDrawerView_android_background);
472        int elevation = typedArray
473                .getDimensionPixelSize(R.styleable.WearableDrawerView_android_elevation, 0);
474        setBackground(background);
475        setElevation(elevation);
476
477        mContentResId = typedArray.getResourceId(R.styleable.WearableDrawerView_drawerContent, 0);
478        mPeekResId = typedArray.getResourceId(R.styleable.WearableDrawerView_peekView, 0);
479        mCanAutoPeek =
480                typedArray.getBoolean(R.styleable.WearableDrawerView_enableAutoPeek, mCanAutoPeek);
481        typedArray.recycle();
482    }
483
484    private void setPeekContent(View content, int index, ViewGroup.LayoutParams params) {
485        if (content == null) {
486            return;
487        }
488        if (mPeekContainer.getChildCount() > 0) {
489            mPeekContainer.removeAllViews();
490        }
491        mPeekContainer.addView(content, index, params);
492    }
493
494    /**
495     * @return {@code true} if this is a new and valid {@code content}.
496     */
497    private boolean setDrawerContentWithoutAdding(View content) {
498        if (content == mContent) {
499            return false;
500        }
501        if (mContent != null) {
502            removeView(mContent);
503        }
504
505        mContent = content;
506        return mContent != null;
507    }
508}
509