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 android.support.v7.view.menu;
     18 
     19 import android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Rect;
     24 import android.graphics.drawable.Drawable;
     25 import android.os.Build;
     26 import android.os.Parcelable;
     27 import android.support.v4.content.res.ConfigurationHelper;
     28 import android.support.v4.view.GravityCompat;
     29 import android.support.v4.view.ViewCompat;
     30 import android.support.v7.appcompat.R;
     31 import android.support.v7.widget.ActionMenuView;
     32 import android.support.v7.widget.AppCompatTextView;
     33 import android.support.v7.widget.ForwardingListener;
     34 import android.support.v7.widget.ListPopupWindow;
     35 import android.text.TextUtils;
     36 import android.util.AttributeSet;
     37 import android.util.DisplayMetrics;
     38 import android.view.Gravity;
     39 import android.view.MotionEvent;
     40 import android.view.View;
     41 import android.widget.Toast;
     42 
     43 /**
     44  * @hide
     45  */
     46 public class ActionMenuItemView extends AppCompatTextView
     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 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 = shouldAllowTextWithIcon();
     79         TypedArray a = context.obtainStyledAttributes(attrs,
     80                 R.styleable.ActionMenuItemView, defStyle, 0);
     81         mMinWidth = a.getDimensionPixelSize(
     82                 R.styleable.ActionMenuItemView_android_minWidth, 0);
     83         a.recycle();
     84 
     85         final float density = res.getDisplayMetrics().density;
     86         mMaxIconSize = (int) (MAX_ICON_SIZE * density + 0.5f);
     87 
     88         setOnClickListener(this);
     89         setOnLongClickListener(this);
     90 
     91         mSavedPaddingLeft = -1;
     92         setSaveEnabled(false);
     93     }
     94 
     95     public void onConfigurationChanged(Configuration newConfig) {
     96         if (Build.VERSION.SDK_INT >= 8) {
     97             super.onConfigurationChanged(newConfig);
     98         }
     99 
    100         mAllowTextWithIcon = shouldAllowTextWithIcon();
    101         updateTextButtonVisibility();
    102     }
    103 
    104     /**
    105      * Whether action menu items should obey the "withText" showAsAction flag. This may be set to
    106      * false for situations where space is extremely limited. -->
    107      */
    108     private boolean shouldAllowTextWithIcon() {
    109         final Configuration config = getContext().getResources().getConfiguration();
    110         final int widthDp = ConfigurationHelper.getScreenWidthDp(getResources());
    111         final int heightDp = ConfigurationHelper.getScreenHeightDp(getResources());
    112 
    113         return widthDp >= 480 || (widthDp >= 640 && heightDp >= 480)
    114                 || config.orientation == Configuration.ORIENTATION_LANDSCAPE;
    115     }
    116 
    117     @Override
    118     public void setPadding(int l, int t, int r, int b) {
    119         mSavedPaddingLeft = l;
    120         super.setPadding(l, t, r, b);
    121     }
    122 
    123     public MenuItemImpl getItemData() {
    124         return mItemData;
    125     }
    126 
    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
    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
    153     public void onClick(View v) {
    154         if (mItemInvoker != null) {
    155             mItemInvoker.invokeItem(mItemData);
    156         }
    157     }
    158 
    159     public void setItemInvoker(MenuBuilder.ItemInvoker invoker) {
    160         mItemInvoker = invoker;
    161     }
    162 
    163     public void setPopupCallback(PopupCallback popupCallback) {
    164         mPopupCallback = popupCallback;
    165     }
    166 
    167     public boolean prefersCondensedTitle() {
    168         return true;
    169     }
    170 
    171     public void setCheckable(boolean checkable) {
    172         // TODO Support checkable action items
    173     }
    174 
    175     public void setChecked(boolean checked) {
    176         // TODO Support checkable action items
    177     }
    178 
    179     public void setExpandedFormat(boolean expandedFormat) {
    180         if (mExpandedFormat != expandedFormat) {
    181             mExpandedFormat = expandedFormat;
    182             if (mItemData != null) {
    183                 mItemData.actionFormatChanged();
    184             }
    185         }
    186     }
    187 
    188     private void updateTextButtonVisibility() {
    189         boolean visible = !TextUtils.isEmpty(mTitle);
    190         visible &= mIcon == null ||
    191                 (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat));
    192 
    193         setText(visible ? mTitle : null);
    194     }
    195 
    196     public void setIcon(Drawable icon) {
    197         mIcon = icon;
    198         if (icon != null) {
    199             int width = icon.getIntrinsicWidth();
    200             int height = icon.getIntrinsicHeight();
    201             if (width > mMaxIconSize) {
    202                 final float scale = (float) mMaxIconSize / width;
    203                 width = mMaxIconSize;
    204                 height *= scale;
    205             }
    206             if (height > mMaxIconSize) {
    207                 final float scale = (float) mMaxIconSize / height;
    208                 height = mMaxIconSize;
    209                 width *= scale;
    210             }
    211             icon.setBounds(0, 0, width, height);
    212         }
    213         setCompoundDrawables(icon, null, null, null);
    214 
    215         updateTextButtonVisibility();
    216     }
    217 
    218     public boolean hasText() {
    219         return !TextUtils.isEmpty(getText());
    220     }
    221 
    222     public void setShortcut(boolean showShortcut, char shortcutKey) {
    223         // Action buttons don't show text for shortcut keys.
    224     }
    225 
    226     public void setTitle(CharSequence title) {
    227         mTitle = title;
    228 
    229         setContentDescription(mTitle);
    230         updateTextButtonVisibility();
    231     }
    232 
    233     public boolean showsIcon() {
    234         return true;
    235     }
    236 
    237     public boolean needsDividerBefore() {
    238         return hasText() && mItemData.getIcon() == null;
    239     }
    240 
    241     public boolean needsDividerAfter() {
    242         return hasText();
    243     }
    244 
    245     @Override
    246     public boolean onLongClick(View v) {
    247         if (hasText()) {
    248             // Don't show the cheat sheet for items that already show text.
    249             return false;
    250         }
    251 
    252         final int[] screenPos = new int[2];
    253         final Rect displayFrame = new Rect();
    254         getLocationOnScreen(screenPos);
    255         getWindowVisibleDisplayFrame(displayFrame);
    256 
    257         final Context context = getContext();
    258         final int width = getWidth();
    259         final int height = getHeight();
    260         final int midy = screenPos[1] + height / 2;
    261         int referenceX = screenPos[0] + width / 2;
    262         if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
    263             final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
    264             referenceX = screenWidth - referenceX; // mirror
    265         }
    266         Toast cheatSheet = Toast.makeText(context, mItemData.getTitle(), Toast.LENGTH_SHORT);
    267         if (midy < displayFrame.height()) {
    268             // Show along the top; follow action buttons
    269             cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX,
    270                     screenPos[1] + height - displayFrame.top);
    271         } else {
    272             // Show along the bottom center
    273             cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
    274         }
    275         cheatSheet.show();
    276         return true;
    277     }
    278 
    279     @Override
    280     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    281         final boolean textVisible = hasText();
    282         if (textVisible && mSavedPaddingLeft >= 0) {
    283             super.setPadding(mSavedPaddingLeft, getPaddingTop(),
    284                     getPaddingRight(), getPaddingBottom());
    285         }
    286 
    287         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    288 
    289         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    290         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    291         final int oldMeasuredWidth = getMeasuredWidth();
    292         final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth)
    293                 : mMinWidth;
    294 
    295         if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) {
    296             // Remeasure at exactly the minimum width.
    297             super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
    298                     heightMeasureSpec);
    299         }
    300 
    301         if (!textVisible && mIcon != null) {
    302             // TextView won't center compound drawables in both dimensions without
    303             // a little coercion. Pad in to center the icon after we've measured.
    304             final int w = getMeasuredWidth();
    305             final int dw = mIcon.getBounds().width();
    306             super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom());
    307         }
    308     }
    309 
    310     private class ActionMenuItemForwardingListener extends ForwardingListener {
    311         public ActionMenuItemForwardingListener() {
    312             super(ActionMenuItemView.this);
    313         }
    314 
    315         @Override
    316         public ShowableListMenu getPopup() {
    317             if (mPopupCallback != null) {
    318                 return mPopupCallback.getPopup();
    319             }
    320             return null;
    321         }
    322 
    323         @Override
    324         protected boolean onForwardingStarted() {
    325             // Call the invoker, then check if the expected popup is showing.
    326             if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) {
    327                 final ShowableListMenu popup = getPopup();
    328                 return popup != null && popup.isShowing();
    329             }
    330             return false;
    331         }
    332 
    333         // Do not backport the framework impl here.
    334         // The framework's ListPopupWindow uses an animation before performing the item click
    335         // after selecting an item. As AppCompat doesn't use an animation, the popup is
    336         // dismissed and thus null'ed out before onForwardingStopped() has been called.
    337         // This messes up ActionMenuItemView's onForwardingStopped() impl since it will now
    338         // return false and make ListPopupWindow think it's still forwarding.
    339     }
    340 
    341     @Override
    342     public void onRestoreInstanceState(Parcelable state) {
    343         // This might get called with the state of ActionView since it shares the same ID with
    344         // ActionMenuItemView. Do not restore this state as ActionMenuItemView never saved it.
    345         super.onRestoreInstanceState(null);
    346     }
    347 
    348     public static abstract class PopupCallback {
    349         public abstract ShowableListMenu getPopup();
    350     }
    351 }
    352