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.os.Handler;
     23 import android.os.Looper;
     24 import android.util.AttributeSet;
     25 import android.util.Log;
     26 import android.view.GestureDetector;
     27 import android.view.GestureDetector.SimpleOnGestureListener;
     28 import android.view.Gravity;
     29 import android.view.MotionEvent;
     30 import android.view.accessibility.AccessibilityManager;
     31 
     32 import androidx.annotation.IntDef;
     33 import androidx.annotation.Nullable;
     34 import androidx.annotation.RestrictTo;
     35 import androidx.annotation.RestrictTo.Scope;
     36 import androidx.wear.R;
     37 import androidx.wear.internal.widget.drawer.MultiPagePresenter;
     38 import androidx.wear.internal.widget.drawer.MultiPageUi;
     39 import androidx.wear.internal.widget.drawer.SinglePagePresenter;
     40 import androidx.wear.internal.widget.drawer.SinglePageUi;
     41 import androidx.wear.internal.widget.drawer.WearableNavigationDrawerPresenter;
     42 
     43 import java.lang.annotation.Retention;
     44 import java.lang.annotation.RetentionPolicy;
     45 import java.util.concurrent.TimeUnit;
     46 
     47 /**
     48  * Ease of use class for creating a Wearable navigation drawer. This can be used with {@link
     49  * WearableDrawerLayout} to create a drawer for users to easily navigate a wearable app.
     50  *
     51  * <p>There are two ways this information may be presented: as a single page and as multiple pages.
     52  * The single page navigation drawer will display 1-7 items to the user representing different
     53  * navigation verticals. If more than 7 items are provided to a single page navigation drawer, the
     54  * navigation drawer will be displayed as empty. The multiple page navigation drawer will display 1
     55  * or more pages to the user, each representing different navigation verticals.
     56  *
     57  * <p>The developer may specify which style to use with the {@code app:navigationStyle} custom
     58  * attribute. If not specified, {@link #SINGLE_PAGE singlePage} will be used as the default.
     59  */
     60 public class WearableNavigationDrawerView extends WearableDrawerView {
     61 
     62     private static final String TAG = "WearableNavDrawer";
     63 
     64     /**
     65      * Listener which is notified when the user selects an item.
     66      */
     67     public interface OnItemSelectedListener {
     68 
     69         /**
     70          * Notified when the user has selected an item at position {@code pos}.
     71          */
     72         void onItemSelected(int pos);
     73     }
     74 
     75     /**
     76      * Enumeration of possible drawer styles.
     77      * @hide
     78      */
     79     @Retention(RetentionPolicy.SOURCE)
     80     @RestrictTo(Scope.LIBRARY)
     81     @IntDef({SINGLE_PAGE, MULTI_PAGE})
     82     public @interface NavigationStyle {}
     83 
     84     /**
     85      * Single page navigation drawer style. This is the default drawer style. It is ideal for 1-5
     86      * items, but works with up to 7 items. If more than 7 items exist, then the drawer will be
     87      * displayed as empty.
     88      */
     89     public static final int SINGLE_PAGE = 0;
     90 
     91     /**
     92      * Multi-page navigation drawer style. Each item is on its own page. Useful when more than 7
     93      * items exist.
     94      */
     95     public static final int MULTI_PAGE = 1;
     96 
     97     @NavigationStyle private static final int DEFAULT_STYLE = SINGLE_PAGE;
     98     private static final long AUTO_CLOSE_DRAWER_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
     99     private final boolean mIsAccessibilityEnabled;
    100     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    101     private final Runnable mCloseDrawerRunnable =
    102             new Runnable() {
    103                 @Override
    104                 public void run() {
    105                     getController().closeDrawer();
    106                 }
    107             };
    108     /**
    109      * Listens for single taps on the drawer.
    110      */
    111     @Nullable private final GestureDetector mGestureDetector;
    112     @NavigationStyle private final int mNavigationStyle;
    113     private final WearableNavigationDrawerPresenter mPresenter;
    114     private final SimpleOnGestureListener mOnGestureListener =
    115             new SimpleOnGestureListener() {
    116                 @Override
    117                 public boolean onSingleTapUp(MotionEvent e) {
    118                     return mPresenter.onDrawerTapped();
    119                 }
    120             };
    121     public WearableNavigationDrawerView(Context context) {
    122         this(context, (AttributeSet) null);
    123     }
    124     public WearableNavigationDrawerView(Context context, AttributeSet attrs) {
    125         this(context, attrs, 0);
    126     }
    127 
    128     public WearableNavigationDrawerView(Context context, AttributeSet attrs, int defStyleAttr) {
    129         this(context, attrs, defStyleAttr, 0);
    130     }
    131 
    132     public WearableNavigationDrawerView(Context context, AttributeSet attrs, int defStyleAttr,
    133             int defStyleRes) {
    134         super(context, attrs, defStyleAttr, defStyleRes);
    135 
    136         mGestureDetector = new GestureDetector(getContext(), mOnGestureListener);
    137 
    138         @NavigationStyle int navStyle = DEFAULT_STYLE;
    139         if (attrs != null) {
    140             TypedArray typedArray = context.obtainStyledAttributes(
    141                     attrs,
    142                     R.styleable.WearableNavigationDrawerView,
    143                     defStyleAttr,
    144                     0 /* defStyleRes */);
    145 
    146             //noinspection WrongConstant
    147             navStyle = typedArray.getInt(
    148                     R.styleable.WearableNavigationDrawerView_navigationStyle, DEFAULT_STYLE);
    149             typedArray.recycle();
    150         }
    151 
    152         mNavigationStyle = navStyle;
    153         AccessibilityManager accessibilityManager =
    154                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    155         mIsAccessibilityEnabled = accessibilityManager.isEnabled();
    156 
    157         mPresenter =
    158                 mNavigationStyle == SINGLE_PAGE
    159                         ? new SinglePagePresenter(new SinglePageUi(this), mIsAccessibilityEnabled)
    160                         : new MultiPagePresenter(this, new MultiPageUi(), mIsAccessibilityEnabled);
    161 
    162         getPeekContainer()
    163                 .setContentDescription(
    164                         context.getString(R.string.ws_navigation_drawer_content_description));
    165 
    166         setOpenOnlyAtTopEnabled(true);
    167     }
    168 
    169     /**
    170      * Set a {@link WearableNavigationDrawerAdapter} that will supply data for this drawer.
    171      */
    172     public void setAdapter(final WearableNavigationDrawerAdapter adapter) {
    173         mPresenter.onNewAdapter(adapter);
    174     }
    175 
    176     /**
    177      * Add an {@link OnItemSelectedListener} that will be notified when the user selects an item.
    178      */
    179     public void addOnItemSelectedListener(OnItemSelectedListener listener) {
    180         mPresenter.onItemSelectedListenerAdded(listener);
    181     }
    182 
    183     /**
    184      * Remove an {@link OnItemSelectedListener}.
    185      */
    186     public void removeOnItemSelectedListener(OnItemSelectedListener listener) {
    187         mPresenter.onItemSelectedListenerRemoved(listener);
    188     }
    189 
    190     /**
    191      * Changes which index is selected. {@link OnItemSelectedListener#onItemSelected} will
    192      * be called when the specified {@code index} is reached, but it won't be called for items
    193      * between the current index and the destination index.
    194      */
    195     public void setCurrentItem(int index, boolean smoothScrollTo) {
    196         mPresenter.onSetCurrentItemRequested(index, smoothScrollTo);
    197     }
    198 
    199     /**
    200      * Returns the style this drawer is using, either {@link #SINGLE_PAGE} or {@link #MULTI_PAGE}.
    201      */
    202     @NavigationStyle
    203     public int getNavigationStyle() {
    204         return mNavigationStyle;
    205     }
    206 
    207     @Override
    208     public boolean onInterceptTouchEvent(MotionEvent ev) {
    209         autoCloseDrawerAfterDelay();
    210         return mGestureDetector != null && mGestureDetector.onTouchEvent(ev);
    211     }
    212 
    213     @Override
    214     public boolean canScrollHorizontally(int direction) {
    215         // Prevent the window from being swiped closed while it is open by saying that it can scroll
    216         // horizontally.
    217         return isOpened();
    218     }
    219 
    220     @Override
    221     public void onDrawerOpened() {
    222         autoCloseDrawerAfterDelay();
    223     }
    224 
    225     @Override
    226     public void onDrawerClosed() {
    227         mMainThreadHandler.removeCallbacks(mCloseDrawerRunnable);
    228     }
    229 
    230     private void autoCloseDrawerAfterDelay() {
    231         if (!mIsAccessibilityEnabled) {
    232             mMainThreadHandler.removeCallbacks(mCloseDrawerRunnable);
    233             mMainThreadHandler.postDelayed(mCloseDrawerRunnable, AUTO_CLOSE_DRAWER_DELAY_MS);
    234         }
    235     }
    236 
    237     @Override
    238   /* package */ int preferGravity() {
    239         return Gravity.TOP;
    240     }
    241 
    242     /**
    243      * Adapter for specifying the contents of WearableNavigationDrawer.
    244      */
    245     public abstract static class WearableNavigationDrawerAdapter {
    246 
    247         @Nullable
    248         private WearableNavigationDrawerPresenter mPresenter;
    249 
    250         /**
    251          * Get the text associated with the item at {@code pos}.
    252          */
    253         public abstract CharSequence getItemText(int pos);
    254 
    255         /**
    256          * Get the drawable associated with the item at {@code pos}.
    257          */
    258         public abstract Drawable getItemDrawable(int pos);
    259 
    260         /**
    261          * Returns the number of items in this adapter.
    262          */
    263         public abstract int getCount();
    264 
    265         /**
    266          * This method should be called by the application if the data backing this adapter has
    267          * changed and associated views should update.
    268          */
    269         public void notifyDataSetChanged() {
    270             // If this method is called before drawer.setAdapter, then we will not yet have a
    271             // presenter.
    272             if (mPresenter != null) {
    273                 mPresenter.onDataSetChanged();
    274             } else {
    275                 Log.w(TAG,
    276                         "adapter.notifyDataSetChanged called before drawer.setAdapter; ignoring.");
    277             }
    278         }
    279 
    280         /**
    281          * @hide
    282          */
    283         @RestrictTo(Scope.LIBRARY)
    284         public void setPresenter(WearableNavigationDrawerPresenter presenter) {
    285             mPresenter = presenter;
    286         }
    287     }
    288 
    289 }
    290