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