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