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