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