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