Home | History | Annotate | Download | only in menu
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.view.menu;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.os.Parcelable;
     22 import android.view.Gravity;
     23 import android.view.KeyEvent;
     24 import android.view.LayoutInflater;
     25 import android.view.MenuItem;
     26 import android.view.View;
     27 import android.view.View.MeasureSpec;
     28 import android.view.ViewGroup;
     29 import android.view.ViewTreeObserver;
     30 import android.widget.AdapterView;
     31 import android.widget.BaseAdapter;
     32 import android.widget.FrameLayout;
     33 import android.widget.ListAdapter;
     34 import android.widget.ListPopupWindow;
     35 import android.widget.PopupWindow;
     36 
     37 import java.util.ArrayList;
     38 
     39 /**
     40  * Presents a menu as a small, simple popup anchored to another view.
     41  * @hide
     42  */
     43 public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener,
     44         ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener,
     45         View.OnAttachStateChangeListener, MenuPresenter {
     46     private static final String TAG = "MenuPopupHelper";
     47 
     48     static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;
     49 
     50     private final Context mContext;
     51     private final LayoutInflater mInflater;
     52     private final MenuBuilder mMenu;
     53     private final MenuAdapter mAdapter;
     54     private final boolean mOverflowOnly;
     55     private final int mPopupMaxWidth;
     56     private final int mPopupStyleAttr;
     57 
     58     private View mAnchorView;
     59     private ListPopupWindow mPopup;
     60     private ViewTreeObserver mTreeObserver;
     61     private Callback mPresenterCallback;
     62 
     63     boolean mForceShowIcon;
     64 
     65     private ViewGroup mMeasureParent;
     66 
     67     /** Whether the cached content width value is valid. */
     68     private boolean mHasContentWidth;
     69 
     70     /** Cached content width from {@link #measureContentWidth}. */
     71     private int mContentWidth;
     72 
     73     private int mDropDownGravity = Gravity.NO_GRAVITY;
     74 
     75     public MenuPopupHelper(Context context, MenuBuilder menu) {
     76         this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle);
     77     }
     78 
     79     public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView) {
     80         this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle);
     81     }
     82 
     83     public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView,
     84             boolean overflowOnly, int popupStyleAttr) {
     85         mContext = context;
     86         mInflater = LayoutInflater.from(context);
     87         mMenu = menu;
     88         mAdapter = new MenuAdapter(mMenu);
     89         mOverflowOnly = overflowOnly;
     90         mPopupStyleAttr = popupStyleAttr;
     91 
     92         final Resources res = context.getResources();
     93         mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
     94                 res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
     95 
     96         mAnchorView = anchorView;
     97 
     98         // Present the menu using our context, not the menu builder's context.
     99         menu.addMenuPresenter(this, context);
    100     }
    101 
    102     public void setAnchorView(View anchor) {
    103         mAnchorView = anchor;
    104     }
    105 
    106     public void setForceShowIcon(boolean forceShow) {
    107         mForceShowIcon = forceShow;
    108     }
    109 
    110     public void setGravity(int gravity) {
    111         mDropDownGravity = gravity;
    112     }
    113 
    114     public void show() {
    115         if (!tryShow()) {
    116             throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
    117         }
    118     }
    119 
    120     public ListPopupWindow getPopup() {
    121         return mPopup;
    122     }
    123 
    124     public boolean tryShow() {
    125         mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr);
    126         mPopup.setOnDismissListener(this);
    127         mPopup.setOnItemClickListener(this);
    128         mPopup.setAdapter(mAdapter);
    129         mPopup.setModal(true);
    130 
    131         View anchor = mAnchorView;
    132         if (anchor != null) {
    133             final boolean addGlobalListener = mTreeObserver == null;
    134             mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
    135             if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
    136             anchor.addOnAttachStateChangeListener(this);
    137             mPopup.setAnchorView(anchor);
    138             mPopup.setDropDownGravity(mDropDownGravity);
    139         } else {
    140             return false;
    141         }
    142 
    143         if (!mHasContentWidth) {
    144             mContentWidth = measureContentWidth();
    145             mHasContentWidth = true;
    146         }
    147 
    148         mPopup.setContentWidth(mContentWidth);
    149         mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
    150         mPopup.show();
    151         mPopup.getListView().setOnKeyListener(this);
    152         return true;
    153     }
    154 
    155     public void dismiss() {
    156         if (isShowing()) {
    157             mPopup.dismiss();
    158         }
    159     }
    160 
    161     public void onDismiss() {
    162         mPopup = null;
    163         mMenu.close();
    164         if (mTreeObserver != null) {
    165             if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
    166             mTreeObserver.removeGlobalOnLayoutListener(this);
    167             mTreeObserver = null;
    168         }
    169         mAnchorView.removeOnAttachStateChangeListener(this);
    170     }
    171 
    172     public boolean isShowing() {
    173         return mPopup != null && mPopup.isShowing();
    174     }
    175 
    176     @Override
    177     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    178         MenuAdapter adapter = mAdapter;
    179         adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
    180     }
    181 
    182     public boolean onKey(View v, int keyCode, KeyEvent event) {
    183         if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
    184             dismiss();
    185             return true;
    186         }
    187         return false;
    188     }
    189 
    190     private int measureContentWidth() {
    191         // Menus don't tend to be long, so this is more sane than it looks.
    192         int maxWidth = 0;
    193         View itemView = null;
    194         int itemType = 0;
    195 
    196         final ListAdapter adapter = mAdapter;
    197         final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    198         final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    199         final int count = adapter.getCount();
    200         for (int i = 0; i < count; i++) {
    201             final int positionType = adapter.getItemViewType(i);
    202             if (positionType != itemType) {
    203                 itemType = positionType;
    204                 itemView = null;
    205             }
    206 
    207             if (mMeasureParent == null) {
    208                 mMeasureParent = new FrameLayout(mContext);
    209             }
    210 
    211             itemView = adapter.getView(i, itemView, mMeasureParent);
    212             itemView.measure(widthMeasureSpec, heightMeasureSpec);
    213 
    214             final int itemWidth = itemView.getMeasuredWidth();
    215             if (itemWidth >= mPopupMaxWidth) {
    216                 return mPopupMaxWidth;
    217             } else if (itemWidth > maxWidth) {
    218                 maxWidth = itemWidth;
    219             }
    220         }
    221 
    222         return maxWidth;
    223     }
    224 
    225     @Override
    226     public void onGlobalLayout() {
    227         if (isShowing()) {
    228             final View anchor = mAnchorView;
    229             if (anchor == null || !anchor.isShown()) {
    230                 dismiss();
    231             } else if (isShowing()) {
    232                 // Recompute window size and position
    233                 mPopup.show();
    234             }
    235         }
    236     }
    237 
    238     @Override
    239     public void onViewAttachedToWindow(View v) {
    240     }
    241 
    242     @Override
    243     public void onViewDetachedFromWindow(View v) {
    244         if (mTreeObserver != null) {
    245             if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
    246             mTreeObserver.removeGlobalOnLayoutListener(this);
    247         }
    248         v.removeOnAttachStateChangeListener(this);
    249     }
    250 
    251     @Override
    252     public void initForMenu(Context context, MenuBuilder menu) {
    253         // Don't need to do anything; we added as a presenter in the constructor.
    254     }
    255 
    256     @Override
    257     public MenuView getMenuView(ViewGroup root) {
    258         throw new UnsupportedOperationException("MenuPopupHelpers manage their own views");
    259     }
    260 
    261     @Override
    262     public void updateMenuView(boolean cleared) {
    263         mHasContentWidth = false;
    264 
    265         if (mAdapter != null) {
    266             mAdapter.notifyDataSetChanged();
    267         }
    268     }
    269 
    270     @Override
    271     public void setCallback(Callback cb) {
    272         mPresenterCallback = cb;
    273     }
    274 
    275     @Override
    276     public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
    277         if (subMenu.hasVisibleItems()) {
    278             MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView);
    279             subPopup.setCallback(mPresenterCallback);
    280 
    281             boolean preserveIconSpacing = false;
    282             final int count = subMenu.size();
    283             for (int i = 0; i < count; i++) {
    284                 MenuItem childItem = subMenu.getItem(i);
    285                 if (childItem.isVisible() && childItem.getIcon() != null) {
    286                     preserveIconSpacing = true;
    287                     break;
    288                 }
    289             }
    290             subPopup.setForceShowIcon(preserveIconSpacing);
    291 
    292             if (subPopup.tryShow()) {
    293                 if (mPresenterCallback != null) {
    294                     mPresenterCallback.onOpenSubMenu(subMenu);
    295                 }
    296                 return true;
    297             }
    298         }
    299         return false;
    300     }
    301 
    302     @Override
    303     public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
    304         // Only care about the (sub)menu we're presenting.
    305         if (menu != mMenu) return;
    306 
    307         dismiss();
    308         if (mPresenterCallback != null) {
    309             mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
    310         }
    311     }
    312 
    313     @Override
    314     public boolean flagActionItems() {
    315         return false;
    316     }
    317 
    318     public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
    319         return false;
    320     }
    321 
    322     public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
    323         return false;
    324     }
    325 
    326     @Override
    327     public int getId() {
    328         return 0;
    329     }
    330 
    331     @Override
    332     public Parcelable onSaveInstanceState() {
    333         return null;
    334     }
    335 
    336     @Override
    337     public void onRestoreInstanceState(Parcelable state) {
    338     }
    339 
    340     private class MenuAdapter extends BaseAdapter {
    341         private MenuBuilder mAdapterMenu;
    342         private int mExpandedIndex = -1;
    343 
    344         public MenuAdapter(MenuBuilder menu) {
    345             mAdapterMenu = menu;
    346             findExpandedIndex();
    347         }
    348 
    349         public int getCount() {
    350             ArrayList<MenuItemImpl> items = mOverflowOnly ?
    351                     mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
    352             if (mExpandedIndex < 0) {
    353                 return items.size();
    354             }
    355             return items.size() - 1;
    356         }
    357 
    358         public MenuItemImpl getItem(int position) {
    359             ArrayList<MenuItemImpl> items = mOverflowOnly ?
    360                     mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
    361             if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
    362                 position++;
    363             }
    364             return items.get(position);
    365         }
    366 
    367         public long getItemId(int position) {
    368             // Since a menu item's ID is optional, we'll use the position as an
    369             // ID for the item in the AdapterView
    370             return position;
    371         }
    372 
    373         public View getView(int position, View convertView, ViewGroup parent) {
    374             if (convertView == null) {
    375                 convertView = mInflater.inflate(ITEM_LAYOUT, parent, false);
    376             }
    377 
    378             MenuView.ItemView itemView = (MenuView.ItemView) convertView;
    379             if (mForceShowIcon) {
    380                 ((ListMenuItemView) convertView).setForceShowIcon(true);
    381             }
    382             itemView.initialize(getItem(position), 0);
    383             return convertView;
    384         }
    385 
    386         void findExpandedIndex() {
    387             final MenuItemImpl expandedItem = mMenu.getExpandedItem();
    388             if (expandedItem != null) {
    389                 final ArrayList<MenuItemImpl> items = mMenu.getNonActionItems();
    390                 final int count = items.size();
    391                 for (int i = 0; i < count; i++) {
    392                     final MenuItemImpl item = items.get(i);
    393                     if (item == expandedItem) {
    394                         mExpandedIndex = i;
    395                         return;
    396                     }
    397                 }
    398             }
    399             mExpandedIndex = -1;
    400         }
    401 
    402         @Override
    403         public void notifyDataSetChanged() {
    404             findExpandedIndex();
    405             super.notifyDataSetChanged();
    406         }
    407     }
    408 }
    409