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