blob: c5c49fe389ce3feef7941d9b09dd029e8b2da3c4 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.wear.widget.drawer;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
import android.support.wear.R;
import android.support.wear.internal.widget.drawer.MultiPagePresenter;
import android.support.wear.internal.widget.drawer.MultiPageUi;
import android.support.wear.internal.widget.drawer.SinglePagePresenter;
import android.support.wear.internal.widget.drawer.SinglePageUi;
import android.support.wear.internal.widget.drawer.WearableNavigationDrawerPresenter;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.TimeUnit;
/**
* Ease of use class for creating a Wearable navigation drawer. This can be used with {@link
* WearableDrawerLayout} to create a drawer for users to easily navigate a wearable app.
*
* <p>There are two ways this information may be presented: as a single page and as multiple pages.
* The single page navigation drawer will display 1-7 items to the user representing different
* navigation verticals. If more than 7 items are provided to a single page navigation drawer, the
* navigation drawer will be displayed as empty. The multiple page navigation drawer will display 1
* or more pages to the user, each representing different navigation verticals.
*
* <p>The developer may specify which style to use with the {@code app:navigationStyle} custom
* attribute. If not specified, {@link #SINGLE_PAGE singlePage} will be used as the default.
*/
public class WearableNavigationDrawerView extends WearableDrawerView {
private static final String TAG = "WearableNavDrawer";
/**
* Listener which is notified when the user selects an item.
*/
public interface OnItemSelectedListener {
/**
* Notified when the user has selected an item at position {@code pos}.
*/
void onItemSelected(int pos);
}
/**
* Enumeration of possible drawer styles.
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY)
@IntDef({SINGLE_PAGE, MULTI_PAGE})
public @interface NavigationStyle {}
/**
* Single page navigation drawer style. This is the default drawer style. It is ideal for 1-5
* items, but works with up to 7 items. If more than 7 items exist, then the drawer will be
* displayed as empty.
*/
public static final int SINGLE_PAGE = 0;
/**
* Multi-page navigation drawer style. Each item is on its own page. Useful when more than 7
* items exist.
*/
public static final int MULTI_PAGE = 1;
@NavigationStyle private static final int DEFAULT_STYLE = SINGLE_PAGE;
private static final long AUTO_CLOSE_DRAWER_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
private final boolean mIsAccessibilityEnabled;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final Runnable mCloseDrawerRunnable =
new Runnable() {
@Override
public void run() {
getController().closeDrawer();
}
};
/**
* Listens for single taps on the drawer.
*/
@Nullable private final GestureDetector mGestureDetector;
@NavigationStyle private final int mNavigationStyle;
private final WearableNavigationDrawerPresenter mPresenter;
private final SimpleOnGestureListener mOnGestureListener =
new SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
return mPresenter.onDrawerTapped();
}
};
public WearableNavigationDrawerView(Context context) {
this(context, (AttributeSet) null);
}
public WearableNavigationDrawerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WearableNavigationDrawerView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public WearableNavigationDrawerView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mGestureDetector = new GestureDetector(getContext(), mOnGestureListener);
@NavigationStyle int navStyle = DEFAULT_STYLE;
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.WearableNavigationDrawerView,
defStyleAttr,
0 /* defStyleRes */);
//noinspection WrongConstant
navStyle = typedArray.getInt(
R.styleable.WearableNavigationDrawerView_navigationStyle, DEFAULT_STYLE);
typedArray.recycle();
}
mNavigationStyle = navStyle;
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mIsAccessibilityEnabled = accessibilityManager.isEnabled();
mPresenter =
mNavigationStyle == SINGLE_PAGE
? new SinglePagePresenter(new SinglePageUi(this), mIsAccessibilityEnabled)
: new MultiPagePresenter(this, new MultiPageUi(), mIsAccessibilityEnabled);
getPeekContainer()
.setContentDescription(
context.getString(R.string.ws_navigation_drawer_content_description));
setOpenOnlyAtTopEnabled(true);
}
/**
* Set a {@link WearableNavigationDrawerAdapter} that will supply data for this drawer.
*/
public void setAdapter(final WearableNavigationDrawerAdapter adapter) {
mPresenter.onNewAdapter(adapter);
}
/**
* Add an {@link OnItemSelectedListener} that will be notified when the user selects an item.
*/
public void addOnItemSelectedListener(OnItemSelectedListener listener) {
mPresenter.onItemSelectedListenerAdded(listener);
}
/**
* Remove an {@link OnItemSelectedListener}.
*/
public void removeOnItemSelectedListener(OnItemSelectedListener listener) {
mPresenter.onItemSelectedListenerRemoved(listener);
}
/**
* Changes which index is selected. {@link OnItemSelectedListener#onItemSelected} will
* be called when the specified {@code index} is reached, but it won't be called for items
* between the current index and the destination index.
*/
public void setCurrentItem(int index, boolean smoothScrollTo) {
mPresenter.onSetCurrentItemRequested(index, smoothScrollTo);
}
/**
* Returns the style this drawer is using, either {@link #SINGLE_PAGE} or {@link #MULTI_PAGE}.
*/
@NavigationStyle
public int getNavigationStyle() {
return mNavigationStyle;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
autoCloseDrawerAfterDelay();
return mGestureDetector != null && mGestureDetector.onTouchEvent(ev);
}
@Override
public boolean canScrollHorizontally(int direction) {
// Prevent the window from being swiped closed while it is open by saying that it can scroll
// horizontally.
return isOpened();
}
@Override
public void onDrawerOpened() {
autoCloseDrawerAfterDelay();
}
@Override
public void onDrawerClosed() {
mMainThreadHandler.removeCallbacks(mCloseDrawerRunnable);
}
private void autoCloseDrawerAfterDelay() {
if (!mIsAccessibilityEnabled) {
mMainThreadHandler.removeCallbacks(mCloseDrawerRunnable);
mMainThreadHandler.postDelayed(mCloseDrawerRunnable, AUTO_CLOSE_DRAWER_DELAY_MS);
}
}
@Override
/* package */ int preferGravity() {
return Gravity.TOP;
}
/**
* Adapter for specifying the contents of WearableNavigationDrawer.
*/
public abstract static class WearableNavigationDrawerAdapter {
@Nullable
private WearableNavigationDrawerPresenter mPresenter;
/**
* Get the text associated with the item at {@code pos}.
*/
public abstract CharSequence getItemText(int pos);
/**
* Get the drawable associated with the item at {@code pos}.
*/
public abstract Drawable getItemDrawable(int pos);
/**
* Returns the number of items in this adapter.
*/
public abstract int getCount();
/**
* This method should be called by the application if the data backing this adapter has
* changed and associated views should update.
*/
public void notifyDataSetChanged() {
// If this method is called before drawer.setAdapter, then we will not yet have a
// presenter.
if (mPresenter != null) {
mPresenter.onDataSetChanged();
} else {
Log.w(TAG,
"adapter.notifyDataSetChanged called before drawer.setAdapter; ignoring.");
}
}
/**
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public void setPresenter(WearableNavigationDrawerPresenter presenter) {
mPresenter = presenter;
}
}
}