blob: 7d4507150f8e8a4b0774ef8441cba93943d2d7bc [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 Powell696cba52011-03-29 10:38:16 -070046 static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout;
47
Alan Viverette0bce6ab2013-06-26 17:46:16 -070048 private final Context mContext;
49 private final LayoutInflater mInflater;
50 private final MenuBuilder mMenu;
51 private final MenuAdapter mAdapter;
52 private final boolean mOverflowOnly;
53 private final int mPopupMaxWidth;
Alan Viverette560f1702014-05-05 14:40:07 -070054 private final int mPopupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070055 private final int mPopupStyleRes;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070056
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) {
Alan Viverette29632522014-10-15 17:19:30 -070075 this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070076 }
77
78 public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView) {
Alan Viverette29632522014-10-15 17:19:30 -070079 this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
Adam Powell8028dd32010-07-15 10:16:33 -070080 }
81
Alan Viverette560f1702014-05-05 14:40:07 -070082 public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView,
83 boolean overflowOnly, int popupStyleAttr) {
Alan Viverette29632522014-10-15 17:19:30 -070084 this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
85 }
86
87 public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView,
88 boolean overflowOnly, int popupStyleAttr, int popupStyleRes) {
Adam Powell42675342010-07-09 18:02:59 -070089 mContext = context;
Adam Powell696cba52011-03-29 10:38:16 -070090 mInflater = LayoutInflater.from(context);
Adam Powell8028dd32010-07-15 10:16:33 -070091 mMenu = menu;
Alan Viverette0bce6ab2013-06-26 17:46:16 -070092 mAdapter = new MenuAdapter(mMenu);
Adam Powell8028dd32010-07-15 10:16:33 -070093 mOverflowOnly = overflowOnly;
Alan Viverette560f1702014-05-05 14:40:07 -070094 mPopupStyleAttr = popupStyleAttr;
Alan Viverette29632522014-10-15 17:19:30 -070095 mPopupStyleRes = popupStyleRes;
Adam Powell42675342010-07-09 18:02:59 -070096
Adam Powell38639b12011-06-17 16:02:32 -070097 final Resources res = context.getResources();
98 mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2,
99 res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
Adam Powell8028dd32010-07-15 10:16:33 -0700100
Adam Powell4afd62b2011-02-18 15:02:18 -0800101 mAnchorView = anchorView;
Adam Powell696cba52011-03-29 10:38:16 -0700102
Alan Viverette3d0f21d2014-07-10 16:15:01 -0700103 // Present the menu using our context, not the menu builder's context.
104 menu.addMenuPresenter(this, context);
Adam Powell42675342010-07-09 18:02:59 -0700105 }
106
Adam Powellf0ad6e62011-01-10 17:14:06 -0800107 public void setAnchorView(View anchor) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800108 mAnchorView = anchor;
Adam Powellf0ad6e62011-01-10 17:14:06 -0800109 }
110
Adam Powell91511032011-07-13 10:24:06 -0700111 public void setForceShowIcon(boolean forceShow) {
112 mForceShowIcon = forceShow;
113 }
114
Adam Powell54c94de2013-09-26 15:36:34 -0700115 public void setGravity(int gravity) {
116 mDropDownGravity = gravity;
117 }
118
Alan Viverette75d83792015-01-07 15:51:54 -0800119 public int getGravity() {
120 return mDropDownGravity;
121 }
122
Adam Powell42675342010-07-09 18:02:59 -0700123 public void show() {
Adam Powell5e3f2842011-01-07 17:16:56 -0800124 if (!tryShow()) {
125 throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
126 }
127 }
128
Alan Viveretteca6a36112013-08-16 14:41:06 -0700129 public ListPopupWindow getPopup() {
130 return mPopup;
131 }
132
Alan Viverette8fd949e2015-03-11 12:21:30 -0700133 /**
134 * Attempts to show the popup anchored to the view specified by
135 * {@link #setAnchorView(View)}.
136 *
137 * @return {@code true} if the popup was shown or was already showing prior
138 * to calling this method, {@code false} otherwise
139 */
Adam Powell5e3f2842011-01-07 17:16:56 -0800140 public boolean tryShow() {
Alan Viverette8fd949e2015-03-11 12:21:30 -0700141 if (isShowing()) {
142 return true;
143 }
144
Alan Viverette29632522014-10-15 17:19:30 -0700145 mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);
Adam Powell8515ee82010-11-30 14:09:55 -0800146 mPopup.setOnDismissListener(this);
Adam Powell696cba52011-03-29 10:38:16 -0700147 mPopup.setOnItemClickListener(this);
Adam Powell696cba52011-03-29 10:38:16 -0700148 mPopup.setAdapter(mAdapter);
Adam Powell42675342010-07-09 18:02:59 -0700149 mPopup.setModal(true);
150
Alan Viverette75d83792015-01-07 15:51:54 -0800151 final View anchor = mAnchorView;
Adam Powell04587962010-11-11 22:15:07 -0800152 if (anchor != null) {
Adam Powell4afd62b2011-02-18 15:02:18 -0800153 final boolean addGlobalListener = mTreeObserver == null;
154 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
155 if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
156 anchor.addOnAttachStateChangeListener(this);
Adam Powell04587962010-11-11 22:15:07 -0800157 mPopup.setAnchorView(anchor);
Adam Powell54c94de2013-09-26 15:36:34 -0700158 mPopup.setDropDownGravity(mDropDownGravity);
Adam Powell4be0d522010-08-03 17:53:14 -0700159 } else {
Adam Powell5e3f2842011-01-07 17:16:56 -0800160 return false;
Adam Powell8028dd32010-07-15 10:16:33 -0700161 }
Adam Powell42675342010-07-09 18:02:59 -0700162
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700163 if (!mHasContentWidth) {
164 mContentWidth = measureContentWidth();
165 mHasContentWidth = true;
166 }
167
168 mPopup.setContentWidth(mContentWidth);
Adam Powellaa0b92c2010-12-13 22:38:53 -0800169 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Adam Powell42675342010-07-09 18:02:59 -0700170 mPopup.show();
Adam Powell8028dd32010-07-15 10:16:33 -0700171 mPopup.getListView().setOnKeyListener(this);
Adam Powell5e3f2842011-01-07 17:16:56 -0800172 return true;
Adam Powell42675342010-07-09 18:02:59 -0700173 }
174
175 public void dismiss() {
Adam Powell3d3da272010-08-11 18:06:17 -0700176 if (isShowing()) {
177 mPopup.dismiss();
178 }
Adam Powell8515ee82010-11-30 14:09:55 -0800179 }
180
Alan Viverette8fd949e2015-03-11 12:21:30 -0700181 @Override
Adam Powell8515ee82010-11-30 14:09:55 -0800182 public void onDismiss() {
183 mPopup = null;
Adam Powell42b91bb2011-06-21 18:32:26 -0700184 mMenu.close();
Adam Powell4afd62b2011-02-18 15:02:18 -0800185 if (mTreeObserver != null) {
186 if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
Adam Powellca51e872011-02-14 19:54:29 -0800187 mTreeObserver.removeGlobalOnLayoutListener(this);
Adam Powell4afd62b2011-02-18 15:02:18 -0800188 mTreeObserver = null;
Adam Powelled8b4032010-11-16 10:22:35 -0800189 }
Adam Powell4afd62b2011-02-18 15:02:18 -0800190 mAnchorView.removeOnAttachStateChangeListener(this);
Adam Powell42675342010-07-09 18:02:59 -0700191 }
192
Adam Powell8028dd32010-07-15 10:16:33 -0700193 public boolean isShowing() {
194 return mPopup != null && mPopup.isShowing();
195 }
196
Adam Powell696cba52011-03-29 10:38:16 -0700197 @Override
Adam Powell42675342010-07-09 18:02:59 -0700198 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Adam Powell696cba52011-03-29 10:38:16 -0700199 MenuAdapter adapter = mAdapter;
200 adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
Adam Powell42675342010-07-09 18:02:59 -0700201 }
202
Alan Viverette8fd949e2015-03-11 12:21:30 -0700203 @Override
Adam Powell8028dd32010-07-15 10:16:33 -0700204 public boolean onKey(View v, int keyCode, KeyEvent event) {
205 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
206 dismiss();
207 return true;
208 }
209 return false;
210 }
211
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700212 private int measureContentWidth() {
Adam Powell42675342010-07-09 18:02:59 -0700213 // Menus don't tend to be long, so this is more sane than it looks.
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700214 int maxWidth = 0;
Adam Powell42675342010-07-09 18:02:59 -0700215 View itemView = null;
Adam Powell50f784c2010-12-19 16:12:19 -0800216 int itemType = 0;
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700217
218 final ListAdapter adapter = mAdapter;
219 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
220 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
Adam Powell42675342010-07-09 18:02:59 -0700221 final int count = adapter.getCount();
222 for (int i = 0; i < count; i++) {
Adam Powell50f784c2010-12-19 16:12:19 -0800223 final int positionType = adapter.getItemViewType(i);
224 if (positionType != itemType) {
225 itemType = positionType;
226 itemView = null;
227 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700228
Adam Powell91511032011-07-13 10:24:06 -0700229 if (mMeasureParent == null) {
230 mMeasureParent = new FrameLayout(mContext);
231 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700232
Adam Powell91511032011-07-13 10:24:06 -0700233 itemView = adapter.getView(i, itemView, mMeasureParent);
Adam Powell42675342010-07-09 18:02:59 -0700234 itemView.measure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700235
236 final int itemWidth = itemView.getMeasuredWidth();
237 if (itemWidth >= mPopupMaxWidth) {
238 return mPopupMaxWidth;
239 } else if (itemWidth > maxWidth) {
240 maxWidth = itemWidth;
241 }
Adam Powell42675342010-07-09 18:02:59 -0700242 }
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700243
244 return maxWidth;
Adam Powell42675342010-07-09 18:02:59 -0700245 }
Adam Powell04587962010-11-11 22:15:07 -0800246
247 @Override
248 public void onGlobalLayout() {
Adam Powell4afd62b2011-02-18 15:02:18 -0800249 if (isShowing()) {
250 final View anchor = mAnchorView;
Adam Powellca51e872011-02-14 19:54:29 -0800251 if (anchor == null || !anchor.isShown()) {
Adam Powell04587962010-11-11 22:15:07 -0800252 dismiss();
Adam Powellca51e872011-02-14 19:54:29 -0800253 } else if (isShowing()) {
Adam Powellaa0b92c2010-12-13 22:38:53 -0800254 // Recompute window size and position
255 mPopup.show();
Adam Powell04587962010-11-11 22:15:07 -0800256 }
257 }
258 }
Adam Powell4afd62b2011-02-18 15:02:18 -0800259
260 @Override
261 public void onViewAttachedToWindow(View v) {
262 }
263
264 @Override
265 public void onViewDetachedFromWindow(View v) {
266 if (mTreeObserver != null) {
267 if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver();
268 mTreeObserver.removeGlobalOnLayoutListener(this);
269 }
270 v.removeOnAttachStateChangeListener(this);
271 }
Adam Powell696cba52011-03-29 10:38:16 -0700272
273 @Override
274 public void initForMenu(Context context, MenuBuilder menu) {
275 // Don't need to do anything; we added as a presenter in the constructor.
276 }
277
278 @Override
279 public MenuView getMenuView(ViewGroup root) {
280 throw new UnsupportedOperationException("MenuPopupHelpers manage their own views");
281 }
282
283 @Override
284 public void updateMenuView(boolean cleared) {
Alan Viverette0bce6ab2013-06-26 17:46:16 -0700285 mHasContentWidth = false;
286
287 if (mAdapter != null) {
288 mAdapter.notifyDataSetChanged();
289 }
Adam Powell696cba52011-03-29 10:38:16 -0700290 }
291
292 @Override
293 public void setCallback(Callback cb) {
294 mPresenterCallback = cb;
295 }
296
297 @Override
298 public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
299 if (subMenu.hasVisibleItems()) {
Alan Viverette560f1702014-05-05 14:40:07 -0700300 MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView);
Adam Powell696cba52011-03-29 10:38:16 -0700301 subPopup.setCallback(mPresenterCallback);
Adam Powell91511032011-07-13 10:24:06 -0700302
303 boolean preserveIconSpacing = false;
304 final int count = subMenu.size();
305 for (int i = 0; i < count; i++) {
306 MenuItem childItem = subMenu.getItem(i);
307 if (childItem.isVisible() && childItem.getIcon() != null) {
308 preserveIconSpacing = true;
309 break;
310 }
311 }
312 subPopup.setForceShowIcon(preserveIconSpacing);
313
Adam Powell696cba52011-03-29 10:38:16 -0700314 if (subPopup.tryShow()) {
315 if (mPresenterCallback != null) {
316 mPresenterCallback.onOpenSubMenu(subMenu);
317 }
318 return true;
319 }
320 }
321 return false;
322 }
323
324 @Override
325 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
326 // Only care about the (sub)menu we're presenting.
327 if (menu != mMenu) return;
328
329 dismiss();
330 if (mPresenterCallback != null) {
331 mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
332 }
333 }
334
335 @Override
336 public boolean flagActionItems() {
337 return false;
338 }
339
Adam Powell8d02dea2011-05-31 21:35:13 -0700340 public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
341 return false;
342 }
343
344 public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
345 return false;
346 }
347
Adam Powell275702c2011-09-23 17:34:04 -0700348 @Override
349 public int getId() {
350 return 0;
351 }
352
353 @Override
354 public Parcelable onSaveInstanceState() {
355 return null;
356 }
357
358 @Override
359 public void onRestoreInstanceState(Parcelable state) {
360 }
361
Adam Powell696cba52011-03-29 10:38:16 -0700362 private class MenuAdapter extends BaseAdapter {
363 private MenuBuilder mAdapterMenu;
Adam Powell275702c2011-09-23 17:34:04 -0700364 private int mExpandedIndex = -1;
Adam Powell696cba52011-03-29 10:38:16 -0700365
366 public MenuAdapter(MenuBuilder menu) {
367 mAdapterMenu = menu;
Adam Powell275702c2011-09-23 17:34:04 -0700368 findExpandedIndex();
Adam Powell696cba52011-03-29 10:38:16 -0700369 }
370
371 public int getCount() {
372 ArrayList<MenuItemImpl> items = mOverflowOnly ?
373 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
Adam Powell275702c2011-09-23 17:34:04 -0700374 if (mExpandedIndex < 0) {
375 return items.size();
376 }
377 return items.size() - 1;
Adam Powell696cba52011-03-29 10:38:16 -0700378 }
379
380 public MenuItemImpl getItem(int position) {
381 ArrayList<MenuItemImpl> items = mOverflowOnly ?
382 mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
Adam Powell275702c2011-09-23 17:34:04 -0700383 if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
384 position++;
385 }
Adam Powell696cba52011-03-29 10:38:16 -0700386 return items.get(position);
387 }
388
389 public long getItemId(int position) {
390 // Since a menu item's ID is optional, we'll use the position as an
391 // ID for the item in the AdapterView
392 return position;
393 }
394
395 public View getView(int position, View convertView, ViewGroup parent) {
396 if (convertView == null) {
397 convertView = mInflater.inflate(ITEM_LAYOUT, parent, false);
398 }
399
400 MenuView.ItemView itemView = (MenuView.ItemView) convertView;
Adam Powell91511032011-07-13 10:24:06 -0700401 if (mForceShowIcon) {
402 ((ListMenuItemView) convertView).setForceShowIcon(true);
403 }
Adam Powell696cba52011-03-29 10:38:16 -0700404 itemView.initialize(getItem(position), 0);
405 return convertView;
406 }
Adam Powell275702c2011-09-23 17:34:04 -0700407
408 void findExpandedIndex() {
409 final MenuItemImpl expandedItem = mMenu.getExpandedItem();
410 if (expandedItem != null) {
411 final ArrayList<MenuItemImpl> items = mMenu.getNonActionItems();
412 final int count = items.size();
413 for (int i = 0; i < count; i++) {
414 final MenuItemImpl item = items.get(i);
415 if (item == expandedItem) {
416 mExpandedIndex = i;
417 return;
418 }
419 }
420 }
421 mExpandedIndex = -1;
422 }
Adam Powell11ed1d62011-07-11 21:19:59 -0700423
Adam Powell275702c2011-09-23 17:34:04 -0700424 @Override
Adam Powell76889f32012-05-08 22:22:52 -0700425 public void notifyDataSetChanged() {
426 findExpandedIndex();
427 super.notifyDataSetChanged();
Adam Powell275702c2011-09-23 17:34:04 -0700428 }
Adam Powell11ed1d62011-07-11 21:19:59 -0700429 }
Adam Powell42675342010-07-09 18:02:59 -0700430}