org.edx.mobile.view.custom.popup.menu.MenuPopupHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.edx.mobile.view.custom.popup.menu.MenuPopupHelper.java

Source

package org.edx.mobile.view.custom.popup.menu;

/*
 * This class is copied and modified according to our specifications from
 * the AOSP. It uses the appcompat implementation because it exposes it's
 * internal classes, so we only need to duplicate minimal code. We use a
 * custom attribute set to define the width and some other things, and a
 * custom adapter with it's own layout that automatically expands the first
 * level of submenus in order to support headers.
 *
 * 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.
 */

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.os.Build;
import android.os.Parcelable;
import android.support.annotation.LayoutRes;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.view.menu.MenuBuilder;
import android.support.v7.view.menu.MenuItemImpl;
import android.support.v7.view.menu.MenuPresenter;
import android.support.v7.view.menu.MenuView;
import android.support.v7.view.menu.SubMenuBuilder;
import android.support.v7.widget.ListPopupWindow;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Checkable;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;

import org.edx.mobile.R;

import java.util.List;

/**
 * Presents a menu as a small, simple popup anchored to another view.
 */
class MenuPopupHelper
        implements AdapterView.OnItemClickListener, View.OnKeyListener, 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 mPopupMinWidth;
    private final int mPopupMaxWidth;
    private final int mPopupPaddingLeft;
    private final int mPopupPaddingRight;
    private final int mPopupPaddingStart;
    private final int mPopupPaddingEnd;
    private final int mPopupPaddingTop;
    private final int mPopupPaddingBottom;
    private final int mPopupItemVerticalPadding;
    private final int mPopupIconPadding;
    private final int mPopupIconDefaultSize;
    private final int mPopupHeaderTextAppearance;
    private final int mPopupRowTextAppearance;
    private final int mPopupStyleAttr;
    private final int mPopupStyleRes;

    private View mAnchorView;
    private ListPopupWindow mPopup;
    private ViewTreeObserver mTreeObserver;
    private Callback mPresenterCallback;

    boolean mForceShowIcon;

    /**
     * Dummy ListView to use as parent when measuring rows, in order to
     * generate the row layout params and resolve layout direction
     * inheritance (and thereby the compound drawable alignment in
     * TextView).
     */
    private ListView mMeasureListView;

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

    /** A MeasureSpec with UNSPECIFIED mode. */
    private static final int UNSPECIFIED_MEASURE_SPEC = View.MeasureSpec.makeMeasureSpec(0,
            View.MeasureSpec.UNSPECIFIED);

    private int mDropDownGravity = Gravity.NO_GRAVITY;

    public MenuPopupHelper(Context context, MenuBuilder menu) {
        this(context, menu, null, false, android.R.attr.popupMenuStyle, 0);
    }

    public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView) {
        this(context, menu, anchorView, false, android.R.attr.popupMenuStyle, 0);
    }

    public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly,
            int popupStyleAttr) {
        this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
    }

    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();
        mOverflowOnly = overflowOnly;
        mPopupStyleAttr = popupStyleAttr;
        mPopupStyleRes = popupStyleRes;

        final Resources res = context.getResources();
        mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
                res.getDimensionPixelSize(android.support.v7.appcompat.R.dimen.abc_config_prefDialogWidth));

        TypedArray a = context.obtainStyledAttributes(null, R.styleable.PopupMenu, mPopupStyleAttr, mPopupStyleRes);
        mPopupMinWidth = a.getDimensionPixelSize(R.styleable.PopupMenu_android_minWidth, 0);
        int popupPadding = a.getDimensionPixelSize(R.styleable.PopupMenu_android_padding, -1);
        if (popupPadding >= 0) {
            mPopupPaddingLeft = popupPadding;
            mPopupPaddingRight = popupPadding;
            mPopupPaddingStart = popupPadding;
            mPopupPaddingEnd = popupPadding;
            mPopupPaddingTop = popupPadding;
            mPopupPaddingBottom = popupPadding;
        } else {
            int paddingStart = Integer.MIN_VALUE;
            int paddingEnd = Integer.MIN_VALUE;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                paddingStart = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingStart,
                        Integer.MIN_VALUE);
                paddingEnd = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingEnd, Integer.MIN_VALUE);
            }
            int paddingLeft = 0;
            int paddingRight = 0;
            if (paddingStart == Integer.MIN_VALUE && paddingEnd == Integer.MIN_VALUE) {
                paddingLeft = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingLeft, 0);
                paddingRight = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingRight, 0);
            } else {
                if (paddingStart == Integer.MIN_VALUE) {
                    paddingStart = 0;
                }
                if (paddingEnd == Integer.MIN_VALUE) {
                    paddingEnd = 0;
                }
            }
            mPopupPaddingLeft = paddingLeft;
            mPopupPaddingRight = paddingRight;
            mPopupPaddingStart = paddingStart;
            mPopupPaddingEnd = paddingEnd;
            mPopupPaddingTop = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingTop, 0);
            mPopupPaddingBottom = a.getDimensionPixelSize(R.styleable.PopupMenu_android_paddingBottom, 0);
        }
        mPopupItemVerticalPadding = a.getDimensionPixelSize(R.styleable.PopupMenu_itemVerticalPadding, 0);
        mPopupIconPadding = a.getDimensionPixelSize(R.styleable.PopupMenu_iconPadding, 0);
        mPopupIconDefaultSize = a.getDimensionPixelSize(R.styleable.PopupMenu_iconDefaultSize, 0);
        mPopupHeaderTextAppearance = a.getResourceId(R.styleable.PopupMenu_headerTextAppearance, -1);
        mPopupRowTextAppearance = a.getResourceId(R.styleable.PopupMenu_rowTextAppearance, -1);
        a.recycle();

        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);
            anchor.addOnAttachStateChangeListener(this);
            mPopup.setAnchorView(anchor);
            mPopup.setDropDownGravity(mDropDownGravity);
        } else {
            return false;
        }

        if (mContentWidth == Integer.MIN_VALUE) {
            mContentWidth = measureContentWidth();
        }
        mPopup.setContentWidth(mContentWidth);
        // Invert the horizontal offset in RTL mode.
        if (ViewCompat.getLayoutDirection(mAnchorView) == ViewCompat.LAYOUT_DIRECTION_RTL) {
            mPopup.setHorizontalOffset(-mPopup.getHorizontalOffset());
        }
        // Implement right gravity manually through horizontal offset pre-KitKat.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT
                && (Gravity.getAbsoluteGravity(mDropDownGravity, ViewCompat.getLayoutDirection(anchor))
                        & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.RIGHT) {
            mPopup.setHorizontalOffset(mPopup.getHorizontalOffset() + (mAnchorView.getWidth() - mPopup.getWidth()));
        }
        // If vertical offset is defined as 0, then ListPopupWindow infers
        // it as the negative of the top padding of the background, in
        // order to anchor the content area. Since that is not the effect
        // we want, we'll force it to use only the explicitly defined
        // offset by explicitly setting it dynamically as well, and thus
        // forcing it to discard it's 'unset' flag.
        mPopup.setVerticalOffset(mPopup.getVerticalOffset());
        // Top/bottom padding will be applied on the background drawable,
        // as the ListView is both initialized and set up only after show()
        // is called on the ListPopupWindow. Left/right padding will be
        // set up on the list items from the adapter, to keep the correct
        // item boundaries for the selector.
        ShapeDrawable paddedDrawable = new ShapeDrawable();
        paddedDrawable.setAlpha(0);
        // Don't apply top padding if the first item is a header, to
        // comply with the design.
        paddedDrawable.setPadding(0, mAdapter.hasHeader() ? 0 : (mPopupPaddingTop - mPopupItemVerticalPadding), 0,
                mPopupPaddingBottom - mPopupItemVerticalPadding);
        Drawable background = mPopup.getBackground();
        mPopup.setBackgroundDrawable(background == null ? paddedDrawable
                : new LayerDrawable(new Drawable[] { background, paddedDrawable }));
        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
        mPopup.show();
        mPopup.getListView().setOnKeyListener(this);
        return true;
    }

    public void dismiss() {
        if (isShowing()) {
            mPopup.dismiss();
        }
    }

    @Override
    public void onDismiss() {
        mPopup = null;
        mMenu.close();
        if (mTreeObserver != null) {
            if (!mTreeObserver.isAlive())
                mTreeObserver = mAnchorView.getViewTreeObserver();
            mTreeObserver.removeGlobalOnLayoutListener(this);
            mTreeObserver = null;
        }
        mAnchorView.removeOnAttachStateChangeListener(this);
    }

    public boolean isShowing() {
        return mPopup != null && mPopup.isShowing();
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mMenu.performItemAction(mAdapter.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 = mPopupMinWidth;
        final int count = mAdapter.getCount();
        if (count > 0) {
            View[] viewTypes = new View[mAdapter.getViewTypeCount()];
            if (mMeasureListView == null) {
                mMeasureListView = new ListView(mContext);
            }
            // The layout direction needs to be resolved in order for the row view
            // items to resolve layout direction inheritance, which is needed for
            // the TextView to resolve the icon compound drawable alignment (which
            // is a prerequisite for measuring it).
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                mMeasureListView.setLayoutDirection(mAnchorView.getLayoutDirection());
            }
            for (int i = 0; i < count; i++) {
                final int positionType = mAdapter.getItemViewType(i);
                View itemView = viewTypes[positionType];
                itemView = mAdapter.getView(i, itemView, mMeasureListView);
                // Add row to the ListView to resolve layout direction inheritance.
                mMeasureListView.addHeaderView(itemView);
                itemView.measure(UNSPECIFIED_MEASURE_SPEC, UNSPECIFIED_MEASURE_SPEC);
                viewTypes[positionType] = itemView;
                final int itemWidth = itemView.getMeasuredWidth();
                if (itemWidth >= mPopupMaxWidth) {
                    return mPopupMaxWidth;
                }
                if (itemWidth > maxWidth) {
                    maxWidth = itemWidth;
                }
            }
        }
        return maxWidth;
    }

    @Override
    public void onGlobalLayout() {
        if (isShowing()) {
            final View anchor = mAnchorView;
            if (anchor == null || !anchor.isShown()) {
                dismiss();
            } else {
                // Recompute window size and position
                mPopup.show();
            }
        }
    }

    @Override
    public void onViewAttachedToWindow(View v) {
    }

    @Override
    public void onViewDetachedFromWindow(View v) {
        if (mTreeObserver != null) {
            if (!mTreeObserver.isAlive())
                mTreeObserver = v.getViewTreeObserver();
            mTreeObserver.removeGlobalOnLayoutListener(this);
        }
        v.removeOnAttachStateChangeListener(this);
    }

    @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) {
        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);
            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 boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
        return false;
    }

    @Override
    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 enum ItemType {
        HEADER(R.layout.popup_menu_header, false), ITEM(R.layout.popup_menu_item, true);

        final int mLayoutRes;
        final boolean mIsEnabled;

        ItemType(@LayoutRes int layoutRes, boolean isEnabled) {
            mLayoutRes = layoutRes;
            mIsEnabled = isEnabled;
        }
    }

    private class MenuAdapter extends BaseAdapter {
        boolean hasHeader() {
            return getCount() > 0 && getItemType(0) == ItemType.HEADER;
        }

        private List<? extends MenuItem> getMenuItems() {
            return mOverflowOnly ? mMenu.getNonActionItems() : mMenu.getVisibleItems();
        }

        @Override
        public int getCount() {
            int count = 0;
            MenuItem expandedItem = mMenu.getExpandedItem();
            for (MenuItem item : getMenuItems()) {
                if (item != expandedItem) {
                    count++;
                    MenuBuilder subMenu = (MenuBuilder) item.getSubMenu();
                    if (subMenu != null) {
                        count += subMenu.size();
                        // Since the menu structure can be set up at any point,
                        // this is the only place where the presenter can be set
                        // up for the submenus. There is no method to query
                        // whether the presenter has already been set up, so it
                        // will be removed and added each time.
                        subMenu.removeMenuPresenter(MenuPopupHelper.this);
                        subMenu.addMenuPresenter(MenuPopupHelper.this, mContext);
                    }
                }
            }
            return count;
        }

        @Override
        public MenuItem getItem(int position) {
            int count = getCount();
            if (position < 0 || position >= count) {
                throw new IndexOutOfBoundsException();
            }
            int index = 0;
            MenuItem expandedItem = mMenu.getExpandedItem();
            for (MenuItem item : getMenuItems()) {
                if (item != expandedItem) {
                    if (index++ == position)
                        return item;
                    Menu subMenu = item.getSubMenu();
                    if (subMenu != null) {
                        int subMenuCount = subMenu.size();
                        if (position < index + subMenuCount) {
                            return subMenu.getItem(position - index);
                        }
                        index += subMenuCount;
                    }
                }
            }
            throw new IllegalStateException();
        }

        @Override
        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;
        }

        @Override
        public int getViewTypeCount() {
            return 2;
        }

        @Override
        public int getItemViewType(int position) {
            return getItemType(position).ordinal();
        }

        private ItemType getItemType(int position) {
            int count = getCount();
            if (position < 0 || position >= count) {
                throw new IndexOutOfBoundsException();
            }
            int index = 0;
            MenuItem expandedItem = mMenu.getExpandedItem();
            for (MenuItem item : getMenuItems()) {
                if (item != expandedItem) {
                    Menu subMenu = item.getSubMenu();
                    if (index++ == position) {
                        return subMenu == null ? ItemType.ITEM : ItemType.HEADER;
                    }
                    if (subMenu != null) {
                        int subMenuCount = subMenu.size();
                        if (position < index + subMenuCount) {
                            return ItemType.ITEM;
                        }
                        index += subMenuCount;
                    }
                }
            }
            throw new IllegalStateException();
        }

        @Override
        public boolean areAllItemsEnabled() {
            return false;
        }

        @Override
        public boolean isEnabled(int position) {
            return getItemType(position).mIsEnabled;
        }

        @Override
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        public View getView(int position, View convertView, ViewGroup parent) {
            TextView textView;
            ItemType itemType = getItemType(position);
            if (convertView != null) {
                textView = (TextView) convertView;
            } else {
                textView = (TextView) mInflater.inflate(itemType.mLayoutRes, parent, false);
            }
            MenuItem item = getItem(position);
            textView.setText(item.getTitle());
            switch (itemType) {
            case HEADER:
                textView.setTextAppearance(mContext, mPopupHeaderTextAppearance);
                break;
            case ITEM:
                textView.setTextAppearance(mContext, mPopupRowTextAppearance);
                break;
            default:
                throw new IllegalStateException();
            }
            if (textView instanceof Checkable) {
                ((Checkable) textView).setChecked(item.isChecked());
            }
            Drawable icon = item.getIcon();
            if (icon != null) {
                textView.setCompoundDrawablePadding(mPopupIconPadding);
                int iconWidth = icon.getIntrinsicWidth();
                int iconHeight = icon.getIntrinsicHeight();
                if (iconWidth < 0 || iconHeight < 0) {
                    iconWidth = iconHeight = mPopupIconDefaultSize;
                }
                icon.setBounds(0, 0, iconWidth, iconHeight);
                TextViewCompat.setCompoundDrawablesRelative(textView, icon, null, null, null);
            }
            if (mPopupPaddingStart == Integer.MIN_VALUE && mPopupPaddingEnd == Integer.MIN_VALUE) {
                textView.setPadding(mPopupPaddingLeft, mPopupItemVerticalPadding, mPopupPaddingRight,
                        mPopupItemVerticalPadding);
            } else {
                textView.setPaddingRelative(mPopupPaddingStart, mPopupItemVerticalPadding, mPopupPaddingEnd,
                        mPopupItemVerticalPadding);
            }
            return textView;
        }
    }
}