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 package com.android.internal.view.menu;
     17 
     18 import android.content.Context;
     19 import android.content.res.Configuration;
     20 import android.content.res.TypedArray;
     21 import android.util.AttributeSet;
     22 import android.view.Gravity;
     23 import android.view.View;
     24 import android.view.ViewDebug;
     25 import android.view.ViewGroup;
     26 import android.view.accessibility.AccessibilityEvent;
     27 import android.widget.LinearLayout;
     28 
     29 import com.android.internal.R;
     30 
     31 /**
     32  * @hide
     33  */
     34 public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
     35     private static final String TAG = "ActionMenuView";
     36 
     37     static final int MIN_CELL_SIZE = 56; // dips
     38     static final int GENERATED_ITEM_PADDING = 4; // dips
     39 
     40     private MenuBuilder mMenu;
     41 
     42     private boolean mReserveOverflow;
     43     private ActionMenuPresenter mPresenter;
     44     private boolean mFormatItems;
     45     private int mFormatItemsWidth;
     46     private int mMinCellSize;
     47     private int mGeneratedItemPadding;
     48     private int mMeasuredExtraWidth;
     49     private int mMaxItemHeight;
     50 
     51     public ActionMenuView(Context context) {
     52         this(context, null);
     53     }
     54 
     55     public ActionMenuView(Context context, AttributeSet attrs) {
     56         super(context, attrs);
     57         setBaselineAligned(false);
     58         final float density = context.getResources().getDisplayMetrics().density;
     59         mMinCellSize = (int) (MIN_CELL_SIZE * density);
     60         mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density);
     61 
     62         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
     63                 R.attr.actionBarStyle, 0);
     64         mMaxItemHeight = a.getDimensionPixelSize(R.styleable.ActionBar_height, 0);
     65         a.recycle();
     66     }
     67 
     68     public void setPresenter(ActionMenuPresenter presenter) {
     69         mPresenter = presenter;
     70     }
     71 
     72     public boolean isExpandedFormat() {
     73         return mFormatItems;
     74     }
     75 
     76     @Override
     77     public void onConfigurationChanged(Configuration newConfig) {
     78         super.onConfigurationChanged(newConfig);
     79         mPresenter.updateMenuView(false);
     80 
     81         if (mPresenter != null && mPresenter.isOverflowMenuShowing()) {
     82             mPresenter.hideOverflowMenu();
     83             mPresenter.showOverflowMenu();
     84         }
     85     }
     86 
     87     @Override
     88     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     89         // If we've been given an exact size to match, apply special formatting during layout.
     90         final boolean wasFormatted = mFormatItems;
     91         mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
     92 
     93         if (wasFormatted != mFormatItems) {
     94             mFormatItemsWidth = 0; // Reset this when switching modes
     95         }
     96 
     97         // Special formatting can change whether items can fit as action buttons.
     98         // Kick the menu and update presenters when this changes.
     99         final int widthSize = MeasureSpec.getMode(widthMeasureSpec);
    100         if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) {
    101             mFormatItemsWidth = widthSize;
    102             mMenu.onItemsChanged(true);
    103         }
    104 
    105         if (mFormatItems) {
    106             onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec);
    107         } else {
    108             // Previous measurement at exact format may have set margins - reset them.
    109             final int childCount = getChildCount();
    110             for (int i = 0; i < childCount; i++) {
    111                 final View child = getChildAt(i);
    112                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    113                 lp.leftMargin = lp.rightMargin = 0;
    114             }
    115             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    116         }
    117     }
    118 
    119     private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) {
    120         // We already know the width mode is EXACTLY if we're here.
    121         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    122         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    123         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    124 
    125         final int widthPadding = getPaddingLeft() + getPaddingRight();
    126         final int heightPadding = getPaddingTop() + getPaddingBottom();
    127 
    128         final int itemHeightSpec = heightMode == MeasureSpec.EXACTLY
    129                 ? MeasureSpec.makeMeasureSpec(heightSize - heightPadding, MeasureSpec.EXACTLY)
    130                 : MeasureSpec.makeMeasureSpec(
    131                     Math.min(mMaxItemHeight, heightSize - heightPadding), MeasureSpec.AT_MOST);
    132 
    133         widthSize -= widthPadding;
    134 
    135         // Divide the view into cells.
    136         final int cellCount = widthSize / mMinCellSize;
    137         final int cellSizeRemaining = widthSize % mMinCellSize;
    138 
    139         if (cellCount == 0) {
    140             // Give up, nothing fits.
    141             setMeasuredDimension(widthSize, 0);
    142             return;
    143         }
    144 
    145         final int cellSize = mMinCellSize + cellSizeRemaining / cellCount;
    146 
    147         int cellsRemaining = cellCount;
    148         int maxChildHeight = 0;
    149         int maxCellsUsed = 0;
    150         int expandableItemCount = 0;
    151         int visibleItemCount = 0;
    152         boolean hasOverflow = false;
    153 
    154         // This is used as a bitfield to locate the smallest items present. Assumes childCount < 64.
    155         long smallestItemsAt = 0;
    156 
    157         final int childCount = getChildCount();
    158         for (int i = 0; i < childCount; i++) {
    159             final View child = getChildAt(i);
    160             if (child.getVisibility() == GONE) continue;
    161 
    162             final boolean isGeneratedItem = child instanceof ActionMenuItemView;
    163             visibleItemCount++;
    164 
    165             if (isGeneratedItem) {
    166                 // Reset padding for generated menu item views; it may change below
    167                 // and views are recycled.
    168                 child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0);
    169             }
    170 
    171             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    172             lp.expanded = false;
    173             lp.extraPixels = 0;
    174             lp.cellsUsed = 0;
    175             lp.expandable = false;
    176             lp.leftMargin = 0;
    177             lp.rightMargin = 0;
    178             lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText();
    179 
    180             // Overflow always gets 1 cell. No more, no less.
    181             final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining;
    182 
    183             final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable,
    184                     itemHeightSpec, heightPadding);
    185 
    186             maxCellsUsed = Math.max(maxCellsUsed, cellsUsed);
    187             if (lp.expandable) expandableItemCount++;
    188             if (lp.isOverflowButton) hasOverflow = true;
    189 
    190             cellsRemaining -= cellsUsed;
    191             maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
    192             if (cellsUsed == 1) smallestItemsAt |= (1 << i);
    193         }
    194 
    195         // When we have overflow and a single expanded (text) item, we want to try centering it
    196         // visually in the available space even though overflow consumes some of it.
    197         final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2;
    198 
    199         // Divide space for remaining cells if we have items that can expand.
    200         // Try distributing whole leftover cells to smaller items first.
    201 
    202         boolean needsExpansion = false;
    203         while (expandableItemCount > 0 && cellsRemaining > 0) {
    204             int minCells = Integer.MAX_VALUE;
    205             long minCellsAt = 0; // Bit locations are indices of relevant child views
    206             int minCellsItemCount = 0;
    207             for (int i = 0; i < childCount; i++) {
    208                 final View child = getChildAt(i);
    209                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    210 
    211                 // Don't try to expand items that shouldn't.
    212                 if (!lp.expandable) continue;
    213 
    214                 // Mark indices of children that can receive an extra cell.
    215                 if (lp.cellsUsed < minCells) {
    216                     minCells = lp.cellsUsed;
    217                     minCellsAt = 1 << i;
    218                     minCellsItemCount = 1;
    219                 } else if (lp.cellsUsed == minCells) {
    220                     minCellsAt |= 1 << i;
    221                     minCellsItemCount++;
    222                 }
    223             }
    224 
    225             // Items that get expanded will always be in the set of smallest items when we're done.
    226             smallestItemsAt |= minCellsAt;
    227 
    228             if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop.
    229 
    230             // We have enough cells, all minimum size items will be incremented.
    231             minCells++;
    232 
    233             for (int i = 0; i < childCount; i++) {
    234                 final View child = getChildAt(i);
    235                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    236                 if ((minCellsAt & (1 << i)) == 0) {
    237                     // If this item is already at our small item count, mark it for later.
    238                     if (lp.cellsUsed == minCells) smallestItemsAt |= 1 << i;
    239                     continue;
    240                 }
    241 
    242                 if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) {
    243                     // Add padding to this item such that it centers.
    244                     child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0);
    245                 }
    246                 lp.cellsUsed++;
    247                 lp.expanded = true;
    248                 cellsRemaining--;
    249             }
    250 
    251             needsExpansion = true;
    252         }
    253 
    254         // Divide any space left that wouldn't divide along cell boundaries
    255         // evenly among the smallest items
    256 
    257         final boolean singleItem = !hasOverflow && visibleItemCount == 1;
    258         if (cellsRemaining > 0 && smallestItemsAt != 0 &&
    259                 (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) {
    260             float expandCount = Long.bitCount(smallestItemsAt);
    261 
    262             if (!singleItem) {
    263                 // The items at the far edges may only expand by half in order to pin to either side.
    264                 if ((smallestItemsAt & 1) != 0) {
    265                     LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams();
    266                     if (!lp.preventEdgeOffset) expandCount -= 0.5f;
    267                 }
    268                 if ((smallestItemsAt & (1 << (childCount - 1))) != 0) {
    269                     LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams());
    270                     if (!lp.preventEdgeOffset) expandCount -= 0.5f;
    271                 }
    272             }
    273 
    274             final int extraPixels = expandCount > 0 ?
    275                     (int) (cellsRemaining * cellSize / expandCount) : 0;
    276 
    277             for (int i = 0; i < childCount; i++) {
    278                 if ((smallestItemsAt & (1 << i)) == 0) continue;
    279 
    280                 final View child = getChildAt(i);
    281                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    282                 if (child instanceof ActionMenuItemView) {
    283                     // If this is one of our views, expand and measure at the larger size.
    284                     lp.extraPixels = extraPixels;
    285                     lp.expanded = true;
    286                     if (i == 0 && !lp.preventEdgeOffset) {
    287                         // First item gets part of its new padding pushed out of sight.
    288                         // The last item will get this implicitly from layout.
    289                         lp.leftMargin = -extraPixels / 2;
    290                     }
    291                     needsExpansion = true;
    292                 } else if (lp.isOverflowButton) {
    293                     lp.extraPixels = extraPixels;
    294                     lp.expanded = true;
    295                     lp.rightMargin = -extraPixels / 2;
    296                     needsExpansion = true;
    297                 } else {
    298                     // If we don't know what it is, give it some margins instead
    299                     // and let it center within its space. We still want to pin
    300                     // against the edges.
    301                     if (i != 0) {
    302                         lp.leftMargin = extraPixels / 2;
    303                     }
    304                     if (i != childCount - 1) {
    305                         lp.rightMargin = extraPixels / 2;
    306                     }
    307                 }
    308             }
    309 
    310             cellsRemaining = 0;
    311         }
    312 
    313         // Remeasure any items that have had extra space allocated to them.
    314         if (needsExpansion) {
    315             for (int i = 0; i < childCount; i++) {
    316                 final View child = getChildAt(i);
    317                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    318 
    319                 if (!lp.expanded) continue;
    320 
    321                 final int width = lp.cellsUsed * cellSize + lp.extraPixels;
    322                 child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
    323                         itemHeightSpec);
    324             }
    325         }
    326 
    327         if (heightMode != MeasureSpec.EXACTLY) {
    328             heightSize = maxChildHeight;
    329         }
    330 
    331         setMeasuredDimension(widthSize, heightSize);
    332         mMeasuredExtraWidth = cellsRemaining * cellSize;
    333     }
    334 
    335     /**
    336      * Measure a child view to fit within cell-based formatting. The child's width
    337      * will be measured to a whole multiple of cellSize.
    338      *
    339      * <p>Sets the expandable and cellsUsed fields of LayoutParams.
    340      *
    341      * @param child Child to measure
    342      * @param cellSize Size of one cell
    343      * @param cellsRemaining Number of cells remaining that this view can expand to fill
    344      * @param parentHeightMeasureSpec MeasureSpec used by the parent view
    345      * @param parentHeightPadding Padding present in the parent view
    346      * @return Number of cells this child was measured to occupy
    347      */
    348     static int measureChildForCells(View child, int cellSize, int cellsRemaining,
    349             int parentHeightMeasureSpec, int parentHeightPadding) {
    350         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    351 
    352         final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) -
    353                 parentHeightPadding;
    354         final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec);
    355         final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
    356 
    357         final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
    358                 (ActionMenuItemView) child : null;
    359         final boolean hasText = itemView != null && itemView.hasText();
    360 
    361         int cellsUsed = 0;
    362         if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) {
    363             final int childWidthSpec = MeasureSpec.makeMeasureSpec(
    364                     cellSize * cellsRemaining, MeasureSpec.AT_MOST);
    365             child.measure(childWidthSpec, childHeightSpec);
    366 
    367             final int measuredWidth = child.getMeasuredWidth();
    368             cellsUsed = measuredWidth / cellSize;
    369             if (measuredWidth % cellSize != 0) cellsUsed++;
    370             if (hasText && cellsUsed < 2) cellsUsed = 2;
    371         }
    372 
    373         final boolean expandable = !lp.isOverflowButton && hasText;
    374         lp.expandable = expandable;
    375 
    376         lp.cellsUsed = cellsUsed;
    377         final int targetWidth = cellsUsed * cellSize;
    378         child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
    379                 childHeightSpec);
    380         return cellsUsed;
    381     }
    382 
    383     @Override
    384     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    385         if (!mFormatItems) {
    386             super.onLayout(changed, left, top, right, bottom);
    387             return;
    388         }
    389 
    390         final int childCount = getChildCount();
    391         final int midVertical = (top + bottom) / 2;
    392         final int dividerWidth = getDividerWidth();
    393         int overflowWidth = 0;
    394         int nonOverflowWidth = 0;
    395         int nonOverflowCount = 0;
    396         int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
    397         boolean hasOverflow = false;
    398         for (int i = 0; i < childCount; i++) {
    399             final View v = getChildAt(i);
    400             if (v.getVisibility() == GONE) {
    401                 continue;
    402             }
    403 
    404             LayoutParams p = (LayoutParams) v.getLayoutParams();
    405             if (p.isOverflowButton) {
    406                 overflowWidth = v.getMeasuredWidth();
    407                 if (hasDividerBeforeChildAt(i)) {
    408                     overflowWidth += dividerWidth;
    409                 }
    410 
    411                 int height = v.getMeasuredHeight();
    412                 int r = getWidth() - getPaddingRight() - p.rightMargin;
    413                 int l = r - overflowWidth;
    414                 int t = midVertical - (height / 2);
    415                 int b = t + height;
    416                 v.layout(l, t, r, b);
    417 
    418                 widthRemaining -= overflowWidth;
    419                 hasOverflow = true;
    420             } else {
    421                 final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin;
    422                 nonOverflowWidth += size;
    423                 widthRemaining -= size;
    424                 if (hasDividerBeforeChildAt(i)) {
    425                     nonOverflowWidth += dividerWidth;
    426                 }
    427                 nonOverflowCount++;
    428             }
    429         }
    430 
    431         if (childCount == 1 && !hasOverflow) {
    432             // Center a single child
    433             final View v = getChildAt(0);
    434             final int width = v.getMeasuredWidth();
    435             final int height = v.getMeasuredHeight();
    436             final int midHorizontal = (right - left) / 2;
    437             final int l = midHorizontal - width / 2;
    438             final int t = midVertical - height / 2;
    439             v.layout(l, t, l + width, t + height);
    440             return;
    441         }
    442 
    443         final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
    444         final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0);
    445 
    446         int startLeft = getPaddingLeft();
    447         for (int i = 0; i < childCount; i++) {
    448             final View v = getChildAt(i);
    449             final LayoutParams lp = (LayoutParams) v.getLayoutParams();
    450             if (v.getVisibility() == GONE || lp.isOverflowButton) {
    451                 continue;
    452             }
    453 
    454             startLeft += lp.leftMargin;
    455             int width = v.getMeasuredWidth();
    456             int height = v.getMeasuredHeight();
    457             int t = midVertical - height / 2;
    458             v.layout(startLeft, t, startLeft + width, t + height);
    459             startLeft += width + lp.rightMargin + spacerSize;
    460         }
    461     }
    462 
    463     @Override
    464     public void onDetachedFromWindow() {
    465         super.onDetachedFromWindow();
    466         mPresenter.dismissPopupMenus();
    467     }
    468 
    469     public boolean isOverflowReserved() {
    470         return mReserveOverflow;
    471     }
    472 
    473     public void setOverflowReserved(boolean reserveOverflow) {
    474         mReserveOverflow = reserveOverflow;
    475     }
    476 
    477     @Override
    478     protected LayoutParams generateDefaultLayoutParams() {
    479         LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
    480                 LayoutParams.WRAP_CONTENT);
    481         params.gravity = Gravity.CENTER_VERTICAL;
    482         return params;
    483     }
    484 
    485     @Override
    486     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    487         return new LayoutParams(getContext(), attrs);
    488     }
    489 
    490     @Override
    491     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    492         if (p instanceof LayoutParams) {
    493             LayoutParams result = new LayoutParams((LayoutParams) p);
    494             if (result.gravity <= Gravity.NO_GRAVITY) {
    495                 result.gravity = Gravity.CENTER_VERTICAL;
    496             }
    497             return result;
    498         }
    499         return generateDefaultLayoutParams();
    500     }
    501 
    502     @Override
    503     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    504         return p != null && p instanceof LayoutParams;
    505     }
    506 
    507     public LayoutParams generateOverflowButtonLayoutParams() {
    508         LayoutParams result = generateDefaultLayoutParams();
    509         result.isOverflowButton = true;
    510         return result;
    511     }
    512 
    513     public boolean invokeItem(MenuItemImpl item) {
    514         return mMenu.performItemAction(item, 0);
    515     }
    516 
    517     public int getWindowAnimations() {
    518         return 0;
    519     }
    520 
    521     public void initialize(MenuBuilder menu) {
    522         mMenu = menu;
    523     }
    524 
    525     @Override
    526     protected boolean hasDividerBeforeChildAt(int childIndex) {
    527         final View childBefore = getChildAt(childIndex - 1);
    528         final View child = getChildAt(childIndex);
    529         boolean result = false;
    530         if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) {
    531             result |= ((ActionMenuChildView) childBefore).needsDividerAfter();
    532         }
    533         if (childIndex > 0 && child instanceof ActionMenuChildView) {
    534             result |= ((ActionMenuChildView) child).needsDividerBefore();
    535         }
    536         return result;
    537     }
    538 
    539     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    540         return false;
    541     }
    542 
    543     public interface ActionMenuChildView {
    544         public boolean needsDividerBefore();
    545         public boolean needsDividerAfter();
    546     }
    547 
    548     public static class LayoutParams extends LinearLayout.LayoutParams {
    549         @ViewDebug.ExportedProperty(category = "layout")
    550         public boolean isOverflowButton;
    551         @ViewDebug.ExportedProperty(category = "layout")
    552         public int cellsUsed;
    553         @ViewDebug.ExportedProperty(category = "layout")
    554         public int extraPixels;
    555         @ViewDebug.ExportedProperty(category = "layout")
    556         public boolean expandable;
    557         @ViewDebug.ExportedProperty(category = "layout")
    558         public boolean preventEdgeOffset;
    559 
    560         public boolean expanded;
    561 
    562         public LayoutParams(Context c, AttributeSet attrs) {
    563             super(c, attrs);
    564         }
    565 
    566         public LayoutParams(LayoutParams other) {
    567             super((LinearLayout.LayoutParams) other);
    568             isOverflowButton = other.isOverflowButton;
    569         }
    570 
    571         public LayoutParams(int width, int height) {
    572             super(width, height);
    573             isOverflowButton = false;
    574         }
    575 
    576         public LayoutParams(int width, int height, boolean isOverflowButton) {
    577             super(width, height);
    578             this.isOverflowButton = isOverflowButton;
    579         }
    580     }
    581 }
    582