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