[go: nahoru, domu]

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 com.android.internal.view.menu;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.os.Parcelable;
22import android.view.Gravity;
23import android.view.KeyEvent;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.View.OnAttachStateChangeListener;
27import android.view.View.OnKeyListener;
28import android.view.ViewTreeObserver.OnGlobalLayoutListener;
29import android.view.ViewTreeObserver;
30import android.widget.FrameLayout;
31import android.widget.ListView;
32import android.widget.MenuPopupWindow;
33import android.widget.PopupWindow;
34import android.widget.TextView;
35import android.widget.AdapterView.OnItemClickListener;
36import android.widget.PopupWindow.OnDismissListener;
37
38import 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 */
44final 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
267            // Pass responsibility for handling onDismiss to the submenu.
268            subPopup.setOnDismissListener(mOnDismissListener);
269            mOnDismissListener = null;
270
271            // Close this menu popup to make room for the submenu popup.
272            mMenu.close(false /* closeAllMenus */);
273
274            // Show the new sub-menu popup at the same location as this popup.
275            final int horizontalOffset = mPopup.getHorizontalOffset();
276            final int verticalOffset = mPopup.getVerticalOffset();
277            if (subPopup.tryShow(horizontalOffset, verticalOffset)) {
278                if (mPresenterCallback != null) {
279                    mPresenterCallback.onOpenSubMenu(subMenu);
280                }
281                return true;
282            }
283        }
284        return false;
285    }
286
287    @Override
288    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
289        // Only care about the (sub)menu we're presenting.
290        if (menu != mMenu) return;
291
292        dismiss();
293        if (mPresenterCallback != null) {
294            mPresenterCallback.onCloseMenu(menu, allMenusAreClosing);
295        }
296    }
297
298    @Override
299    public boolean flagActionItems() {
300        return false;
301    }
302
303    @Override
304    public Parcelable onSaveInstanceState() {
305        return null;
306    }
307
308    @Override
309    public void onRestoreInstanceState(Parcelable state) {
310    }
311
312    @Override
313    public void setAnchorView(View anchor) {
314        mAnchorView = anchor;
315    }
316
317    @Override
318    public boolean onKey(View v, int keyCode, KeyEvent event) {
319        if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_MENU) {
320            dismiss();
321            return true;
322        }
323        return false;
324    }
325
326    @Override
327    public void setOnDismissListener(OnDismissListener listener) {
328        mOnDismissListener = listener;
329    }
330
331    @Override
332    public ListView getListView() {
333        return mPopup.getListView();
334    }
335
336
337    @Override
338    public void setHorizontalOffset(int x) {
339        mPopup.setHorizontalOffset(x);
340    }
341
342    @Override
343    public void setVerticalOffset(int y) {
344        mPopup.setVerticalOffset(y);
345    }
346
347    @Override
348    public void setShowTitle(boolean showTitle) {
349        mShowTitle = showTitle;
350    }
351}
352