ActionMenuItemView.java revision d719539f91d4f5cf6c0f9be0b68b750dcd443433
1/*
2 * Copyright (C) 2010 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.Configuration;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.text.TextUtils;
26import android.util.AttributeSet;
27import android.view.Gravity;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.accessibility.AccessibilityEvent;
31import android.widget.ActionMenuView;
32import android.widget.ListPopupWindow;
33import android.widget.TextView;
34import android.widget.Toast;
35import android.widget.ListPopupWindow.ForwardingListener;
36
37/**
38 * @hide
39 */
40public class ActionMenuItemView extends TextView
41        implements MenuView.ItemView, View.OnClickListener, View.OnLongClickListener,
42        ActionMenuView.ActionMenuChildView {
43    private static final String TAG = "ActionMenuItemView";
44
45    private MenuItemImpl mItemData;
46    private CharSequence mTitle;
47    private Drawable mIcon;
48    private MenuBuilder.ItemInvoker mItemInvoker;
49    private ForwardingListener mForwardingListener;
50    private PopupCallback mPopupCallback;
51
52    private boolean mAllowTextWithIcon;
53    private boolean mExpandedFormat;
54    private int mMinWidth;
55    private int mSavedPaddingLeft;
56
57    private static final int MAX_ICON_SIZE = 32; // dp
58    private int mMaxIconSize;
59
60    public ActionMenuItemView(Context context) {
61        this(context, null);
62    }
63
64    public ActionMenuItemView(Context context, AttributeSet attrs) {
65        this(context, attrs, 0);
66    }
67
68    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
69        this(context, attrs, defStyleAttr, 0);
70    }
71
72    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
73        super(context, attrs, defStyleAttr, defStyleRes);
74        final Resources res = context.getResources();
75        mAllowTextWithIcon = res.getBoolean(
76                com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
77        final TypedArray a = context.obtainStyledAttributes(attrs,
78                com.android.internal.R.styleable.ActionMenuItemView, defStyleAttr, defStyleRes);
79        mMinWidth = a.getDimensionPixelSize(
80                com.android.internal.R.styleable.ActionMenuItemView_minWidth, 0);
81        a.recycle();
82
83        final float density = res.getDisplayMetrics().density;
84        mMaxIconSize = (int) (MAX_ICON_SIZE * density + 0.5f);
85
86        setOnClickListener(this);
87        setOnLongClickListener(this);
88
89        mSavedPaddingLeft = -1;
90    }
91
92    @Override
93    public void onConfigurationChanged(Configuration newConfig) {
94        super.onConfigurationChanged(newConfig);
95
96        mAllowTextWithIcon = getContext().getResources().getBoolean(
97                com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
98        updateTextButtonVisibility();
99    }
100
101    @Override
102    public void setPadding(int l, int t, int r, int b) {
103        mSavedPaddingLeft = l;
104        super.setPadding(l, t, r, b);
105    }
106
107    public MenuItemImpl getItemData() {
108        return mItemData;
109    }
110
111    @Override
112    public void initialize(MenuItemImpl itemData, int menuType) {
113        mItemData = itemData;
114
115        setIcon(itemData.getIcon());
116        setTitle(itemData.getTitleForItemView(this)); // Title only takes effect if there is no icon
117        setId(itemData.getItemId());
118
119        setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
120        setEnabled(itemData.isEnabled());
121
122        if (itemData.hasSubMenu()) {
123            if (mForwardingListener == null) {
124                mForwardingListener = new ActionMenuItemForwardingListener();
125            }
126        }
127    }
128
129    @Override
130    public boolean onTouchEvent(MotionEvent e) {
131        if (mItemData.hasSubMenu() && mForwardingListener != null
132                && mForwardingListener.onTouch(this, e)) {
133            return true;
134        }
135        return super.onTouchEvent(e);
136    }
137
138    @Override
139    public void onClick(View v) {
140        if (mItemInvoker != null) {
141            mItemInvoker.invokeItem(mItemData);
142        }
143    }
144
145    public void setItemInvoker(MenuBuilder.ItemInvoker invoker) {
146        mItemInvoker = invoker;
147    }
148
149    public void setPopupCallback(PopupCallback popupCallback) {
150        mPopupCallback = popupCallback;
151    }
152
153    public boolean prefersCondensedTitle() {
154        return true;
155    }
156
157    public void setCheckable(boolean checkable) {
158        // TODO Support checkable action items
159    }
160
161    public void setChecked(boolean checked) {
162        // TODO Support checkable action items
163    }
164
165    public void setExpandedFormat(boolean expandedFormat) {
166        if (mExpandedFormat != expandedFormat) {
167            mExpandedFormat = expandedFormat;
168            if (mItemData != null) {
169                mItemData.actionFormatChanged();
170            }
171        }
172    }
173
174    private void updateTextButtonVisibility() {
175        boolean visible = !TextUtils.isEmpty(mTitle);
176        visible &= mIcon == null ||
177                (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat));
178
179        setText(visible ? mTitle : null);
180    }
181
182    public void setIcon(Drawable icon) {
183        mIcon = icon;
184        if (icon != null) {
185            int width = icon.getIntrinsicWidth();
186            int height = icon.getIntrinsicHeight();
187            if (width > mMaxIconSize) {
188                final float scale = (float) mMaxIconSize / width;
189                width = mMaxIconSize;
190                height *= scale;
191            }
192            if (height > mMaxIconSize) {
193                final float scale = (float) mMaxIconSize / height;
194                height = mMaxIconSize;
195                width *= scale;
196            }
197            icon.setBounds(0, 0, width, height);
198        }
199        setCompoundDrawables(icon, null, null, null);
200
201        updateTextButtonVisibility();
202    }
203
204    public boolean hasText() {
205        return !TextUtils.isEmpty(getText());
206    }
207
208    public void setShortcut(boolean showShortcut, char shortcutKey) {
209        // Action buttons don't show text for shortcut keys.
210    }
211
212    public void setTitle(CharSequence title) {
213        mTitle = title;
214
215        setContentDescription(mTitle);
216        updateTextButtonVisibility();
217    }
218
219    @Override
220    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
221        onPopulateAccessibilityEvent(event);
222        return true;
223    }
224
225    @Override
226    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
227        super.onPopulateAccessibilityEvent(event);
228        final CharSequence cdesc = getContentDescription();
229        if (!TextUtils.isEmpty(cdesc)) {
230            event.getText().add(cdesc);
231        }
232    }
233
234    @Override
235    public boolean dispatchHoverEvent(MotionEvent event) {
236        // Don't allow children to hover; we want this to be treated as a single component.
237        return onHoverEvent(event);
238    }
239
240    public boolean showsIcon() {
241        return true;
242    }
243
244    public boolean needsDividerBefore() {
245        return hasText() && mItemData.getIcon() == null;
246    }
247
248    public boolean needsDividerAfter() {
249        return hasText();
250    }
251
252    @Override
253    public boolean onLongClick(View v) {
254        if (hasText()) {
255            // Don't show the cheat sheet for items that already show text.
256            return false;
257        }
258
259        final int[] screenPos = new int[2];
260        final Rect displayFrame = new Rect();
261        getLocationOnScreen(screenPos);
262        getWindowVisibleDisplayFrame(displayFrame);
263
264        final Context context = getContext();
265        final int width = getWidth();
266        final int height = getHeight();
267        final int midy = screenPos[1] + height / 2;
268        int referenceX = screenPos[0] + width / 2;
269        if (v.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
270            final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
271            referenceX = screenWidth - referenceX; // mirror
272        }
273        Toast cheatSheet = Toast.makeText(context, mItemData.getTitle(), Toast.LENGTH_SHORT);
274        if (midy < displayFrame.height()) {
275            // Show along the top; follow action buttons
276            cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX, height);
277        } else {
278            // Show along the bottom center
279            cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
280        }
281        cheatSheet.show();
282        return true;
283    }
284
285    @Override
286    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
287        final boolean textVisible = hasText();
288        if (textVisible && mSavedPaddingLeft >= 0) {
289            super.setPadding(mSavedPaddingLeft, getPaddingTop(),
290                    getPaddingRight(), getPaddingBottom());
291        }
292
293        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
294
295        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
296        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
297        final int oldMeasuredWidth = getMeasuredWidth();
298        final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth)
299                : mMinWidth;
300
301        if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) {
302            // Remeasure at exactly the minimum width.
303            super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
304                    heightMeasureSpec);
305        }
306
307        if (!textVisible && mIcon != null) {
308            // TextView won't center compound drawables in both dimensions without
309            // a little coercion. Pad in to center the icon after we've measured.
310            final int w = getMeasuredWidth();
311            final int dw = mIcon.getBounds().width();
312            super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom());
313        }
314    }
315
316    private class ActionMenuItemForwardingListener extends ForwardingListener {
317        public ActionMenuItemForwardingListener() {
318            super(ActionMenuItemView.this);
319        }
320
321        @Override
322        public ListPopupWindow getPopup() {
323            if (mPopupCallback != null) {
324                return mPopupCallback.getPopup();
325            }
326            return null;
327        }
328
329        @Override
330        protected boolean onForwardingStarted() {
331            // Call the invoker, then check if the expected popup is showing.
332            if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) {
333                final ListPopupWindow popup = getPopup();
334                return popup != null && popup.isShowing();
335            }
336            return false;
337        }
338
339        @Override
340        protected boolean onForwardingStopped() {
341            final ListPopupWindow popup = getPopup();
342            if (popup != null) {
343                popup.dismiss();
344                return true;
345            }
346            return false;
347        }
348    }
349
350    public static abstract class PopupCallback {
351        public abstract ListPopupWindow getPopup();
352    }
353}
354