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