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