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