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