From b23976efdd6ffe42cb3b8fe6650fc77bd9a161e8 Mon Sep 17 00:00:00 2001 From: Oren Blasberg Date: Tue, 1 Sep 2015 14:55:42 -0700 Subject: MenuPopupHelper: Factor out a MenuPopup interface. Move much of the responsibility into implementations of this interface. Delegate functionality to it where appropriate. Provide a standard (non-cascading) implementation for this interface. This CL should have NO BEHAVIOR CHANGES. A follow-up CL will provide a cascading implementation, whereby a config variable will enable submenus to open side by side with their parent menus. That CL will be the first with functional/ actual behavior changes. Bug: 20127825 Change-Id: Iecac2d340dd8750ebe4e99162d447c9411f09227 --- core/java/android/widget/ForwardingListener.java | 2 +- .../com/android/internal/view/menu/MenuPopup.java | 122 ++++++++++ .../internal/view/menu/MenuPopupHelper.java | 183 +++------------ .../internal/view/menu/StandardMenuPopup.java | 246 +++++++++++++++++++++ core/res/res/values/config.xml | 4 + core/res/res/values/symbols.xml | 2 + 6 files changed, 410 insertions(+), 149 deletions(-) create mode 100644 core/java/com/android/internal/view/menu/MenuPopup.java create mode 100644 core/java/com/android/internal/view/menu/StandardMenuPopup.java diff --git a/core/java/android/widget/ForwardingListener.java b/core/java/android/widget/ForwardingListener.java index fd7140f779fe..7ddeff91c1e0 100644 --- a/core/java/android/widget/ForwardingListener.java +++ b/core/java/android/widget/ForwardingListener.java @@ -25,7 +25,7 @@ import android.view.ViewParent; import com.android.internal.view.menu.ShowableListMenu; /** - * Abstract class that forwards touch events to a {@link ListPopupWindow}. + * Abstract class that forwards touch events to a {@link ShowableListMenu}. * * @hide */ diff --git a/core/java/com/android/internal/view/menu/MenuPopup.java b/core/java/com/android/internal/view/menu/MenuPopup.java new file mode 100644 index 000000000000..91788cab49fa --- /dev/null +++ b/core/java/com/android/internal/view/menu/MenuPopup.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 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 com.android.internal.view.menu; + +import android.content.Context; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ListAdapter; +import android.widget.PopupWindow; + +/** + * Base class for a menu popup abstraction - i.e., some type of menu, housed in a popup window + * environment. + * + * @hide + */ +public abstract class MenuPopup implements ShowableListMenu, MenuPresenter { + + public abstract void setForceShowIcon(boolean forceShow); + + /** + * Adds the given menu to the popup. If this is the first menu shown it'll be displayed; if it's + * a submenu it will be displayed adjacent to the most recent menu (if supported by the + * implementation). + * + * @param menu + */ + public abstract void addMenu(MenuBuilder menu); + + public abstract void setGravity(int dropDownGravity); + + public abstract void setAnchorView(View anchor); + + /** + * Set a listener to receive a callback when the popup is dismissed. + * + * @param listener Listener that will be notified when the popup is dismissed. + */ + public abstract void setOnDismissListener(PopupWindow.OnDismissListener listener); + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + // Don't need to do anything; we added as a presenter in the constructor. + } + + @Override + public MenuView getMenuView(ViewGroup root) { + throw new UnsupportedOperationException("MenuPopups manage their own views"); + } + + @Override + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + @Override + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + @Override + public int getId() { + return 0; + } + + /** + * Measures the width of the given menu view. + * + * @param view The view to measure. + * @return The width. + */ + protected static int measureIndividualMenuWidth(ListAdapter adapter, ViewGroup parent, + Context context, int maxAllowedWidth) { + // Menus don't tend to be long, so this is more sane than it looks. + int maxWidth = 0; + View itemView = null; + int itemType = 0; + + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + final int positionType = adapter.getItemViewType(i); + if (positionType != itemType) { + itemType = positionType; + itemView = null; + } + + if (parent == null) { + parent = new FrameLayout(context); + } + + itemView = adapter.getView(i, itemView, parent); + itemView.measure(widthMeasureSpec, heightMeasureSpec); + + final int itemWidth = itemView.getMeasuredWidth(); + if (itemWidth >= maxAllowedWidth) { + return maxAllowedWidth; + } else if (itemWidth > maxWidth) { + maxWidth = itemWidth; + } + } + + return maxWidth; + } +} \ No newline at end of file diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index e6bc6c3ae044..9a47fc1834bb 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -17,57 +17,29 @@ package com.android.internal.view.menu; import android.content.Context; -import android.content.res.Resources; import android.os.Parcelable; import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; -import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.FrameLayout; -import android.widget.ListAdapter; -import android.widget.MenuPopupWindow; import android.widget.PopupWindow; -import java.util.ArrayList; - /** * Presents a menu as a small, simple popup anchored to another view. + * * @hide */ -public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener, - ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener, - View.OnAttachStateChangeListener, MenuPresenter { - static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout; - +public class MenuPopupHelper implements ViewTreeObserver.OnGlobalLayoutListener, + PopupWindow.OnDismissListener, View.OnAttachStateChangeListener, MenuPresenter { private final Context mContext; - private final LayoutInflater mInflater; private final MenuBuilder mMenu; - private final MenuAdapter mAdapter; private final boolean mOverflowOnly; - private final int mPopupMaxWidth; private final int mPopupStyleAttr; private final int mPopupStyleRes; private View mAnchorView; - private MenuPopupWindow mPopup; + private MenuPopup mPopup; private ViewTreeObserver mTreeObserver; - private Callback mPresenterCallback; - - boolean mForceShowIcon; - - private ViewGroup mMeasureParent; - - /** Whether the cached content width value is valid. */ - private boolean mHasContentWidth; - - /** Cached content width from {@link #measureContentWidth}. */ - private int mContentWidth; private int mDropDownGravity = Gravity.NO_GRAVITY; @@ -87,33 +59,37 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly, int popupStyleAttr, int popupStyleRes) { mContext = context; - mInflater = LayoutInflater.from(context); mMenu = menu; - mAdapter = new MenuAdapter(mMenu, mInflater, overflowOnly); mOverflowOnly = overflowOnly; mPopupStyleAttr = popupStyleAttr; mPopupStyleRes = popupStyleRes; - - final Resources res = context.getResources(); - mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, - res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); - mAnchorView = anchorView; + mPopup = createMenuPopup(); + } - // Present the menu using our context, not the menu builder's context. - menu.addMenuPresenter(this, context); + private MenuPopup createMenuPopup() { + if (mContext.getResources().getBoolean( + com.android.internal.R.bool.config_enableCascadingSubmenus)) { + // TODO: Return a Cascading implementation of MenuPopup instead. + return new StandardMenuPopup( + mContext, mMenu, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly); + } + return new StandardMenuPopup( + mContext, mMenu, mAnchorView, mPopupStyleAttr, mPopupStyleRes, mOverflowOnly); } public void setAnchorView(View anchor) { mAnchorView = anchor; + mPopup.setAnchorView(anchor); } public void setForceShowIcon(boolean forceShow) { - mForceShowIcon = forceShow; + mPopup.setForceShowIcon(forceShow); } public void setGravity(int gravity) { mDropDownGravity = gravity; + mPopup.setGravity(gravity); } public int getGravity() { @@ -126,28 +102,21 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On } } - public MenuPopupWindow getPopup() { + public ShowableListMenu getPopup() { return mPopup; } /** - * Attempts to show the popup anchored to the view specified by - * {@link #setAnchorView(View)}. + * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}. * - * @return {@code true} if the popup was shown or was already showing prior - * to calling this method, {@code false} otherwise + * @return {@code true} if the popup was shown or was already showing prior to calling this + * method, {@code false} otherwise */ public boolean tryShow() { if (isShowing()) { return true; } - mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); - mPopup.setOnDismissListener(this); - mPopup.setOnItemClickListener(this); - mPopup.setAdapter(mAdapter); - mPopup.setModal(true); - final View anchor = mAnchorView; if (anchor != null) { final boolean addGlobalListener = mTreeObserver == null; @@ -155,20 +124,19 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this); anchor.addOnAttachStateChangeListener(this); mPopup.setAnchorView(anchor); - mPopup.setDropDownGravity(mDropDownGravity); + mPopup.setGravity(mDropDownGravity); } else { return false; } - if (!mHasContentWidth) { - mContentWidth = measureContentWidth(); - mHasContentWidth = true; - } + // In order for subclasses of MenuPopupHelper to satisfy the OnDismissedListener interface, + // we must set the listener to this outer Helper rather than to the inner MenuPopup. + // Not to worry -- the inner MenuPopup will call our own #onDismiss method after it's done + // its own handling. + mPopup.setOnDismissListener(this); - mPopup.setContentWidth(mContentWidth); - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopup.addMenu(mMenu); mPopup.show(); - mPopup.getListView().setOnKeyListener(this); return true; } @@ -181,7 +149,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On @Override public void onDismiss() { mPopup = null; - mMenu.close(); if (mTreeObserver != null) { if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver(); mTreeObserver.removeGlobalOnLayoutListener(this); @@ -194,56 +161,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return mPopup != null && mPopup.isShowing(); } - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - MenuAdapter adapter = mAdapter; - adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0); - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { - dismiss(); - return true; - } - return false; - } - - private int measureContentWidth() { - // Menus don't tend to be long, so this is more sane than it looks. - int maxWidth = 0; - View itemView = null; - int itemType = 0; - - final ListAdapter adapter = mAdapter; - final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - final int count = adapter.getCount(); - for (int i = 0; i < count; i++) { - final int positionType = adapter.getItemViewType(i); - if (positionType != itemType) { - itemType = positionType; - itemView = null; - } - - if (mMeasureParent == null) { - mMeasureParent = new FrameLayout(mContext); - } - - itemView = adapter.getView(i, itemView, mMeasureParent); - itemView.measure(widthMeasureSpec, heightMeasureSpec); - - final int itemWidth = itemView.getMeasuredWidth(); - if (itemWidth >= mPopupMaxWidth) { - return mPopupMaxWidth; - } else if (itemWidth > maxWidth) { - maxWidth = itemWidth; - } - } - - return maxWidth; - } - @Override public void onGlobalLayout() { if (isShowing()) { @@ -282,54 +199,22 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On @Override public void updateMenuView(boolean cleared) { - mHasContentWidth = false; - - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } + mPopup.updateMenuView(cleared); } @Override public void setCallback(Callback cb) { - mPresenterCallback = cb; + mPopup.setCallback(cb); } @Override public boolean onSubMenuSelected(SubMenuBuilder subMenu) { - if (subMenu.hasVisibleItems()) { - MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView); - subPopup.setCallback(mPresenterCallback); - - boolean preserveIconSpacing = false; - final int count = subMenu.size(); - for (int i = 0; i < count; i++) { - MenuItem childItem = subMenu.getItem(i); - if (childItem.isVisible() && childItem.getIcon() != null) { - preserveIconSpacing = true; - break; - } - } - subPopup.setForceShowIcon(preserveIconSpacing); - - if (subPopup.tryShow()) { - if (mPresenterCallback != null) { - mPresenterCallback.onOpenSubMenu(subMenu); - } - return true; - } - } - return false; + return mPopup.onSubMenuSelected(subMenu); } @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { - // Only care about the (sub)menu we're presenting. - if (menu != mMenu) return; - - dismiss(); - if (mPresenterCallback != null) { - mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); - } + mPopup.onCloseMenu(menu, allMenusAreClosing); } @Override @@ -337,10 +222,12 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return false; } + @Override public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { return false; } + @Override public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { return false; } diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java new file mode 100644 index 000000000000..9a30ffafb75d --- /dev/null +++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java @@ -0,0 +1,246 @@ +package com.android.internal.view.menu; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Parcelable; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnKeyListener; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.MenuPopupWindow; +import android.widget.PopupWindow; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.PopupWindow.OnDismissListener; + +import com.android.internal.util.Preconditions; + +/** + * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the + * viewport. + */ +final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener, + MenuPresenter, OnKeyListener { + + private final Context mContext; + private final LayoutInflater mInflater; + private final MenuBuilder mMenu; + private final MenuAdapter mAdapter; + private final boolean mOverflowOnly; + private final int mPopupMaxWidth; + private final int mPopupStyleAttr; + private final int mPopupStyleRes; + + private PopupWindow.OnDismissListener mOnDismissListener; + + private View mAnchorView; + private MenuPopupWindow mPopup; + private Callback mPresenterCallback; + + private ViewGroup mMeasureParent; + + /** Whether the cached content width value is valid. */ + private boolean mHasContentWidth; + + /** Cached content width. */ + private int mContentWidth; + + private int mDropDownGravity = Gravity.NO_GRAVITY; + + public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr, + int popupStyleRes, boolean overflowOnly) { + mContext = Preconditions.checkNotNull(context); + mInflater = LayoutInflater.from(context); + mMenu = menu; + mOverflowOnly = overflowOnly; + mAdapter = new MenuAdapter(menu, mInflater, mOverflowOnly); + mPopupStyleAttr = popupStyleAttr; + mPopupStyleRes = popupStyleRes; + + final Resources res = context.getResources(); + mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, + res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); + + mAnchorView = anchorView; + + mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); + + // Present the menu using our context, not the menu builder's context. + menu.addMenuPresenter(this, context); + } + + @Override + public void setForceShowIcon(boolean forceShow) { + mAdapter.setForceShowIcon(forceShow); + } + + @Override + public void setGravity(int gravity) { + mDropDownGravity = gravity; + } + + private boolean tryShow() { + if (isShowing()) { + return true; + } + + mPopup.setOnDismissListener(this); + mPopup.setOnItemClickListener(this); + mPopup.setAdapter(mAdapter); + mPopup.setModal(true); + + final View anchor = mAnchorView; + if (anchor != null) { + mPopup.setAnchorView(anchor); + mPopup.setDropDownGravity(mDropDownGravity); + } else { + return false; + } + + if (!mHasContentWidth) { + mContentWidth = measureIndividualMenuWidth( + mAdapter, mMeasureParent, mContext, mPopupMaxWidth); + mHasContentWidth = true; + } + + mPopup.setContentWidth(mContentWidth); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopup.show(); + mPopup.getListView().setOnKeyListener(this); + return true; + } + + @Override + public void show() { + if (!tryShow()) { + throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); + } + } + + @Override + public void dismiss() { + if (isShowing()) { + mPopup.dismiss(); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MenuAdapter adapter = mAdapter; + adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0); + } + + @Override + public void addMenu(MenuBuilder menu) { + // No-op: standard implementation has only one menu which is set in the constructor. + } + + @Override + public boolean isShowing() { + return mPopup != null && mPopup.isShowing(); + } + + @Override + public void onDismiss() { + mPopup = null; + mMenu.close(); + + mOnDismissListener.onDismiss(); + } + + @Override + public void updateMenuView(boolean cleared) { + mHasContentWidth = false; + + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + @Override + public void setCallback(Callback cb) { + mPresenterCallback = cb; + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (subMenu.hasVisibleItems()) { + MenuPopupHelper subPopup = new MenuPopupHelper( + mContext, subMenu, mAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes); + subPopup.setCallback(mPresenterCallback); + + boolean preserveIconSpacing = false; + final int count = subMenu.size(); + for (int i = 0; i < count; i++) { + MenuItem childItem = subMenu.getItem(i); + if (childItem.isVisible() && childItem.getIcon() != null) { + preserveIconSpacing = true; + break; + } + } + subPopup.setForceShowIcon(preserveIconSpacing); + + if (subPopup.tryShow()) { + if (mPresenterCallback != null) { + mPresenterCallback.onOpenSubMenu(subMenu); + } + return true; + } + } + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + // Only care about the (sub)menu we're presenting. + if (menu != mMenu) return; + + dismiss(); + if (mPresenterCallback != null) { + mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + @Override + public boolean flagActionItems() { + return false; + } + + + @Override + public Parcelable onSaveInstanceState() { + return null; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + } + + @Override + public void setAnchorView(View anchor) { + mAnchorView = anchor; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { + dismiss(); + return true; + } + return false; + } + + @Override + public void setOnDismissListener(OnDismissListener listener) { + mOnDismissListener = listener; + } + + @Override + public ListView getListView() { + return mPopup.getListView(); + } +} \ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index ef4e26155ddf..c313392a40ca 100755 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2283,4 +2283,8 @@ + + + false diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e04c7438a224..58adabdad30c 100755 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2185,6 +2185,8 @@ + + -- cgit v1.2.3-59-g8ed1b