From f29cfbc563ebc415a679f4db81cc72895036e07e Mon Sep 17 00:00:00 2001 From: serso Date: Fri, 1 Apr 2016 20:52:54 +0200 Subject: [PATCH] Custom popup menu classes copied to the project The original classes can be found in Appcompat library --- .../android/widget/menu/CustomPopupMenu.java | 285 +++++++++++++ .../widget/menu/CustomPopupMenuHelper.java | 395 ++++++++++++++++++ 2 files changed, 680 insertions(+) create mode 100644 app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenu.java create mode 100644 app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenuHelper.java diff --git a/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenu.java b/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenu.java new file mode 100644 index 00000000..2866958b --- /dev/null +++ b/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenu.java @@ -0,0 +1,285 @@ +package org.solovyev.android.widget.menu; + +/* + * Copyright (C) 2014 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. + */ + +import android.content.Context; +import android.support.annotation.MenuRes; +import android.support.v7.appcompat.R; +import android.support.v7.view.SupportMenuInflater; +import android.support.v7.view.menu.MenuBuilder; +import android.support.v7.view.menu.MenuPopupHelper; +import android.support.v7.view.menu.MenuPresenter; +import android.support.v7.view.menu.SubMenuBuilder; +import android.support.v7.widget.ListPopupWindow; +import android.view.*; + +/** + * Static library support version of the framework's {@link android.widget.PopupMenu}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class CustomPopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { + private Context mContext; + private MenuBuilder mMenu; + private View mAnchor; + private CustomPopupMenuHelper mPopup; + private OnMenuItemClickListener mMenuItemClickListener; + private OnDismissListener mDismissListener; + private View.OnTouchListener mDragListener; + + /** + * Construct a new PopupMenu. + * + * @param context Context for the PopupMenu. + * @param anchor Anchor view for this popup. The popup will appear below the anchor if there + * is room, or above it if there is not. + */ + public CustomPopupMenu(Context context, View anchor) { + this(context, anchor, Gravity.NO_GRAVITY); + } + + /** + * Constructor to create a new popup menu with an anchor view and alignment + * gravity. + * + * @param context Context the popup menu is running in, through which it + * can access the current theme, resources, etc. + * @param anchor Anchor view for this popup. The popup will appear below + * the anchor if there is room, or above it if there is not. + * @param gravity The {@link Gravity} value for aligning the popup with its + * anchor. + */ + public CustomPopupMenu(Context context, View anchor, int gravity) { + this(context, anchor, gravity, R.attr.popupMenuStyle, 0); + } + + /** + * Constructor a create a new popup menu with a specific style. + * + * @param context Context the popup menu is running in, through which it + * can access the current theme, resources, etc. + * @param anchor Anchor view for this popup. The popup will appear below + * the anchor if there is room, or above it if there is not. + * @param gravity The {@link Gravity} value for aligning the popup with its + * anchor. + * @param popupStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the popup window. Can be 0 to not look for defaults. + * @param popupStyleRes A resource identifier of a style resource that + * supplies default values for the popup window, used only if + * popupStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public CustomPopupMenu(Context context, View anchor, int gravity, int popupStyleAttr, + int popupStyleRes) { + mContext = context; + mMenu = new MenuBuilder(context); + mMenu.setCallback(this); + mAnchor = anchor; + mPopup = new CustomPopupMenuHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes); + mPopup.setGravity(gravity); + mPopup.setCallback(this); + } + + /** + * @return the gravity used to align the popup window to its anchor view + * @see #setGravity(int) + */ + public int getGravity() { + return mPopup.getGravity(); + } + + /** + * Sets the gravity used to align the popup window to its anchor view. + *

+ * If the popup is showing, calling this method will take effect only + * the next time the popup is shown. + * + * @param gravity the gravity used to align the popup window + * @see #getGravity() + */ + public void setGravity(int gravity) { + mPopup.setGravity(gravity); + } + + /** + * Returns an {@link android.view.View.OnTouchListener} that can be added to the anchor view + * to implement drag-to-open behavior. + *

+ * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

+ * Example usage: + *

+     * PopupMenu myPopup = new PopupMenu(context, myAnchor);
+     * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
+     * 
+ * + * @return a touch listener that controls drag-to-open behavior + */ + public View.OnTouchListener getDragToOpenListener() { + if (mDragListener == null) { + mDragListener = new ListPopupWindow.ForwardingListener(mAnchor) { + @Override + protected boolean onForwardingStarted() { + show(); + return true; + } + + @Override + protected boolean onForwardingStopped() { + dismiss(); + return true; + } + + @Override + public ListPopupWindow getPopup() { + // This will be null until show() is called. + return mPopup.getPopup(); + } + }; + } + + return mDragListener; + } + + /** + * @return the {@link Menu} associated with this popup. Populate the returned Menu with + * items before calling {@link #show()}. + * @see #show() + * @see #getMenuInflater() + */ + public Menu getMenu() { + return mMenu; + } + + /** + * @return a {@link MenuInflater} that can be used to inflate menu items from XML into the + * menu returned by {@link #getMenu()}. + * @see #getMenu() + */ + public MenuInflater getMenuInflater() { + return new SupportMenuInflater(mContext); + } + + /** + * Inflate a menu resource into this PopupMenu. This is equivalent to calling + * popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu()). + * + * @param menuRes Menu resource to inflate + */ + public void inflate(@MenuRes int menuRes) { + getMenuInflater().inflate(menuRes, mMenu); + } + + /** + * Show the menu popup anchored to the view specified during construction. + * + * @see #dismiss() + */ + public void show() { + mPopup.show(); + } + + /** + * Dismiss the menu popup. + * + * @see #show() + */ + public void dismiss() { + mPopup.dismiss(); + } + + /** + * Set a listener that will be notified when the user selects an item from the menu. + * + * @param listener Listener to notify + */ + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mMenuItemClickListener = listener; + } + + /** + * Set a listener that will be notified when this menu is dismissed. + * + * @param listener Listener to notify + */ + public void setOnDismissListener(OnDismissListener listener) { + mDismissListener = listener; + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + if (mMenuItemClickListener != null) { + return mMenuItemClickListener.onMenuItemClick(item); + } + return false; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (mDismissListener != null) { + mDismissListener.onDismiss(this); + } + } + + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null) return false; + + if (!subMenu.hasVisibleItems()) { + return true; + } + + // Current menu will be dismissed by the normal helper, submenu will be shown in its place. + new MenuPopupHelper(mContext, subMenu, mAnchor).show(); + return true; + } + + public void onCloseSubMenu(SubMenuBuilder menu) { + } + + public void onMenuModeChange(MenuBuilder menu) { + } + + /** + * Callback interface used to notify the application that the menu has closed. + */ + public interface OnDismissListener { + /** + * Called when the associated menu has been dismissed. + * + * @param menu The PopupMenu that was dismissed. + */ + public void onDismiss(CustomPopupMenu menu); + } + + /** + * Interface responsible for receiving menu item click events if the items themselves + * do not have individual item click listeners. + */ + public interface OnMenuItemClickListener { + /** + * This method will be invoked when a menu item is clicked if the item itself did + * not already handle the event. + * + * @param item {@link MenuItem} that was clicked + * @return true if the event was handled, false otherwise. + */ + public boolean onMenuItemClick(MenuItem item); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenuHelper.java b/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenuHelper.java new file mode 100644 index 00000000..a53ea4b0 --- /dev/null +++ b/app/src/main/java/org/solovyev/android/widget/menu/CustomPopupMenuHelper.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2010 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 org.solovyev.android.widget.menu; + +import android.content.Context; +import android.content.res.Resources; +import android.os.Parcelable; +import android.support.v7.appcompat.R; +import android.support.v7.view.menu.*; +import android.support.v7.widget.ListPopupWindow; +import android.view.*; +import android.view.View.MeasureSpec; +import android.widget.*; + +import java.util.ArrayList; + +/** + * Presents a menu as a small, simple popup anchored to another view. + */ +public class CustomPopupMenuHelper implements AdapterView.OnItemClickListener, View.OnKeyListener, + ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener, + MenuPresenter { + + static final int ITEM_LAYOUT = R.layout.abc_popup_menu_item_layout; + + 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 ListPopupWindow 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; + + public CustomPopupMenuHelper(Context context, MenuBuilder menu) { + this(context, menu, null, false, R.attr.popupMenuStyle); + } + + public CustomPopupMenuHelper(Context context, MenuBuilder menu, View anchorView) { + this(context, menu, anchorView, false, R.attr.popupMenuStyle); + } + + public CustomPopupMenuHelper(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly, int popupStyleAttr) { + this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0); + } + + public CustomPopupMenuHelper(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly, int popupStyleAttr, int popupStyleRes) { + mContext = context; + mInflater = LayoutInflater.from(context); + mMenu = menu; + mAdapter = new MenuAdapter(mMenu); + mOverflowOnly = overflowOnly; + mPopupStyleAttr = popupStyleAttr; + mPopupStyleRes = popupStyleRes; + + final Resources res = context.getResources(); + mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, + res.getDimensionPixelSize(R.dimen.abc_config_prefDialogWidth)); + + mAnchorView = anchorView; + + // Present the menu using our context, not the menu builder's context. + menu.addMenuPresenter(this, context); + } + + public void setAnchorView(View anchor) { + mAnchorView = anchor; + } + + public void setForceShowIcon(boolean forceShow) { + mForceShowIcon = forceShow; + } + + public void setGravity(int gravity) { + mDropDownGravity = gravity; + } + + public int getGravity() { + return mDropDownGravity; + } + + public void show() { + if (!tryShow()) { + throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); + } + } + + public ListPopupWindow getPopup() { + return mPopup; + } + + public boolean tryShow() { + mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); + mPopup.setOnDismissListener(this); + mPopup.setOnItemClickListener(this); + mPopup.setAdapter(mAdapter); + mPopup.setModal(true); + + View anchor = mAnchorView; + if (anchor != null) { + final boolean addGlobalListener = mTreeObserver == null; + mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest + if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this); + mPopup.setAnchorView(anchor); + mPopup.setDropDownGravity(mDropDownGravity); + } else { + return false; + } + + if (!mHasContentWidth) { + mContentWidth = measureContentWidth(); + mHasContentWidth = true; + } + + mPopup.setContentWidth(mContentWidth); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopup.show(); + mPopup.getListView().setOnKeyListener(this); + return true; + } + + public void dismiss() { + if (isShowing()) { + mPopup.dismiss(); + } + } + + public void onDismiss() { + mPopup = null; + mMenu.close(); + if (mTreeObserver != null) { + if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver(); + mTreeObserver.removeGlobalOnLayoutListener(this); + mTreeObserver = null; + } + } + + public boolean isShowing() { + 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); + } + + 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()) { + final View anchor = mAnchorView; + if (anchor == null || !anchor.isShown()) { + dismiss(); + } else if (isShowing()) { + // Recompute window size and position + mPopup.show(); + } + } + } + + @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("MenuPopupHelpers manage their own views"); + } + + @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()) { + CustomPopupMenuHelper subPopup = new CustomPopupMenuHelper(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; + } + + @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; + } + + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + @Override + public int getId() { + return 0; + } + + @Override + public Parcelable onSaveInstanceState() { + return null; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + } + + private class MenuAdapter extends BaseAdapter { + private MenuBuilder mAdapterMenu; + private int mExpandedIndex = -1; + + public MenuAdapter(MenuBuilder menu) { + mAdapterMenu = menu; + findExpandedIndex(); + } + + public int getCount() { + ArrayList items = mOverflowOnly ? + mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); + if (mExpandedIndex < 0) { + return items.size(); + } + return items.size() - 1; + } + + public MenuItemImpl getItem(int position) { + ArrayList items = mOverflowOnly ? + mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); + if (mExpandedIndex >= 0 && position >= mExpandedIndex) { + position++; + } + return items.get(position); + } + + public long getItemId(int position) { + // Since a menu item's ID is optional, we'll use the position as an + // ID for the item in the AdapterView + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); + } + + MenuView.ItemView itemView = (MenuView.ItemView) convertView; + if (mForceShowIcon) { + ((ListMenuItemView) convertView).setForceShowIcon(true); + } + itemView.initialize(getItem(position), 0); + return convertView; + } + + void findExpandedIndex() { + final MenuItemImpl expandedItem = mMenu.getExpandedItem(); + if (expandedItem != null) { + final ArrayList items = mMenu.getNonActionItems(); + final int count = items.size(); + for (int i = 0; i < count; i++) { + final MenuItemImpl item = items.get(i); + if (item == expandedItem) { + mExpandedIndex = i; + return; + } + } + } + mExpandedIndex = -1; + } + + @Override + public void notifyDataSetChanged() { + findExpandedIndex(); + super.notifyDataSetChanged(); + } + } +} +