ActionMenuItemView.java revision 722327e27579c196b92883c07f4b47e9efada8ad
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 17package android.support.v7.view.menu; 18 19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.content.Context; 22import android.content.res.Configuration; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.drawable.Drawable; 26import android.os.Parcelable; 27import android.support.annotation.RestrictTo; 28import android.support.v7.appcompat.R; 29import android.support.v7.widget.ActionMenuView; 30import android.support.v7.widget.AppCompatTextView; 31import android.support.v7.widget.ForwardingListener; 32import android.support.v7.widget.TooltipCompat; 33import android.text.TextUtils; 34import android.util.AttributeSet; 35import android.view.MotionEvent; 36import android.view.View; 37 38/** 39 * @hide 40 */ 41@RestrictTo(LIBRARY_GROUP) 42public class ActionMenuItemView extends AppCompatTextView 43 implements MenuView.ItemView, View.OnClickListener, ActionMenuView.ActionMenuChildView { 44 45 private static final String TAG = "ActionMenuItemView"; 46 47 MenuItemImpl mItemData; 48 private CharSequence mTitle; 49 private Drawable mIcon; 50 MenuBuilder.ItemInvoker mItemInvoker; 51 private ForwardingListener mForwardingListener; 52 PopupCallback mPopupCallback; 53 54 private boolean mAllowTextWithIcon; 55 private boolean mExpandedFormat; 56 private int mMinWidth; 57 private int mSavedPaddingLeft; 58 59 private static final int MAX_ICON_SIZE = 32; // dp 60 private int mMaxIconSize; 61 62 public ActionMenuItemView(Context context) { 63 this(context, null); 64 } 65 66 public ActionMenuItemView(Context context, AttributeSet attrs) { 67 this(context, attrs, 0); 68 } 69 70 public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) { 71 super(context, attrs, defStyle); 72 final Resources res = context.getResources(); 73 mAllowTextWithIcon = shouldAllowTextWithIcon(); 74 TypedArray a = context.obtainStyledAttributes(attrs, 75 R.styleable.ActionMenuItemView, defStyle, 0); 76 mMinWidth = a.getDimensionPixelSize( 77 R.styleable.ActionMenuItemView_android_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 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 */ 101 private boolean shouldAllowTextWithIcon() { 102 final Configuration config = getContext().getResources().getConfiguration(); 103 final int widthDp = config.screenWidthDp; 104 final int heightDp = config.screenHeightDp; 105 106 return widthDp >= 480 || (widthDp >= 640 && heightDp >= 480) 107 || config.orientation == Configuration.ORIENTATION_LANDSCAPE; 108 } 109 110 @Override 111 public void setPadding(int l, int t, int r, int b) { 112 mSavedPaddingLeft = l; 113 super.setPadding(l, t, r, b); 114 } 115 116 @Override 117 public MenuItemImpl getItemData() { 118 return mItemData; 119 } 120 121 @Override 122 public void initialize(MenuItemImpl itemData, int menuType) { 123 mItemData = itemData; 124 125 setIcon(itemData.getIcon()); 126 setTitle(itemData.getTitleForItemView(this)); // Title only takes effect if there is no icon 127 setId(itemData.getItemId()); 128 129 setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE); 130 setEnabled(itemData.isEnabled()); 131 if (itemData.hasSubMenu()) { 132 if (mForwardingListener == null) { 133 mForwardingListener = new ActionMenuItemForwardingListener(); 134 } 135 } 136 } 137 138 @Override 139 public boolean onTouchEvent(MotionEvent e) { 140 if (mItemData.hasSubMenu() && mForwardingListener != null 141 && mForwardingListener.onTouch(this, e)) { 142 return true; 143 } 144 return super.onTouchEvent(e); 145 } 146 147 @Override 148 public void onClick(View v) { 149 if (mItemInvoker != null) { 150 mItemInvoker.invokeItem(mItemData); 151 } 152 } 153 154 public void setItemInvoker(MenuBuilder.ItemInvoker invoker) { 155 mItemInvoker = invoker; 156 } 157 158 public void setPopupCallback(PopupCallback popupCallback) { 159 mPopupCallback = popupCallback; 160 } 161 162 @Override 163 public boolean prefersCondensedTitle() { 164 return true; 165 } 166 167 @Override 168 public void setCheckable(boolean checkable) { 169 // TODO Support checkable action items 170 } 171 172 @Override 173 public void setChecked(boolean checked) { 174 // TODO Support checkable action items 175 } 176 177 public void setExpandedFormat(boolean expandedFormat) { 178 if (mExpandedFormat != expandedFormat) { 179 mExpandedFormat = expandedFormat; 180 if (mItemData != null) { 181 mItemData.actionFormatChanged(); 182 } 183 } 184 } 185 186 private void updateTextButtonVisibility() { 187 boolean visible = !TextUtils.isEmpty(mTitle); 188 visible &= mIcon == null || 189 (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat)); 190 191 setText(visible ? mTitle : null); 192 193 // Show the tooltip for items that do not already show text. 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 TooltipCompat.setTooltipText(this, visible ? null : mItemData.getTitle()); 207 } else { 208 TooltipCompat.setTooltipText(this, tooltipText); 209 } 210 } 211 212 @Override 213 public void setIcon(Drawable icon) { 214 mIcon = icon; 215 if (icon != null) { 216 int width = icon.getIntrinsicWidth(); 217 int height = icon.getIntrinsicHeight(); 218 if (width > mMaxIconSize) { 219 final float scale = (float) mMaxIconSize / width; 220 width = mMaxIconSize; 221 height *= scale; 222 } 223 if (height > mMaxIconSize) { 224 final float scale = (float) mMaxIconSize / height; 225 height = mMaxIconSize; 226 width *= scale; 227 } 228 icon.setBounds(0, 0, width, height); 229 } 230 setCompoundDrawables(icon, null, null, null); 231 232 updateTextButtonVisibility(); 233 } 234 235 public boolean hasText() { 236 return !TextUtils.isEmpty(getText()); 237 } 238 239 @Override 240 public void setShortcut(boolean showShortcut, char shortcutKey) { 241 // Action buttons don't show text for shortcut keys. 242 } 243 244 @Override 245 public void setTitle(CharSequence title) { 246 mTitle = title; 247 248 updateTextButtonVisibility(); 249 } 250 251 @Override 252 public boolean showsIcon() { 253 return true; 254 } 255 256 @Override 257 public boolean needsDividerBefore() { 258 return hasText() && mItemData.getIcon() == null; 259 } 260 261 @Override 262 public boolean needsDividerAfter() { 263 return hasText(); 264 } 265 266 @Override 267 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 268 final boolean textVisible = hasText(); 269 if (textVisible && mSavedPaddingLeft >= 0) { 270 super.setPadding(mSavedPaddingLeft, getPaddingTop(), 271 getPaddingRight(), getPaddingBottom()); 272 } 273 274 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 275 276 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 277 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 278 final int oldMeasuredWidth = getMeasuredWidth(); 279 final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth) 280 : mMinWidth; 281 282 if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) { 283 // Remeasure at exactly the minimum width. 284 super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY), 285 heightMeasureSpec); 286 } 287 288 if (!textVisible && mIcon != null) { 289 // TextView won't center compound drawables in both dimensions without 290 // a little coercion. Pad in to center the icon after we've measured. 291 final int w = getMeasuredWidth(); 292 final int dw = mIcon.getBounds().width(); 293 super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom()); 294 } 295 } 296 297 private class ActionMenuItemForwardingListener extends ForwardingListener { 298 public ActionMenuItemForwardingListener() { 299 super(ActionMenuItemView.this); 300 } 301 302 @Override 303 public ShowableListMenu getPopup() { 304 if (mPopupCallback != null) { 305 return mPopupCallback.getPopup(); 306 } 307 return null; 308 } 309 310 @Override 311 protected boolean onForwardingStarted() { 312 // Call the invoker, then check if the expected popup is showing. 313 if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) { 314 final ShowableListMenu popup = getPopup(); 315 return popup != null && popup.isShowing(); 316 } 317 return false; 318 } 319 320 // Do not backport the framework impl here. 321 // The framework's ListPopupWindow uses an animation before performing the item click 322 // after selecting an item. As AppCompat doesn't use an animation, the popup is 323 // dismissed and thus null'ed out before onForwardingStopped() has been called. 324 // This messes up ActionMenuItemView's onForwardingStopped() impl since it will now 325 // return false and make ListPopupWindow think it's still forwarding. 326 } 327 328 @Override 329 public void onRestoreInstanceState(Parcelable state) { 330 // This might get called with the state of ActionView since it shares the same ID with 331 // ActionMenuItemView. Do not restore this state as ActionMenuItemView never saved it. 332 super.onRestoreInstanceState(null); 333 } 334 335 public static abstract class PopupCallback { 336 public abstract ShowableListMenu getPopup(); 337 } 338} 339