| /* |
| * 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> |
| * <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" /></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); |
| } |
| } |
| } |