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