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