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