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.internal.view.menu; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.graphics.Rect; 24import android.graphics.drawable.Drawable; 25import android.os.Build; 26import android.support.v4.view.GravityCompat; 27import android.support.v4.view.ViewCompat; 28import android.support.v7.appcompat.R; 29import android.support.v7.internal.text.AllCapsTransformationMethod; 30import android.support.v7.internal.widget.CompatTextView; 31import android.support.v7.widget.ActionMenuView; 32import android.support.v7.widget.ListPopupWindow; 33import android.text.TextUtils; 34import android.text.method.TransformationMethod; 35import android.util.AttributeSet; 36import android.view.Gravity; 37import android.view.MotionEvent; 38import android.view.View; 39import android.widget.Toast; 40 41import java.util.Locale; 42 43/** 44 * @hide 45 */ 46public class ActionMenuItemView extends CompatTextView 47 implements MenuView.ItemView, View.OnClickListener, View.OnLongClickListener, 48 ActionMenuView.ActionMenuChildView { 49 50 private static final String TAG = "ActionMenuItemView"; 51 52 private MenuItemImpl mItemData; 53 private CharSequence mTitle; 54 private Drawable mIcon; 55 private MenuBuilder.ItemInvoker mItemInvoker; 56 private ListPopupWindow.ForwardingListener mForwardingListener; 57 private PopupCallback mPopupCallback; 58 59 private boolean mAllowTextWithIcon; 60 private boolean mExpandedFormat; 61 private int mMinWidth; 62 private int mSavedPaddingLeft; 63 64 private static final int MAX_ICON_SIZE = 32; // dp 65 private int mMaxIconSize; 66 67 public ActionMenuItemView(Context context) { 68 this(context, null); 69 } 70 71 public ActionMenuItemView(Context context, AttributeSet attrs) { 72 this(context, attrs, 0); 73 } 74 75 public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) { 76 super(context, attrs, defStyle); 77 final Resources res = context.getResources(); 78 mAllowTextWithIcon = res.getBoolean( 79 R.bool.abc_config_allowActionMenuItemTextWithIcon); 80 TypedArray a = context.obtainStyledAttributes(attrs, 81 R.styleable.ActionMenuItemView, defStyle, 0); 82 mMinWidth = a.getDimensionPixelSize( 83 R.styleable.ActionMenuItemView_android_minWidth, 0); 84 a.recycle(); 85 86 final float density = res.getDisplayMetrics().density; 87 mMaxIconSize = (int) (MAX_ICON_SIZE * density + 0.5f); 88 89 setOnClickListener(this); 90 setOnLongClickListener(this); 91 92 setTransformationMethod(new AllCapsTransformationMethod(context)); 93 94 mSavedPaddingLeft = -1; 95 } 96 97 public void onConfigurationChanged(Configuration newConfig) { 98 if (Build.VERSION.SDK_INT >= 8) { 99 super.onConfigurationChanged(newConfig); 100 } 101 102 mAllowTextWithIcon = getContext().getResources().getBoolean( 103 R.bool.abc_config_allowActionMenuItemTextWithIcon); 104 updateTextButtonVisibility(); 105 } 106 107 @Override 108 public void setPadding(int l, int t, int r, int b) { 109 mSavedPaddingLeft = l; 110 super.setPadding(l, t, r, b); 111 } 112 113 public MenuItemImpl getItemData() { 114 return mItemData; 115 } 116 117 public void initialize(MenuItemImpl itemData, int menuType) { 118 mItemData = itemData; 119 120 setIcon(itemData.getIcon()); 121 setTitle(itemData.getTitleForItemView(this)); // Title only takes effect if there is no icon 122 setId(itemData.getItemId()); 123 124 setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE); 125 setEnabled(itemData.isEnabled()); 126 if (itemData.hasSubMenu()) { 127 if (mForwardingListener == null) { 128 mForwardingListener = new ActionMenuItemForwardingListener(); 129 } 130 } 131 } 132 133 @Override 134 public boolean onTouchEvent(MotionEvent e) { 135 if (mItemData.hasSubMenu() && mForwardingListener != null 136 && mForwardingListener.onTouch(this, e)) { 137 return true; 138 } 139 return super.onTouchEvent(e); 140 } 141 142 @Override 143 public void onClick(View v) { 144 if (mItemInvoker != null) { 145 mItemInvoker.invokeItem(mItemData); 146 } 147 } 148 149 public void setItemInvoker(MenuBuilder.ItemInvoker invoker) { 150 mItemInvoker = invoker; 151 } 152 153 public void setPopupCallback(PopupCallback popupCallback) { 154 mPopupCallback = popupCallback; 155 } 156 157 public boolean prefersCondensedTitle() { 158 return true; 159 } 160 161 public void setCheckable(boolean checkable) { 162 // TODO Support checkable action items 163 } 164 165 public void setChecked(boolean checked) { 166 // TODO Support checkable action items 167 } 168 169 public void setExpandedFormat(boolean expandedFormat) { 170 if (mExpandedFormat != expandedFormat) { 171 mExpandedFormat = expandedFormat; 172 if (mItemData != null) { 173 mItemData.actionFormatChanged(); 174 } 175 } 176 } 177 178 private void updateTextButtonVisibility() { 179 boolean visible = !TextUtils.isEmpty(mTitle); 180 visible &= mIcon == null || 181 (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat)); 182 183 setText(visible ? mTitle : null); 184 } 185 186 public void setIcon(Drawable icon) { 187 mIcon = icon; 188 if (icon != null) { 189 int width = icon.getIntrinsicWidth(); 190 int height = icon.getIntrinsicHeight(); 191 if (width > mMaxIconSize) { 192 final float scale = (float) mMaxIconSize / width; 193 width = mMaxIconSize; 194 height *= scale; 195 } 196 if (height > mMaxIconSize) { 197 final float scale = (float) mMaxIconSize / height; 198 height = mMaxIconSize; 199 width *= scale; 200 } 201 icon.setBounds(0, 0, width, height); 202 } 203 setCompoundDrawables(icon, null, null, null); 204 205 updateTextButtonVisibility(); 206 } 207 208 public boolean hasText() { 209 return !TextUtils.isEmpty(getText()); 210 } 211 212 public void setShortcut(boolean showShortcut, char shortcutKey) { 213 // Action buttons don't show text for shortcut keys. 214 } 215 216 public void setTitle(CharSequence title) { 217 mTitle = title; 218 219 setContentDescription(mTitle); 220 updateTextButtonVisibility(); 221 } 222 223 public boolean showsIcon() { 224 return true; 225 } 226 227 public boolean needsDividerBefore() { 228 return hasText() && mItemData.getIcon() == null; 229 } 230 231 public boolean needsDividerAfter() { 232 return hasText(); 233 } 234 235 @Override 236 public boolean onLongClick(View v) { 237 if (hasText()) { 238 // Don't show the cheat sheet for items that already show text. 239 return false; 240 } 241 242 final int[] screenPos = new int[2]; 243 final Rect displayFrame = new Rect(); 244 getLocationOnScreen(screenPos); 245 getWindowVisibleDisplayFrame(displayFrame); 246 247 final Context context = getContext(); 248 final int width = getWidth(); 249 final int height = getHeight(); 250 final int midy = screenPos[1] + height / 2; 251 int referenceX = screenPos[0] + width / 2; 252 if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { 253 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 254 referenceX = screenWidth - referenceX; // mirror 255 } 256 Toast cheatSheet = Toast.makeText(context, mItemData.getTitle(), Toast.LENGTH_SHORT); 257 if (midy < displayFrame.height()) { 258 // Show along the top; follow action buttons 259 cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX, height); 260 } else { 261 // Show along the bottom center 262 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); 263 } 264 cheatSheet.show(); 265 return true; 266 } 267 268 @Override 269 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 270 final boolean textVisible = hasText(); 271 if (textVisible && mSavedPaddingLeft >= 0) { 272 super.setPadding(mSavedPaddingLeft, getPaddingTop(), 273 getPaddingRight(), getPaddingBottom()); 274 } 275 276 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 277 278 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 279 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 280 final int oldMeasuredWidth = getMeasuredWidth(); 281 final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth) 282 : mMinWidth; 283 284 if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) { 285 // Remeasure at exactly the minimum width. 286 super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY), 287 heightMeasureSpec); 288 } 289 290 if (!textVisible && mIcon != null) { 291 // TextView won't center compound drawables in both dimensions without 292 // a little coercion. Pad in to center the icon after we've measured. 293 final int w = getMeasuredWidth(); 294 final int dw = mIcon.getBounds().width(); 295 super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom()); 296 } 297 } 298 299 private class ActionMenuItemForwardingListener extends ListPopupWindow.ForwardingListener { 300 public ActionMenuItemForwardingListener() { 301 super(ActionMenuItemView.this); 302 } 303 304 @Override 305 public ListPopupWindow getPopup() { 306 if (mPopupCallback != null) { 307 return mPopupCallback.getPopup(); 308 } 309 return null; 310 } 311 312 @Override 313 protected boolean onForwardingStarted() { 314 // Call the invoker, then check if the expected popup is showing. 315 if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) { 316 final ListPopupWindow popup = getPopup(); 317 return popup != null && popup.isShowing(); 318 } 319 return false; 320 } 321 322 @Override 323 protected boolean onForwardingStopped() { 324 final ListPopupWindow popup = getPopup(); 325 if (popup != null) { 326 popup.dismiss(); 327 return true; 328 } 329 return false; 330 } 331 } 332 333 public static abstract class PopupCallback { 334 public abstract ListPopupWindow getPopup(); 335 } 336} 337