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