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