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