StandardMenuPopup.java revision c8fa32982e35b4311aacd07c7f6529708ea22a18
1/* 2 * Copyright (C) 2015 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 android.support.v7.view.menu; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.os.Parcelable; 22import android.support.v7.appcompat.R; 23import android.support.v7.widget.MenuPopupWindow; 24import android.view.Gravity; 25import android.view.KeyEvent; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.View.OnKeyListener; 29import android.view.ViewTreeObserver; 30import android.view.ViewTreeObserver.OnGlobalLayoutListener; 31import android.widget.AdapterView.OnItemClickListener; 32import android.widget.FrameLayout; 33import android.widget.ListView; 34import android.widget.PopupWindow; 35import android.widget.PopupWindow.OnDismissListener; 36import android.widget.TextView; 37 38/** 39 * A standard menu popup in which when a submenu is opened, it replaces its parent menu in the 40 * viewport. 41 */ 42final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener, 43 MenuPresenter, OnKeyListener { 44 45 private final Context mContext; 46 47 private final MenuBuilder mMenu; 48 private final MenuAdapter mAdapter; 49 private final boolean mOverflowOnly; 50 private final int mPopupMaxWidth; 51 private final int mPopupStyleAttr; 52 private final int mPopupStyleRes; 53 // The popup window is final in order to couple its lifecycle to the lifecycle of the 54 // StandardMenuPopup. 55 final MenuPopupWindow mPopup; 56 57 private final OnGlobalLayoutListener mGlobalLayoutListener = new OnGlobalLayoutListener() { 58 @Override 59 public void onGlobalLayout() { 60 // Only move the popup if it's showing and non-modal. We don't want 61 // to be moving around the only interactive window, since there's a 62 // good chance the user is interacting with it. 63 if (isShowing() && !mPopup.isModal()) { 64 final View anchor = mShownAnchorView; 65 if (anchor == null || !anchor.isShown()) { 66 dismiss(); 67 } else { 68 // Recompute window size and position 69 mPopup.show(); 70 } 71 } 72 } 73 }; 74 75 private final View.OnAttachStateChangeListener mAttachStateChangeListener = 76 new View.OnAttachStateChangeListener() { 77 @Override 78 public void onViewAttachedToWindow(View v) { 79 } 80 81 @Override 82 public void onViewDetachedFromWindow(View v) { 83 if (mTreeObserver != null) { 84 if (!mTreeObserver.isAlive()) mTreeObserver = v.getViewTreeObserver(); 85 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); 86 } 87 v.removeOnAttachStateChangeListener(this); 88 } 89 }; 90 91 private PopupWindow.OnDismissListener mOnDismissListener; 92 93 private View mAnchorView; 94 View mShownAnchorView; 95 private Callback mPresenterCallback; 96 private ViewTreeObserver mTreeObserver; 97 98 /** Whether the popup has been dismissed. Once dismissed, it cannot be opened again. */ 99 private boolean mWasDismissed; 100 101 /** Whether the cached content width value is valid. */ 102 private boolean mHasContentWidth; 103 104 /** Cached content width. */ 105 private int mContentWidth; 106 107 private int mDropDownGravity = Gravity.NO_GRAVITY; 108 109 private boolean mShowTitle; 110 111 public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr, 112 int popupStyleRes, boolean overflowOnly) { 113 mContext = context; 114 mMenu = menu; 115 mOverflowOnly = overflowOnly; 116 final LayoutInflater inflater = LayoutInflater.from(context); 117 mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly); 118 mPopupStyleAttr = popupStyleAttr; 119 mPopupStyleRes = popupStyleRes; 120 121 final Resources res = context.getResources(); 122 mPopupMaxWidth = Math.max(res.getDisplayMetrics().widthPixels / 2, 123 res.getDimensionPixelSize(R.dimen.abc_config_prefDialogWidth)); 124 125 mAnchorView = anchorView; 126 127 mPopup = new MenuPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); 128 129 // Present the menu using our context, not the menu builder's context. 130 menu.addMenuPresenter(this, context); 131 } 132 133 @Override 134 public void setForceShowIcon(boolean forceShow) { 135 mAdapter.setForceShowIcon(forceShow); 136 } 137 138 @Override 139 public void setGravity(int gravity) { 140 mDropDownGravity = gravity; 141 } 142 143 private boolean tryShow() { 144 if (isShowing()) { 145 return true; 146 } 147 148 if (mWasDismissed || mAnchorView == null) { 149 return false; 150 } 151 152 mShownAnchorView = mAnchorView; 153 154 mPopup.setOnDismissListener(this); 155 mPopup.setOnItemClickListener(this); 156 mPopup.setModal(true); 157 158 final View anchor = mShownAnchorView; 159 final boolean addGlobalListener = mTreeObserver == null; 160 mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest 161 if (addGlobalListener) { 162 mTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener); 163 } 164 anchor.addOnAttachStateChangeListener(mAttachStateChangeListener); 165 mPopup.setAnchorView(anchor); 166 mPopup.setDropDownGravity(mDropDownGravity); 167 168 if (!mHasContentWidth) { 169 mContentWidth = measureIndividualMenuWidth(mAdapter, null, mContext, mPopupMaxWidth); 170 mHasContentWidth = true; 171 } 172 173 mPopup.setContentWidth(mContentWidth); 174 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 175 mPopup.setEpicenterBounds(getEpicenterBounds()); 176 mPopup.show(); 177 178 final ListView listView = mPopup.getListView(); 179 listView.setOnKeyListener(this); 180 181 if (mShowTitle && mMenu.getHeaderTitle() != null) { 182 FrameLayout titleItemView = 183 (FrameLayout) LayoutInflater.from(mContext).inflate( 184 R.layout.abc_popup_menu_header_item_layout, listView, false); 185 TextView titleView = (TextView) titleItemView.findViewById(android.R.id.title); 186 if (titleView != null) { 187 titleView.setText(mMenu.getHeaderTitle()); 188 } 189 titleItemView.setEnabled(false); 190 listView.addHeaderView(titleItemView, null, false); 191 } 192 193 // Since addHeaderView() needs to be called before setAdapter() pre-v14, we have to set the 194 // adapter as late as possible, and then call show again to update 195 mPopup.setAdapter(mAdapter); 196 mPopup.show(); 197 198 return true; 199 } 200 201 @Override 202 public void show() { 203 if (!tryShow()) { 204 throw new IllegalStateException("StandardMenuPopup cannot be used without an anchor"); 205 } 206 } 207 208 @Override 209 public void dismiss() { 210 if (isShowing()) { 211 mPopup.dismiss(); 212 } 213 } 214 215 @Override 216 public void addMenu(MenuBuilder menu) { 217 // No-op: standard implementation has only one menu which is set in the constructor. 218 } 219 220 @Override 221 public boolean isShowing() { 222 return !mWasDismissed && mPopup.isShowing(); 223 } 224 225 @Override 226 public void onDismiss() { 227 mWasDismissed = true; 228 mMenu.close(); 229 230 if (mTreeObserver != null) { 231 if (!mTreeObserver.isAlive()) mTreeObserver = mShownAnchorView.getViewTreeObserver(); 232 mTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener); 233 mTreeObserver = null; 234 } 235 mShownAnchorView.removeOnAttachStateChangeListener(mAttachStateChangeListener); 236 237 if (mOnDismissListener != null) { 238 mOnDismissListener.onDismiss(); 239 } 240 } 241 242 @Override 243 public void updateMenuView(boolean cleared) { 244 mHasContentWidth = false; 245 246 if (mAdapter != null) { 247 mAdapter.notifyDataSetChanged(); 248 } 249 } 250 251 @Override 252 public void setCallback(Callback cb) { 253 mPresenterCallback = cb; 254 } 255 256 @Override 257 public boolean onSubMenuSelected(SubMenuBuilder subMenu) { 258 if (subMenu.hasVisibleItems()) { 259 final MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, 260 mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes); 261 subPopup.setPresenterCallback(mPresenterCallback); 262 subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu)); 263 subPopup.setGravity(mDropDownGravity); 264 265 // Pass responsibility for handling onDismiss to the submenu. 266 subPopup.setOnDismissListener(mOnDismissListener); 267 mOnDismissListener = null; 268 269 // Close this menu popup to make room for the submenu popup. 270 mMenu.close(false /* closeAllMenus */); 271 272 // Show the new sub-menu popup at the same location as this popup. 273 final int horizontalOffset = mPopup.getHorizontalOffset(); 274 final int verticalOffset = mPopup.getVerticalOffset(); 275 if (subPopup.tryShow(horizontalOffset, verticalOffset)) { 276 if (mPresenterCallback != null) { 277 mPresenterCallback.onOpenSubMenu(subMenu); 278 } 279 return true; 280 } 281 } 282 return false; 283 } 284 285 @Override 286 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 287 // Only care about the (sub)menu we're presenting. 288 if (menu != mMenu) return; 289 290 dismiss(); 291 if (mPresenterCallback != null) { 292 mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); 293 } 294 } 295 296 @Override 297 public boolean flagActionItems() { 298 return false; 299 } 300 301 @Override 302 public Parcelable onSaveInstanceState() { 303 return null; 304 } 305 306 @Override 307 public void onRestoreInstanceState(Parcelable state) { 308 } 309 310 @Override 311 public void setAnchorView(View anchor) { 312 mAnchorView = anchor; 313 } 314 315 @Override 316 public boolean onKey(View v, int keyCode, KeyEvent event) { 317 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) { 318 dismiss(); 319 return true; 320 } 321 return false; 322 } 323 324 @Override 325 public void setOnDismissListener(OnDismissListener listener) { 326 mOnDismissListener = listener; 327 } 328 329 @Override 330 public ListView getListView() { 331 return mPopup.getListView(); 332 } 333 334 335 @Override 336 public void setHorizontalOffset(int x) { 337 mPopup.setHorizontalOffset(x); 338 } 339 340 @Override 341 public void setVerticalOffset(int y) { 342 mPopup.setVerticalOffset(y); 343 } 344 345 @Override 346 public void setShowTitle(boolean showTitle) { 347 mShowTitle = showTitle; 348 } 349} 350