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            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