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