org.solovyev.android.widget.menu.CustomPopupMenuHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.solovyev.android.widget.menu.CustomPopupMenuHelper.java

Source

/*
 * 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.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ActionProvider;
import android.support.v4.view.MenuItemCompat;
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 org.solovyev.android.Check;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

/**
 * Presents a menu as a small, simple popup anchored to another view.
 */
@SuppressWarnings("unused")
@SuppressLint("PrivateResource")
public class CustomPopupMenuHelper implements AdapterView.OnItemClickListener, View.OnKeyListener,
        ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener, MenuPresenter {

    private static final int DEFAULT_VIEW_TAG_KEY = org.solovyev.android.calculator.R.id.cpm_default_view_tag_key;
    private static final int[] COLOR_ATTRS = new int[] { R.attr.colorControlNormal };
    private static final Object DEFAULT_VIEW_TAG = new Object();

    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;
    boolean mForceShowIcon;
    private View mAnchorView;
    private ListPopupWindow mPopup;
    private ViewTreeObserver mTreeObserver;
    private Callback mPresenterCallback;
    private ViewGroup mMeasureParent;
    private boolean mKeepOnSubMenu;

    /**
     * Whether the cached content width value is valid.
     */
    private boolean mHasContentWidth;

    /**
     * Cached content width from {@link #measureContentWidth}.
     */
    private int mContentWidth;

    private int mGravity = 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);
    }

    static void tintMenuItem(@Nonnull MenuItemImpl item, @Nonnull ColorStateList tintColorStateList) {
        Drawable icon = item.getIcon();
        if (icon != null) {
            icon = DrawableCompat.wrap(icon);
            DrawableCompat.setTintList(icon, tintColorStateList);
            item.setIcon(icon);
        }
        if (item.hasSubMenu()) {
            final SubMenu subMenu = item.getSubMenu();
            for (int i = 0; i < subMenu.size(); i++) {
                final MenuItem subItem = subMenu.getItem(i);
                if (subItem instanceof MenuItemImpl) {
                    tintMenuItem((MenuItemImpl) subItem, tintColorStateList);
                }
            }
        }
    }

    @Nullable
    static ColorStateList getTintColorStateList(Context context) {
        final TypedArray a = context.obtainStyledAttributes(null, COLOR_ATTRS);
        try {
            return a.getColorStateList(0);
        } finally {
            a.recycle();
        }
    }

    static void tintMenuItems(Context context, Menu menu, int from, int to) {
        final ColorStateList tintColorStateList = getTintColorStateList(context);
        if (tintColorStateList == null) {
            return;
        }
        for (int i = from; i < to; i++) {
            final MenuItem item = menu.getItem(i);
            if (item instanceof MenuItemImpl) {
                tintMenuItem((MenuItemImpl) item, tintColorStateList);
            }
        }
    }

    public void setAnchorView(View anchor) {
        mAnchorView = anchor;
    }

    public void setForceShowIcon(boolean forceShow) {
        mForceShowIcon = forceShow;
    }

    public int getGravity() {
        return mGravity;
    }

    public void setGravity(int gravity) {
        mGravity = gravity;
    }

    public boolean isKeepOnSubMenu() {
        return mKeepOnSubMenu;
    }

    public void setKeepOnSubMenu(boolean keepOnSubMenu) {
        mKeepOnSubMenu = keepOnSubMenu;
    }

    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(mGravity);
        } 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();
            //noinspection deprecation
            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()) {
            return;
        }
        final View anchor = mAnchorView;
        if (anchor == null || !anchor.isShown()) {
            dismiss();
        } else {
            // 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, false,
                    mPopupStyleAttr, mPopupStyleRes);
            subPopup.setGravity(mGravity);
            subPopup.setCallback(mPresenterCallback);
            subPopup.setKeepOnSubMenu(mKeepOnSubMenu);

            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;

        if (isKeepOnSubMenu() && !allMenusAreClosing) {
            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<MenuItemImpl> items = mOverflowOnly ? mAdapterMenu.getNonActionItems()
                    : mAdapterMenu.getVisibleItems();
            if (mExpandedIndex < 0) {
                return items.size();
            }
            return items.size() - 1;
        }

        public MenuItemImpl getItem(int position) {
            ArrayList<MenuItemImpl> 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) {
            final MenuItemImpl item = getItem(position);
            final ActionProvider actionProvider = MenuItemCompat.getActionProvider(item);
            if (actionProvider != null) {
                final View actionView = actionProvider.onCreateActionView(item);
                fixLayoutParams(actionView, parent);
                return actionView;
            }
            final View actionView = MenuItemCompat.getActionView(item);
            if (actionView != null) {
                ((MenuView.ItemView) actionView).initialize(item, 0);
                fixLayoutParams(actionView, parent);
                return actionView;
            }
            return getDefaultView(item, convertView, parent);
        }

        private void fixLayoutParams(View actionView, ViewGroup parent) {
            if (parent instanceof FrameLayout) {
                // width measure pass, nothing to be done
                return;
            }
            Check.isTrue(parent instanceof AbsListView);
            final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
            if (lp != null && !(lp instanceof AbsListView.LayoutParams)) {
                // see android.widget.AbsListView.generateDefaultLayoutParams()
                actionView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT));
            }
        }

        @Nonnull
        private View getDefaultView(MenuItemImpl item, View convertView, ViewGroup parent) {
            if (convertView == null || convertView.getTag(DEFAULT_VIEW_TAG_KEY) != DEFAULT_VIEW_TAG) {
                convertView = mInflater.inflate(R.layout.abc_popup_menu_item_layout, parent, false);
                convertView.setTag(DEFAULT_VIEW_TAG_KEY, DEFAULT_VIEW_TAG);
            }

            final MenuView.ItemView itemView = (MenuView.ItemView) convertView;
            if (mForceShowIcon) {
                final ListMenuItemView listItemView = (ListMenuItemView) convertView;
                final boolean preserveIconSpacing = ListMenuItemViewCompat.getPreserveIconSpacing(listItemView);
                listItemView.setForceShowIcon(true);
                ListMenuItemViewCompat.setPreserveIconSpacing(listItemView, preserveIconSpacing);
            }
            itemView.initialize(item, 0);
            return convertView;
        }

        void findExpandedIndex() {
            final MenuItemImpl expandedItem = mMenu.getExpandedItem();
            if (expandedItem != null) {
                final ArrayList<MenuItemImpl> 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();
        }

        public int indexOf(MenuItem item) {
            final List<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
            for (int i = 0; i < visibleItems.size(); i++) {
                MenuItemImpl candidate = visibleItems.get(i);
                if (candidate == item) {
                    return i;
                }
            }
            return -1;
        }
    }
}