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