1 /*
2  * Copyright (C) 2006 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.TypedArray;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.text.Layout;
25 import android.text.TextUtils;
26 import android.util.AttributeSet;
27 import android.view.Gravity;
28 import android.view.SoundEffectConstants;
29 import android.view.View;
30 import android.view.ViewDebug;
31 import android.widget.TextView;
32 
33 import com.android.internal.view.menu.MenuBuilder.ItemInvoker;
34 
35 /**
36  * The item view for each item in the {@link IconMenuView}.
37  */
38 public final class IconMenuItemView extends TextView implements MenuView.ItemView {
39 
40     private static final int NO_ALPHA = 0xFF;
41 
42     private IconMenuView mIconMenuView;
43 
44     private ItemInvoker mItemInvoker;
45     private MenuItemImpl mItemData;
46 
47     private Drawable mIcon;
48 
49     private int mTextAppearance;
50     private Context mTextAppearanceContext;
51 
52     private float mDisabledAlpha;
53 
54     private Rect mPositionIconAvailable = new Rect();
55     private Rect mPositionIconOutput = new Rect();
56 
57     private boolean mShortcutCaptionMode;
58     private String mShortcutCaption;
59 
60     private static String sPrependShortcutLabel;
61 
IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)62     public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
63         super(context, attrs, defStyleAttr, defStyleRes);
64 
65         if (sPrependShortcutLabel == null) {
66             /*
67              * Views should only be constructed from the UI thread, so no
68              * synchronization needed
69              */
70             sPrependShortcutLabel = getResources().getString(
71                     com.android.internal.R.string.prepend_shortcut_label);
72         }
73 
74         final TypedArray a = context.obtainStyledAttributes(
75                 attrs, com.android.internal.R.styleable.MenuView, defStyleAttr, defStyleRes);
76 
77         mDisabledAlpha = a.getFloat(
78                 com.android.internal.R.styleable.MenuView_itemIconDisabledAlpha, 0.8f);
79         mTextAppearance = a.getResourceId(com.android.internal.R.styleable.
80                                           MenuView_itemTextAppearance, -1);
81         mTextAppearanceContext = context;
82 
83         a.recycle();
84     }
85 
IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr)86     public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
87         this(context, attrs, defStyleAttr, 0);
88     }
89 
IconMenuItemView(Context context, AttributeSet attrs)90     public IconMenuItemView(Context context, AttributeSet attrs) {
91         this(context, attrs, 0);
92     }
93 
94     /**
95      * Initializes with the provided title and icon
96      * @param title The title of this item
97      * @param icon The icon of this item
98      */
initialize(CharSequence title, Drawable icon)99     void initialize(CharSequence title, Drawable icon) {
100         setClickable(true);
101         setFocusable(true);
102 
103         if (mTextAppearance != -1) {
104             setTextAppearance(mTextAppearanceContext, mTextAppearance);
105         }
106 
107         setTitle(title);
108         setIcon(icon);
109 
110         if (mItemData != null) {
111             final CharSequence contentDescription = mItemData.getContentDescription();
112             if (TextUtils.isEmpty(contentDescription)) {
113                 setContentDescription(title);
114             } else {
115                 setContentDescription(contentDescription);
116             }
117             setTooltipText(mItemData.getTooltipText());
118         }
119     }
120 
initialize(MenuItemImpl itemData, int menuType)121     public void initialize(MenuItemImpl itemData, int menuType) {
122         mItemData = itemData;
123 
124         initialize(itemData.getTitleForItemView(this), itemData.getIcon());
125 
126         setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
127         setEnabled(itemData.isEnabled());
128     }
129 
setItemData(MenuItemImpl data)130     public void setItemData(MenuItemImpl data) {
131         mItemData = data;
132     }
133 
134     @Override
performClick()135     public boolean performClick() {
136         // Let the view's click listener have top priority (the More button relies on this)
137         if (super.performClick()) {
138             return true;
139         }
140 
141         if ((mItemInvoker != null) && (mItemInvoker.invokeItem(mItemData))) {
142             playSoundEffect(SoundEffectConstants.CLICK);
143             return true;
144         } else {
145             return false;
146         }
147     }
148 
setTitle(CharSequence title)149     public void setTitle(CharSequence title) {
150 
151         if (mShortcutCaptionMode) {
152             /*
153              * Don't set the title directly since it will replace the
154              * shortcut+title being shown. Instead, re-set the shortcut caption
155              * mode so the new title is shown.
156              */
157             setCaptionMode(true);
158 
159         } else if (title != null) {
160             setText(title);
161         }
162     }
163 
setCaptionMode(boolean shortcut)164     void setCaptionMode(boolean shortcut) {
165         /*
166          * If there is no item model, don't do any of the below (for example,
167          * the 'More' item doesn't have a model)
168          */
169         if (mItemData == null) {
170             return;
171         }
172 
173         mShortcutCaptionMode = shortcut && (mItemData.shouldShowShortcut());
174 
175         CharSequence text = mItemData.getTitleForItemView(this);
176 
177         if (mShortcutCaptionMode) {
178 
179             if (mShortcutCaption == null) {
180                 mShortcutCaption = mItemData.getShortcutLabel();
181             }
182 
183             text = mShortcutCaption;
184         }
185 
186         setText(text);
187     }
188 
setIcon(Drawable icon)189     public void setIcon(Drawable icon) {
190         mIcon = icon;
191 
192         if (icon != null) {
193 
194             /* Set the bounds of the icon since setCompoundDrawables needs it. */
195             icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
196 
197             // Set the compound drawables
198             setCompoundDrawables(null, icon, null, null);
199 
200             // When there is an icon, make sure the text is at the bottom
201             setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
202 
203             /*
204              * Request a layout to reposition the icon. The positioning of icon
205              * depends on this TextView's line bounds, which is only available
206              * after a layout.
207              */
208             requestLayout();
209         } else {
210             setCompoundDrawables(null, null, null, null);
211 
212             // When there is no icon, make sure the text is centered vertically
213             setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
214         }
215     }
216 
217     @UnsupportedAppUsage
setItemInvoker(ItemInvoker itemInvoker)218     public void setItemInvoker(ItemInvoker itemInvoker) {
219         mItemInvoker = itemInvoker;
220     }
221 
222     @ViewDebug.CapturedViewProperty(retrieveReturn = true)
getItemData()223     public MenuItemImpl getItemData() {
224         return mItemData;
225     }
226 
227     @Override
setVisibility(int v)228     public void setVisibility(int v) {
229         super.setVisibility(v);
230 
231         if (mIconMenuView != null) {
232             // On visibility change, mark the IconMenuView to refresh itself eventually
233             mIconMenuView.markStaleChildren();
234         }
235     }
236 
237     @UnsupportedAppUsage
setIconMenuView(IconMenuView iconMenuView)238     void setIconMenuView(IconMenuView iconMenuView) {
239         mIconMenuView = iconMenuView;
240     }
241 
242     @Override
drawableStateChanged()243     protected void drawableStateChanged() {
244         super.drawableStateChanged();
245 
246         if (mItemData != null && mIcon != null) {
247             // When disabled, the not-focused state and the pressed state should
248             // drop alpha on the icon
249             final boolean isInAlphaState = !mItemData.isEnabled() && (isPressed() || !isFocused());
250             mIcon.setAlpha(isInAlphaState ? (int) (mDisabledAlpha * NO_ALPHA) : NO_ALPHA);
251         }
252     }
253 
254     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)255     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
256         super.onLayout(changed, left, top, right, bottom);
257 
258         positionIcon();
259     }
260 
261     @Override
onTextChanged(CharSequence text, int start, int before, int after)262     protected void onTextChanged(CharSequence text, int start, int before, int after) {
263         super.onTextChanged(text, start, before, after);
264 
265         // our layout params depend on the length of the text
266         setLayoutParams(getTextAppropriateLayoutParams());
267     }
268 
269     /**
270      * @return layout params appropriate for this view.  If layout params already exist, it will
271      *         augment them to be appropriate to the current text size.
272      */
273     @UnsupportedAppUsage
getTextAppropriateLayoutParams()274     IconMenuView.LayoutParams getTextAppropriateLayoutParams() {
275         IconMenuView.LayoutParams lp = (IconMenuView.LayoutParams) getLayoutParams();
276         if (lp == null) {
277             // Default layout parameters
278             lp = new IconMenuView.LayoutParams(
279                     IconMenuView.LayoutParams.MATCH_PARENT, IconMenuView.LayoutParams.MATCH_PARENT);
280         }
281 
282         // Set the desired width of item
283         lp.desiredWidth = (int) Layout.getDesiredWidth(getText(), 0, getText().length(),
284                 getPaint(), getTextDirectionHeuristic());
285 
286         return lp;
287     }
288 
289     /**
290      * Positions the icon vertically (horizontal centering is taken care of by
291      * the TextView's gravity).
292      */
positionIcon()293     private void positionIcon() {
294 
295         if (mIcon == null) {
296             return;
297         }
298 
299         // We reuse the output rectangle as a temp rect
300         Rect tmpRect = mPositionIconOutput;
301         getLineBounds(0, tmpRect);
302         mPositionIconAvailable.set(0, 0, getWidth(), tmpRect.top);
303         final int layoutDirection = getLayoutDirection();
304         Gravity.apply(Gravity.CENTER_VERTICAL | Gravity.START, mIcon.getIntrinsicWidth(), mIcon
305                 .getIntrinsicHeight(), mPositionIconAvailable, mPositionIconOutput,
306                 layoutDirection);
307         mIcon.setBounds(mPositionIconOutput);
308     }
309 
setCheckable(boolean checkable)310     public void setCheckable(boolean checkable) {
311     }
312 
setChecked(boolean checked)313     public void setChecked(boolean checked) {
314     }
315 
setShortcut(boolean showShortcut, char shortcutKey)316     public void setShortcut(boolean showShortcut, char shortcutKey) {
317 
318         if (mShortcutCaptionMode) {
319             /*
320              * Shortcut has changed and we're showing it right now, need to
321              * update (clear the old one first).
322              */
323             mShortcutCaption = null;
324             setCaptionMode(true);
325         }
326     }
327 
prefersCondensedTitle()328     public boolean prefersCondensedTitle() {
329         return true;
330     }
331 
showsIcon()332     public boolean showsIcon() {
333         return true;
334     }
335 
336 }
337