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