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