blob: bde4629324ad4f9776c8ffd79095b67689dfbece [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 androidx.wear.widget.drawer;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.wear.R;
import androidx.wear.internal.widget.ResourcesUtil;
import androidx.wear.widget.drawer.WearableActionDrawerMenu.WearableActionDrawerMenuItem;
/**
* Ease of use class for creating a Wearable action drawer. This can be used with {@link
* WearableDrawerLayout} to create a drawer for users to easily pull up contextual actions. These
* contextual actions may be specified by using a {@link Menu}, which may be populated by either:
*
* <ul> <li>Specifying the {@code app:actionMenu} attribute in the XML layout file. Example:
* <pre>
* &lt;androidx.wear.widget.drawer.WearableActionDrawerView
* xmlns:app="http://schemas.android.com/apk/res-auto"
* android:layout_width=”match_parent”
* android:layout_height=”match_parent”
* app:actionMenu="@menu/action_drawer" /&gt;</pre>
*
* <li>Getting the menu with {@link #getMenu}, and then inflating it with {@link
* MenuInflater#inflate}. Example:
* <pre>
* Menu menu = actionDrawer.getMenu();
* getMenuInflater().inflate(R.menu.action_drawer, menu);</pre>
*
* </ul>
*
* <p><b>The full {@link Menu} and {@link MenuItem} APIs are not implemented.</b> The following
* methods are guaranteed to work:
*
* <p>For {@link Menu}, the add methods, {@link Menu#clear}, {@link Menu#removeItem}, {@link
* Menu#findItem}, {@link Menu#size}, and {@link Menu#getItem} are implemented.
*
* <p>For {@link MenuItem}, setting and getting the title and icon, {@link MenuItem#getItemId}, and
* {@link MenuItem#setOnMenuItemClickListener} are implemented.
*/
public class WearableActionDrawerView extends WearableDrawerView {
private static final String TAG = "WearableActionDrawer";
final RecyclerView mActionList;
final int mTopPadding;
final int mBottomPadding;
final int mLeftPadding;
final int mRightPadding;
final int mFirstItemTopPadding;
final int mLastItemBottomPadding;
final int mIconRightMargin;
private final boolean mShowOverflowInPeek;
@Nullable private final ImageView mPeekActionIcon;
@Nullable private final ImageView mPeekExpandIcon;
final RecyclerView.Adapter<RecyclerView.ViewHolder> mActionListAdapter;
private OnMenuItemClickListener mOnMenuItemClickListener;
private Menu mMenu;
@Nullable CharSequence mTitle;
public WearableActionDrawerView(Context context) {
this(context, null);
}
public WearableActionDrawerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WearableActionDrawerView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public WearableActionDrawerView(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLockedWhenClosed(true);
boolean showOverflowInPeek = false;
int menuRes = 0;
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(
attrs, R.styleable.WearableActionDrawerView, defStyleAttr, 0 /* defStyleRes */);
ViewCompat.saveAttributeDataForStyleable(
this, context, R.styleable.WearableActionDrawerView, attrs, typedArray,
defStyleAttr, 0);
try {
mTitle = typedArray.getString(R.styleable.WearableActionDrawerView_drawerTitle);
showOverflowInPeek = typedArray.getBoolean(
R.styleable.WearableActionDrawerView_showOverflowInPeek, false);
menuRes = typedArray
.getResourceId(R.styleable.WearableActionDrawerView_actionMenu, 0);
} finally {
typedArray.recycle();
}
}
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mShowOverflowInPeek = showOverflowInPeek || accessibilityManager.isEnabled();
if (!mShowOverflowInPeek) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
View peekView = layoutInflater.inflate(R.layout.ws_action_drawer_peek_view,
getPeekContainer(), false /* attachToRoot */);
setPeekContent(peekView);
mPeekActionIcon = peekView.findViewById(R.id.ws_action_drawer_peek_action_icon);
mPeekExpandIcon = peekView.findViewById(R.id.ws_action_drawer_expand_icon);
} else {
mPeekActionIcon = null;
mPeekExpandIcon = null;
getPeekContainer().setContentDescription(
context.getString(R.string.ws_action_drawer_content_description));
}
if (menuRes != 0) {
// This must occur after initializing mPeekActionIcon, otherwise updatePeekIcons will
// exit early.
MenuInflater inflater = new MenuInflater(context);
inflater.inflate(menuRes, getMenu());
}
int screenWidthPx = ResourcesUtil.getScreenWidthPx(context);
int screenHeightPx = ResourcesUtil.getScreenHeightPx(context);
Resources res = getResources();
mTopPadding = res.getDimensionPixelOffset(R.dimen.ws_action_drawer_item_top_padding);
mBottomPadding = res.getDimensionPixelOffset(R.dimen.ws_action_drawer_item_bottom_padding);
mLeftPadding =
ResourcesUtil.getFractionOfScreenPx(
context, screenWidthPx, R.fraction.ws_action_drawer_item_left_padding);
mRightPadding =
ResourcesUtil.getFractionOfScreenPx(
context, screenWidthPx, R.fraction.ws_action_drawer_item_right_padding);
mFirstItemTopPadding =
ResourcesUtil.getFractionOfScreenPx(
context, screenHeightPx,
R.fraction.ws_action_drawer_item_first_item_top_padding);
mLastItemBottomPadding =
ResourcesUtil.getFractionOfScreenPx(
context, screenHeightPx,
R.fraction.ws_action_drawer_item_last_item_bottom_padding);
mIconRightMargin = res
.getDimensionPixelOffset(R.dimen.ws_action_drawer_item_icon_right_margin);
mActionList = new RecyclerView(context);
mActionList.setId(R.id.action_list);
mActionList.setLayoutManager(new LinearLayoutManager(context));
mActionListAdapter = new ActionListAdapter(getMenu());
}
@Override
public void onDrawerOpened() {
setContentIfFirstCall();
if (mActionListAdapter.getItemCount() > 0) {
RecyclerView.ViewHolder holder = mActionList.findViewHolderForAdapterPosition(0);
if (holder != null && holder.itemView != null) {
holder.itemView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}
}
private void setContentIfFirstCall() {
if (mActionList.getAdapter() == null) {
mActionList.setAdapter(mActionListAdapter);
setDrawerContent(mActionList);
}
}
@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 onPeekContainerClicked(View v) {
if (mShowOverflowInPeek) {
super.onPeekContainerClicked(v);
} else {
onMenuItemClicked(0);
}
}
@Override
/* package */ int preferGravity() {
return Gravity.BOTTOM;
}
/**
* Set a {@link OnMenuItemClickListener} for this action drawer.
*/
public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
mOnMenuItemClickListener = listener;
}
/**
* Sets the title for this action drawer. If {@code title} is {@code null}, then the title will
* be removed.
*/
public void setTitle(@Nullable CharSequence title) {
if (TextUtils.equals(title, mTitle)) {
return;
}
CharSequence oldTitle = mTitle;
mTitle = title;
if (oldTitle == null) {
mActionListAdapter.notifyItemInserted(0);
} else if (title == null) {
mActionListAdapter.notifyItemRemoved(0);
} else {
mActionListAdapter.notifyItemChanged(0);
}
}
boolean hasTitle() {
return mTitle != null;
}
void onMenuItemClicked(int position) {
if (position >= 0 && position < getMenu().size()) { // Sanity check.
WearableActionDrawerMenuItem menuItem =
(WearableActionDrawerMenuItem) getMenu().getItem(position);
if (menuItem.invoke()) {
return;
}
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onMenuItemClick(menuItem);
}
}
}
void updatePeekIcons() {
if (mPeekActionIcon == null || mPeekExpandIcon == null) {
return;
}
Menu menu = getMenu();
int numberOfActions = menu.size();
// Only show drawer content (and allow it to be opened) when there's more than one action.
if (numberOfActions > 1) {
setDrawerContent(mActionList);
mPeekExpandIcon.setVisibility(VISIBLE);
} else {
setDrawerContent(null);
mPeekExpandIcon.setVisibility(GONE);
}
if (numberOfActions >= 1) {
Drawable firstActionDrawable = menu.getItem(0).getIcon();
// Because the ImageView will tint the Drawable white, attempt to get a mutable copy of
// it. If a copy isn't made, the icon will be white in the expanded state, rendering it
// invisible.
if (firstActionDrawable != null) {
firstActionDrawable = firstActionDrawable.getConstantState().newDrawable().mutate();
firstActionDrawable.clearColorFilter();
}
mPeekActionIcon.setImageDrawable(firstActionDrawable);
mPeekActionIcon.setContentDescription(menu.getItem(0).getTitle());
}
}
/**
* Returns the Menu object that this WearableActionDrawer represents.
*
* <p>Applications should use this method to obtain the WearableActionDrawers's Menu object and
* inflate or add content to it as necessary.
*
* @return the Menu presented by this view
*/
public Menu getMenu() {
if (mMenu == null) {
mMenu = new WearableActionDrawerMenu(
getContext(),
new WearableActionDrawerMenu.WearableActionDrawerMenuListener() {
@Override
public void menuItemChanged(int position) {
if (mActionListAdapter != null) {
int listPosition = hasTitle() ? position + 1 : position;
mActionListAdapter.notifyItemChanged(listPosition);
}
if (position == 0) {
updatePeekIcons();
}
}
@Override
public void menuItemAdded(int position) {
if (mActionListAdapter != null) {
int listPosition = hasTitle() ? position + 1 : position;
mActionListAdapter.notifyItemInserted(listPosition);
}
// Handle transitioning from 0->1 items (set peek icon) and
// 1->2 (switch to ellipsis.)
if (position <= 1) {
updatePeekIcons();
}
}
@Override
public void menuItemRemoved(int position) {
if (mActionListAdapter != null) {
int listPosition = hasTitle() ? position + 1 : position;
mActionListAdapter.notifyItemRemoved(listPosition);
}
// Handle transitioning from 2->1 items (remove ellipsis), and
// also the removal of item 1, which could cause the peek icon
// to change.
if (position <= 1) {
updatePeekIcons();
}
}
@Override
public void menuChanged() {
if (mActionListAdapter != null) {
mActionListAdapter.notifyDataSetChanged();
}
updatePeekIcons();
}
});
}
return mMenu;
}
private static final class TitleViewHolder extends RecyclerView.ViewHolder {
public final View view;
public final TextView textView;
TitleViewHolder(View view) {
super(view);
this.view = view;
textView = (TextView) view.findViewById(R.id.ws_action_drawer_title);
}
}
private final class ActionListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int TYPE_ACTION = 0;
public static final int TYPE_TITLE = 1;
private final Menu mActionMenu;
private final View.OnClickListener mItemClickListener =
new View.OnClickListener() {
@Override
public void onClick(View v) {
int childPos =
mActionList.getChildAdapterPosition(v) - (hasTitle() ? 1 : 0);
if (childPos == RecyclerView.NO_POSITION) {
Log.w(TAG, "invalid child position");
return;
}
onMenuItemClicked(childPos);
}
};
ActionListAdapter(Menu menu) {
mActionMenu = getMenu();
}
@Override
public int getItemCount() {
return mActionMenu.size() + (hasTitle() ? 1 : 0);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
int titleAwarePosition = hasTitle() ? position - 1 : position;
if (viewHolder instanceof ActionItemViewHolder) {
ActionItemViewHolder holder = (ActionItemViewHolder) viewHolder;
holder.view.setPadding(
mLeftPadding,
position == 0 ? mFirstItemTopPadding : mTopPadding,
mRightPadding,
position == getItemCount() - 1 ? mLastItemBottomPadding : mBottomPadding);
Drawable icon = mActionMenu.getItem(titleAwarePosition).getIcon();
if (icon != null) {
icon = icon.getConstantState().newDrawable().mutate();
}
CharSequence title = mActionMenu.getItem(titleAwarePosition).getTitle();
holder.textView.setText(title);
holder.textView.setContentDescription(title);
holder.iconView.setImageDrawable(icon);
} else if (viewHolder instanceof TitleViewHolder) {
TitleViewHolder holder = (TitleViewHolder) viewHolder;
holder.textView.setPadding(0, mFirstItemTopPadding, 0, mBottomPadding);
holder.textView.setText(mTitle);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_TITLE:
View titleView =
LayoutInflater.from(parent.getContext())
.inflate(R.layout.ws_action_drawer_title_view, parent, false);
return new TitleViewHolder(titleView);
case TYPE_ACTION:
default:
View actionView =
LayoutInflater.from(parent.getContext())
.inflate(R.layout.ws_action_drawer_item_view, parent, false);
actionView.setOnClickListener(mItemClickListener);
return new ActionItemViewHolder(actionView);
}
}
@Override
public int getItemViewType(int position) {
return hasTitle() && position == 0 ? TYPE_TITLE : TYPE_ACTION;
}
}
private final class ActionItemViewHolder extends RecyclerView.ViewHolder {
public final View view;
public final ImageView iconView;
public final TextView textView;
ActionItemViewHolder(View view) {
super(view);
this.view = view;
iconView = (ImageView) view.findViewById(R.id.ws_action_drawer_item_icon);
((LinearLayout.LayoutParams) iconView.getLayoutParams()).setMarginEnd(mIconRightMargin);
textView = (TextView) view.findViewById(R.id.ws_action_drawer_item_text);
}
}
}