Home | History | Annotate | Download | only in menu
      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