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