Home | History | Annotate | Download | only in drawer
      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.Nullable;
     33 import androidx.annotation.RestrictTo;
     34 import androidx.annotation.RestrictTo.Scope;
     35 import androidx.annotation.StyleableRes;
     36 import androidx.customview.widget.ViewDragHelper;
     37 import androidx.wear.R;
     38 
     39 import java.lang.annotation.Retention;
     40 import 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  */
     89 public 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