blob: d664058c351bf3dae53c225fee3cdb8511c73a8a [file] [log] [blame]
Adam Powell42675342010-07-09 18:02:59 -07001/*
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
Adam Powell42675342010-07-09 18:02:59 -070019import android.content.Context;
Adam Powell38639b12011-06-17 16:02:32 -070020import android.content.res.Resources;
Adam Powell11ed1d62011-07-11 21:19:59 -070021import android.os.Parcelable;
Adam Powell54c94de2013-09-26 15:36:34 -070022import android.view.Gravity;
Adam Powell8028dd32010-07-15 10:16:33 -070023import android.view.KeyEvent;
Adam Powell696cba52011-03-29 10:38:16 -070024import android.view.LayoutInflater;
Adam Powell91511032011-07-13 10:24:06 -070025import android.view.MenuItem;
Adam Powell42675342010-07-09 18:02:59 -070026import android.view.View;
27import android.view.View.MeasureSpec;
Adam Powell696cba52011-03-29 10:38:16 -070028import android.view.ViewGroup;
Adam Powell04587962010-11-11 22:15:07 -080029import android.view.ViewTreeObserver;
Adam Powell42675342010-07-09 18:02:59 -070030import android.widget.AdapterView;
Adam Powell696cba52011-03-29 10:38:16 -070031import android.widget.BaseAdapter;
Adam Powell91511032011-07-13 10:24:06 -070032import android.widget.FrameLayout;
Adam Powell696cba52011-03-29 10:38:16 -070033import android.widget.ListAdapter;
Adam Powell42675342010-07-09 18:02:59 -070034import android.widget.ListPopupWindow;
Adam Powell6c6f5752010-08-20 18:34:46 -070035import android.widget.PopupWindow;
Adam Powell42675342010-07-09 18:02:59 -070036
Adam Powell696cba52011-03-29 10:38:16 -070037import java.util.ArrayList;
Adam Powell8028dd32010-07-15 10:16:33 -070038
Adam Powell42675342010-07-09 18:02:59 -070039/**
Adam Powell696cba52011-03-29 10:38:16 -070040 * Presents a menu as a small, simple popup anchored to another view.
Adam Powell42675342010-07-09 18:02:59 -070041 * @hide
42 */
Adam Powell04587962010-11-11 22:15:07 -080043public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener,
Adam Powell4afd62b2011-02-18 15:02:18 -080044 ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener,
Adam Powell696cba52011-03-29 10:38:16 -070045 View.OnAttachStateChangeListener, MenuPresenter {
Adam Powell42675342010-07-09 18:02:59 -070046 private static final String TAG = "MenuPopupHelper";
47
Adam Powell696cba52011-03-29 10:38:16 -070048 static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;
49
Alan Viverette0bce6ab2013-06-26 17:46:16 -070050 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
Adam Powell4afd62b2011-02-18 15:02:18 -080057 private View mAnchorView;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070058 private ListPopupWindow mPopup;
Adam Powell04587962010-11-11 22:15:07 -080059 private ViewTreeObserver mTreeObserver;
Adam Powell696cba52011-03-29 10:38:16 -070060 private Callback mPresenterCallback;
Adam Powelle2b03a62011-01-25 14:39:02 -080061
Adam Powell91511032011-07-13 10:24:06 -070062 boolean mForceShowIcon;
63
64 private ViewGroup mMeasureParent;
65
Alan Viverette0bce6ab2013-06-26 17:46:16 -070066 /** 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
Adam Powell54c94de2013-09-26 15:36:34 -070072 private int mDropDownGravity = Gravity.NO_GRAVITY;
73
Adam Powell8028dd32010-07-15 10:16:33 -070074 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) {
Adam Powell42675342010-07-09 18:02:59 -070084 mContext = context;
Adam Powell696cba52011-03-29 10:38:16 -070085 mInflater = LayoutInflater.from(context);
Adam Powell8028dd32010-07-15 10:16:33 -070086 mMenu = menu;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070087 mAdapter = new MenuAdapter(mMenu);
Adam Powell8028dd32010-07-15 10:16:33 -070088 mOverflowOnly = overflowOnly;
Adam Powell42675342010-07-09 18:02:59 -070089
Adam Powell38639b12011-06-17 16:02:32 -070090 final Resources res = context.getResources();
91 mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
92 res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
Adam Powell8028dd32010-07-15 10:16:33 -070093
Adam Powell4afd62b2011-02-18 15:02:18 -080094 mAnchorView = anchorView;
Adam Powell696cba52011-03-29 10:38:16 -070095
96 menu.addMenuPresenter(this);
Adam Powell42675342010-07-09 18:02:59 -070097 }
98
Adam Powellf0ad6e62011-01-10 17:14:06 -080099 public void setAnchorView(View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800100 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -0800101 }
102
Adam Powell91511032011-07-13 10:24:06 -0700103 public void setForceShowIcon(boolean forceShow) {
104 mForceShowIcon = forceShow;
105 }
106
Adam Powell54c94de2013-09-26 15:36:34 -0700107 public void setGravity(int gravity) {
108 mDropDownGravity = gravity;
109 }
110
Adam Powell42675342010-07-09 18:02:59 -0700111 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800112 if (!tryShow()) {
113 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
114 }
115 }
116
Alan Viveretteca6a36112013-08-16 14:41:06 -0700117 public ListPopupWindow getPopup() {
118 return mPopup;
119 }
120
Adam Powell5e3f2842011-01-07 17:16:56 -0800121 public boolean tryShow() {
Adam Powell0b2d3062010-09-14 16:15:02 -0700122 mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle);
Adam Powell8515ee82010-11-30 14:09:55 -0800123 mPopup.setOnDismissListener(this);
Adam Powell696cba52011-03-29 10:38:16 -0700124 mPopup.setOnItemClickListener(this);
Adam Powell696cba52011-03-29 10:38:16 -0700125 mPopup.setAdapter(mAdapter);
Adam Powell42675342010-07-09 18:02:59 -0700126 mPopup.setModal(true);
127
Adam Powell4afd62b2011-02-18 15:02:18 -0800128 View anchor = mAnchorView;
Adam Powell04587962010-11-11 22:15:07 -0800129 if (anchor != null) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800130 final boolean addGlobalListener = mTreeObserver == null;
131 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
132 if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
133 anchor.addOnAttachStateChangeListener(this);
Adam Powell04587962010-11-11 22:15:07 -0800134 mPopup.setAnchorView(anchor);
Adam Powell54c94de2013-09-26 15:36:34 -0700135 mPopup.setDropDownGravity(mDropDownGravity);
Adam Powell4be0d522010-08-03 17:53:14 -0700136 } else {
Adam Powell5e3f2842011-01-07 17:16:56 -0800137 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700138 }
Adam Powell42675342010-07-09 18:02:59 -0700139
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700140 if (!mHasContentWidth) {
141 mContentWidth = measureContentWidth();
142 mHasContentWidth = true;
143 }
144
145 mPopup.setContentWidth(mContentWidth);
Adam Powellaa0b92c2010-12-13 22:38:53 -0800146 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Adam Powell42675342010-07-09 18:02:59 -0700147 mPopup.show();
Adam Powell8028dd32010-07-15 10:16:33 -0700148 mPopup.getListView().setOnKeyListener(this);
Adam Powell5e3f2842011-01-07 17:16:56 -0800149 return true;
Adam Powell42675342010-07-09 18:02:59 -0700150 }
151
152 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700153 if (isShowing()) {
154 mPopup.dismiss();
155 }
Adam Powell8515ee82010-11-30 14:09:55 -0800156 }
157
158 public void onDismiss() {
159 mPopup = null;
Adam Powell42b91bb2011-06-21 18:32:26 -0700160 mMenu.close();
Adam Powell4afd62b2011-02-18 15:02:18 -0800161 if (mTreeObserver != null) {
162 if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
Adam Powellca51e872011-02-14 19:54:29 -0800163 mTreeObserver.removeGlobalOnLayoutListener(this);
Adam Powell4afd62b2011-02-18 15:02:18 -0800164 mTreeObserver = null;
Adam Powelled8b4032010-11-16 10:22:35 -0800165 }
Adam Powell4afd62b2011-02-18 15:02:18 -0800166 mAnchorView.removeOnAttachStateChangeListener(this);
Adam Powell42675342010-07-09 18:02:59 -0700167 }
168
Adam Powell8028dd32010-07-15 10:16:33 -0700169 public boolean isShowing() {
170 return mPopup != null && mPopup.isShowing();
171 }
172
Adam Powell696cba52011-03-29 10:38:16 -0700173 @Override
Adam Powell42675342010-07-09 18:02:59 -0700174 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Adam Powell696cba52011-03-29 10:38:16 -0700175 MenuAdapter adapter = mAdapter;
176 adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
Adam Powell42675342010-07-09 18:02:59 -0700177 }
178
Adam Powell8028dd32010-07-15 10:16:33 -0700179 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
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700187 private int measureContentWidth() {
Adam Powell42675342010-07-09 18:02:59 -0700188 // Menus don't tend to be long, so this is more sane than it looks.
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700189 int maxWidth = 0;
Adam Powell42675342010-07-09 18:02:59 -0700190 View itemView = null;
Adam Powell50f784c2010-12-19 16:12:19 -0800191 int itemType = 0;
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700192
193 final ListAdapter adapter = mAdapter;
194 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
195 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
Adam Powell42675342010-07-09 18:02:59 -0700196 final int count = adapter.getCount();
197 for (int i = 0; i < count; i++) {
Adam Powell50f784c2010-12-19 16:12:19 -0800198 final int positionType = adapter.getItemViewType(i);
199 if (positionType != itemType) {
200 itemType = positionType;
201 itemView = null;
202 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700203
Adam Powell91511032011-07-13 10:24:06 -0700204 if (mMeasureParent == null) {
205 mMeasureParent = new FrameLayout(mContext);
206 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700207
Adam Powell91511032011-07-13 10:24:06 -0700208 itemView = adapter.getView(i, itemView, mMeasureParent);
Adam Powell42675342010-07-09 18:02:59 -0700209 itemView.measure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700210
211 final int itemWidth = itemView.getMeasuredWidth();
212 if (itemWidth >= mPopupMaxWidth) {
213 return mPopupMaxWidth;
214 } else if (itemWidth > maxWidth) {
215 maxWidth = itemWidth;
216 }
Adam Powell42675342010-07-09 18:02:59 -0700217 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700218
219 return maxWidth;
Adam Powell42675342010-07-09 18:02:59 -0700220 }
Adam Powell04587962010-11-11 22:15:07 -0800221
222 @Override
223 public void onGlobalLayout() {
Adam Powell4afd62b2011-02-18 15:02:18 -0800224 if (isShowing()) {
225 final View anchor = mAnchorView;
Adam Powellca51e872011-02-14 19:54:29 -0800226 if (anchor == null || !anchor.isShown()) {
Adam Powell04587962010-11-11 22:15:07 -0800227 dismiss();
Adam Powellca51e872011-02-14 19:54:29 -0800228 } else if (isShowing()) {
Adam Powellaa0b92c2010-12-13 22:38:53 -0800229 // Recompute window size and position
230 mPopup.show();
Adam Powell04587962010-11-11 22:15:07 -0800231 }
232 }
233 }
Adam Powell4afd62b2011-02-18 15:02:18 -0800234
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 }
Adam Powell696cba52011-03-29 10:38:16 -0700247
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) {
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700260 mHasContentWidth = false;
261
262 if (mAdapter != null) {
263 mAdapter.notifyDataSetChanged();
264 }
Adam Powell696cba52011-03-29 10:38:16 -0700265 }
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);
Adam Powell91511032011-07-13 10:24:06 -0700277
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
Adam Powell696cba52011-03-29 10:38:16 -0700289 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
Adam Powell8d02dea2011-05-31 21:35:13 -0700315 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
Adam Powell275702c2011-09-23 17:34:04 -0700323 @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
Adam Powell696cba52011-03-29 10:38:16 -0700337 private class MenuAdapter extends BaseAdapter {
338 private MenuBuilder mAdapterMenu;
Adam Powell275702c2011-09-23 17:34:04 -0700339 private int mExpandedIndex = -1;
Adam Powell696cba52011-03-29 10:38:16 -0700340
341 public MenuAdapter(MenuBuilder menu) {
342 mAdapterMenu = menu;
Adam Powell275702c2011-09-23 17:34:04 -0700343 findExpandedIndex();
Adam Powell696cba52011-03-29 10:38:16 -0700344 }
345
346 public int getCount() {
347 ArrayList<MenuItemImpl> items = mOverflowOnly ?
348 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
Adam Powell275702c2011-09-23 17:34:04 -0700349 if (mExpandedIndex < 0) {
350 return items.size();
351 }
352 return items.size() - 1;
Adam Powell696cba52011-03-29 10:38:16 -0700353 }
354
355 public MenuItemImpl getItem(int position) {
356 ArrayList<MenuItemImpl> items = mOverflowOnly ?
357 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
Adam Powell275702c2011-09-23 17:34:04 -0700358 if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
359 position++;
360 }
Adam Powell696cba52011-03-29 10:38:16 -0700361 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;
Adam Powell91511032011-07-13 10:24:06 -0700376 if (mForceShowIcon) {
377 ((ListMenuItemView) convertView).setForceShowIcon(true);
378 }
Adam Powell696cba52011-03-29 10:38:16 -0700379 itemView.initialize(getItem(position), 0);
380 return convertView;
381 }
Adam Powell275702c2011-09-23 17:34:04 -0700382
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 }
Adam Powell11ed1d62011-07-11 21:19:59 -0700398
Adam Powell275702c2011-09-23 17:34:04 -0700399 @Override
Adam Powell76889f32012-05-08 22:22:52 -0700400 public void notifyDataSetChanged() {
401 findExpandedIndex();
402 super.notifyDataSetChanged();
Adam Powell275702c2011-09-23 17:34:04 -0700403 }
Adam Powell11ed1d62011-07-11 21:19:59 -0700404 }
Adam Powell42675342010-07-09 18:02:59 -0700405}