Home | History | Annotate | Download | only in menu
      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