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