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