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