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