MenuPopupHelper.java revision 560f17098f90b15c8894cce127f2fed85f7aeb6b
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 17package com.android.internal.view.menu; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.os.Parcelable; 22import android.view.Gravity; 23import android.view.KeyEvent; 24import android.view.LayoutInflater; 25import android.view.MenuItem; 26import android.view.View; 27import android.view.View.MeasureSpec; 28import android.view.ViewGroup; 29import android.view.ViewTreeObserver; 30import android.widget.AdapterView; 31import android.widget.BaseAdapter; 32import android.widget.FrameLayout; 33import android.widget.ListAdapter; 34import android.widget.ListPopupWindow; 35import android.widget.PopupWindow; 36 37import java.util.ArrayList; 38 39/** 40 * Presents a menu as a small, simple popup anchored to another view. 41 * @hide 42 */ 43public 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 menu.addMenuPresenter(this); 99 } 100 101 public void setAnchorView(View anchor) { 102 mAnchorView = anchor; 103 } 104 105 public void setForceShowIcon(boolean forceShow) { 106 mForceShowIcon = forceShow; 107 } 108 109 public void setGravity(int gravity) { 110 mDropDownGravity = gravity; 111 } 112 113 public void show() { 114 if (!tryShow()) { 115 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor"); 116 } 117 } 118 119 public ListPopupWindow getPopup() { 120 return mPopup; 121 } 122 123 public boolean tryShow() { 124 mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr); 125 mPopup.setOnDismissListener(this); 126 mPopup.setOnItemClickListener(this); 127 mPopup.setAdapter(mAdapter); 128 mPopup.setModal(true); 129 130 View anchor = mAnchorView; 131 if (anchor != null) { 132 final boolean addGlobalListener = mTreeObserver == null; 133 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest 134 if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this); 135 anchor.addOnAttachStateChangeListener(this); 136 mPopup.setAnchorView(anchor); 137 mPopup.setDropDownGravity(mDropDownGravity); 138 } else { 139 return false; 140 } 141 142 if (!mHasContentWidth) { 143 mContentWidth = measureContentWidth(); 144 mHasContentWidth = true; 145 } 146 147 mPopup.setContentWidth(mContentWidth); 148 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 149 mPopup.show(); 150 mPopup.getListView().setOnKeyListener(this); 151 return true; 152 } 153 154 public void dismiss() { 155 if (isShowing()) { 156 mPopup.dismiss(); 157 } 158 } 159 160 public void onDismiss() { 161 mPopup = null; 162 mMenu.close(); 163 if (mTreeObserver != null) { 164 if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver(); 165 mTreeObserver.removeGlobalOnLayoutListener(this); 166 mTreeObserver = null; 167 } 168 mAnchorView.removeOnAttachStateChangeListener(this); 169 } 170 171 public boolean isShowing() { 172 return mPopup != null && mPopup.isShowing(); 173 } 174 175 @Override 176 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 177 MenuAdapter adapter = mAdapter; 178 adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0); 179 } 180 181 public boolean onKey(View v, int keyCode, KeyEvent event) { 182 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { 183 dismiss(); 184 return true; 185 } 186 return false; 187 } 188 189 private int measureContentWidth() { 190 // Menus don't tend to be long, so this is more sane than it looks. 191 int maxWidth = 0; 192 View itemView = null; 193 int itemType = 0; 194 195 final ListAdapter adapter = mAdapter; 196 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 197 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 198 final int count = adapter.getCount(); 199 for (int i = 0; i < count; i++) { 200 final int positionType = adapter.getItemViewType(i); 201 if (positionType != itemType) { 202 itemType = positionType; 203 itemView = null; 204 } 205 206 if (mMeasureParent == null) { 207 mMeasureParent = new FrameLayout(mContext); 208 } 209 210 itemView = adapter.getView(i, itemView, mMeasureParent); 211 itemView.measure(widthMeasureSpec, heightMeasureSpec); 212 213 final int itemWidth = itemView.getMeasuredWidth(); 214 if (itemWidth >= mPopupMaxWidth) { 215 return mPopupMaxWidth; 216 } else if (itemWidth > maxWidth) { 217 maxWidth = itemWidth; 218 } 219 } 220 221 return maxWidth; 222 } 223 224 @Override 225 public void onGlobalLayout() { 226 if (isShowing()) { 227 final View anchor = mAnchorView; 228 if (anchor == null || !anchor.isShown()) { 229 dismiss(); 230 } else if (isShowing()) { 231 // Recompute window size and position 232 mPopup.show(); 233 } 234 } 235 } 236 237 @Override 238 public void onViewAttachedToWindow(View v) { 239 } 240 241 @Override 242 public void onViewDetachedFromWindow(View v) { 243 if (mTreeObserver != null) { 244 if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); 245 mTreeObserver.removeGlobalOnLayoutListener(this); 246 } 247 v.removeOnAttachStateChangeListener(this); 248 } 249 250 @Override 251 public void initForMenu(Context context, MenuBuilder menu) { 252 // Don't need to do anything; we added as a presenter in the constructor. 253 } 254 255 @Override 256 public MenuView getMenuView(ViewGroup root) { 257 throw new UnsupportedOperationException("MenuPopupHelpers manage their own views"); 258 } 259 260 @Override 261 public void updateMenuView(boolean cleared) { 262 mHasContentWidth = false; 263 264 if (mAdapter != null) { 265 mAdapter.notifyDataSetChanged(); 266 } 267 } 268 269 @Override 270 public void setCallback(Callback cb) { 271 mPresenterCallback = cb; 272 } 273 274 @Override 275 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 276 if (subMenu.hasVisibleItems()) { 277 MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView); 278 subPopup.setCallback(mPresenterCallback); 279 280 boolean preserveIconSpacing = false; 281 final int count = subMenu.size(); 282 for (int i = 0; i < count; i++) { 283 MenuItem childItem = subMenu.getItem(i); 284 if (childItem.isVisible() && childItem.getIcon() != null) { 285 preserveIconSpacing = true; 286 break; 287 } 288 } 289 subPopup.setForceShowIcon(preserveIconSpacing); 290 291 if (subPopup.tryShow()) { 292 if (mPresenterCallback != null) { 293 mPresenterCallback.onOpenSubMenu(subMenu); 294 } 295 return true; 296 } 297 } 298 return false; 299 } 300 301 @Override 302 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 303 // Only care about the (sub)menu we're presenting. 304 if (menu != mMenu) return; 305 306 dismiss(); 307 if (mPresenterCallback != null) { 308 mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); 309 } 310 } 311 312 @Override 313 public boolean flagActionItems() { 314 return false; 315 } 316 317 public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { 318 return false; 319 } 320 321 public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { 322 return false; 323 } 324 325 @Override 326 public int getId() { 327 return 0; 328 } 329 330 @Override 331 public Parcelable onSaveInstanceState() { 332 return null; 333 } 334 335 @Override 336 public void onRestoreInstanceState(Parcelable state) { 337 } 338 339 private class MenuAdapter extends BaseAdapter { 340 private MenuBuilder mAdapterMenu; 341 private int mExpandedIndex = -1; 342 343 public MenuAdapter(MenuBuilder menu) { 344 mAdapterMenu = menu; 345 findExpandedIndex(); 346 } 347 348 public int getCount() { 349 ArrayList<MenuItemImpl> items = mOverflowOnly ? 350 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); 351 if (mExpandedIndex < 0) { 352 return items.size(); 353 } 354 return items.size() - 1; 355 } 356 357 public MenuItemImpl getItem(int position) { 358 ArrayList<MenuItemImpl> items = mOverflowOnly ? 359 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); 360 if (mExpandedIndex >= 0 && position >= mExpandedIndex) { 361 position++; 362 } 363 return items.get(position); 364 } 365 366 public long getItemId(int position) { 367 // Since a menu item's ID is optional, we'll use the position as an 368 // ID for the item in the AdapterView 369 return position; 370 } 371 372 public View getView(int position, View convertView, ViewGroup parent) { 373 if (convertView == null) { 374 convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); 375 } 376 377 MenuView.ItemView itemView = (MenuView.ItemView) convertView; 378 if (mForceShowIcon) { 379 ((ListMenuItemView) convertView).setForceShowIcon(true); 380 } 381 itemView.initialize(getItem(position), 0); 382 return convertView; 383 } 384 385 void findExpandedIndex() { 386 final MenuItemImpl expandedItem = mMenu.getExpandedItem(); 387 if (expandedItem != null) { 388 final ArrayList<MenuItemImpl> items = mMenu.getNonActionItems(); 389 final int count = items.size(); 390 for (int i = 0; i < count; i++) { 391 final MenuItemImpl item = items.get(i); 392 if (item == expandedItem) { 393 mExpandedIndex = i; 394 return; 395 } 396 } 397 } 398 mExpandedIndex = -1; 399 } 400 401 @Override 402 public void notifyDataSetChanged() { 403 findExpandedIndex(); 404 super.notifyDataSetChanged(); 405 } 406 } 407} 408