ActionMenuView.java revision 160bb7fa60e8ece654e6ce999b6c16af50ee7357
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.widget.LinearLayout;
26
27/**
28 * @hide
29 */
30public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
31    private static final String TAG = "ActionMenuView";
32
33    static final int MIN_CELL_SIZE = 56; // dips
34
35    private MenuBuilder mMenu;
36
37    private boolean mReserveOverflow;
38    private ActionMenuPresenter mPresenter;
39    private boolean mUpdateContentsBeforeMeasure;
40    private boolean mFormatItems;
41    private int mMinCellSize;
42    private int mMeasuredExtraWidth;
43
44    public ActionMenuView(Context context) {
45        this(context, null);
46    }
47
48    public ActionMenuView(Context context, AttributeSet attrs) {
49        super(context, attrs);
50        setBaselineAligned(false);
51        mMinCellSize = (int) (MIN_CELL_SIZE * context.getResources().getDisplayMetrics().density);
52    }
53
54    public void setPresenter(ActionMenuPresenter presenter) {
55        mPresenter = presenter;
56    }
57
58    public boolean isExpandedFormat() {
59        return mFormatItems;
60    }
61
62    @Override
63    public void onConfigurationChanged(Configuration newConfig) {
64        super.onConfigurationChanged(newConfig);
65        mPresenter.updateMenuView(false);
66
67        if (mPresenter != null && mPresenter.isOverflowMenuShowing()) {
68            mPresenter.hideOverflowMenu();
69            mPresenter.showOverflowMenu();
70        }
71    }
72
73    @Override
74    public void requestLayout() {
75        // Layout can influence how many action items fit.
76        mUpdateContentsBeforeMeasure = true;
77        super.requestLayout();
78    }
79
80    @Override
81    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
82        // If we've been given an exact size to match, apply special formatting during layout.
83        mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
84        if (mUpdateContentsBeforeMeasure && mMenu != null) {
85            mMenu.onItemsChanged(true);
86            mUpdateContentsBeforeMeasure = false;
87        }
88
89        if (mFormatItems) {
90            onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec);
91        } else {
92            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
93        }
94    }
95
96    private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) {
97        // We already know the width mode is EXACTLY if we're here.
98        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
99        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
100        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
101
102        final int widthPadding = getPaddingLeft() + getPaddingRight();
103        final int heightPadding = getPaddingTop() + getPaddingBottom();
104
105        widthSize -= widthPadding;
106
107        // Divide the view into cells.
108        final int cellCount = widthSize / mMinCellSize;
109        final int cellSizeRemaining = widthSize % mMinCellSize;
110        final int cellSize = mMinCellSize + cellSizeRemaining / cellCount;
111
112        int cellsRemaining = cellCount;
113        int maxChildHeight = 0;
114        int maxCellsUsed = 0;
115        int expandableItemCount = 0;
116
117        if (mReserveOverflow) cellsRemaining--;
118
119        final int childCount = getChildCount();
120        for (int i = 0; i < childCount; i++) {
121            final View child = getChildAt(i);
122            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
123            lp.expanded = false;
124            lp.extraPixels = 0;
125            lp.cellsUsed = 0;
126            lp.expandable = false;
127
128            // Overflow always gets 1 cell. No more, no less.
129            final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining;
130
131            final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable,
132                    heightMeasureSpec, heightPadding);
133
134            maxCellsUsed = Math.max(maxCellsUsed, cellsUsed);
135            if (lp.expandable) expandableItemCount++;
136
137            cellsRemaining -= cellsUsed;
138            maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
139        }
140
141        // Divide space for remaining cells if we have items that can expand.
142        // Try distributing whole leftover cells to smaller items first.
143
144        boolean needsExpansion = false;
145        long smallestExpandableItemsAt = 0;
146        while (expandableItemCount > 0 && cellsRemaining > 0) {
147            int minCells = Integer.MAX_VALUE;
148            long minCellsAt = 0; // Bit locations are indices of relevant child views
149            int minCellsItemCount = 0;
150            for (int i = 0; i < childCount; i++) {
151                final View child = getChildAt(i);
152                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
153
154                // Don't try to expand items that shouldn't.
155                if (!lp.expandable) continue;
156
157                // Mark indices of children that can receive an extra cell.
158                if (lp.cellsUsed < minCells) {
159                    minCells = lp.cellsUsed;
160                    minCellsAt = 1 << i;
161                    minCellsItemCount = 1;
162                } else if (lp.cellsUsed == minCells) {
163                    minCellsAt |= 1 << i;
164                    minCellsItemCount++;
165                }
166            }
167
168            if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop.
169
170            // Items that get expanded will always be in the set of smallest items when we're done.
171            smallestExpandableItemsAt |= minCellsAt;
172
173            for (int i = 0; i < childCount; i++) {
174                if ((minCellsAt & (1 << i)) == 0) continue;
175
176                final View child = getChildAt(i);
177                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
178                lp.cellsUsed++;
179                lp.expanded = true;
180                cellsRemaining--;
181            }
182
183            needsExpansion = true;
184        }
185
186        // Divide any space left that wouldn't divide along cell boundaries
187        // evenly among the smallest multi-cell (expandable) items.
188
189        if (cellsRemaining > 0 && smallestExpandableItemsAt != 0) {
190            final int expandCount = Long.bitCount(smallestExpandableItemsAt);
191            final int extraPixels = cellsRemaining * cellSize / expandCount;
192
193            for (int i = 0; i < childCount; i++) {
194                if ((smallestExpandableItemsAt & (1 << i)) == 0) continue;
195
196                final View child = getChildAt(i);
197                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
198                lp.extraPixels = extraPixels;
199                lp.expanded = true;
200            }
201
202            needsExpansion = true;
203            cellsRemaining = 0;
204        }
205
206        // Remeasure any items that have had extra space allocated to them.
207        if (needsExpansion) {
208            int heightSpec = MeasureSpec.makeMeasureSpec(heightSize - heightPadding, heightMode);
209            for (int i = 0; i < childCount; i++) {
210                final View child = getChildAt(i);
211                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
212
213                if (!lp.expanded) continue;
214
215                final int width = lp.cellsUsed * cellSize + lp.extraPixels;
216                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), heightSpec);
217            }
218        }
219
220        if (heightMode != MeasureSpec.EXACTLY) {
221            heightSize = maxChildHeight;
222        }
223
224        setMeasuredDimension(widthSize, heightSize);
225        mMeasuredExtraWidth = cellsRemaining * cellSize;
226    }
227
228    /**
229     * Measure a child view to fit within cell-based formatting. The child's width
230     * will be measured to a whole multiple of cellSize.
231     *
232     * <p>Sets the expandable and cellsUsed fields of LayoutParams.
233     *
234     * @param child Child to measure
235     * @param cellSize Size of one cell
236     * @param cellsRemaining Number of cells remaining that this view can expand to fill
237     * @param parentHeightMeasureSpec MeasureSpec used by the parent view
238     * @param parentHeightPadding Padding present in the parent view
239     * @return Number of cells this child was measured to occupy
240     */
241    static int measureChildForCells(View child, int cellSize, int cellsRemaining,
242            int parentHeightMeasureSpec, int parentHeightPadding) {
243        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
244
245        final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) -
246                parentHeightPadding;
247        final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec);
248        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
249
250        int cellsUsed = 0;
251        if (cellsRemaining > 0) {
252            final int childWidthSpec = MeasureSpec.makeMeasureSpec(
253                    cellSize * cellsRemaining, MeasureSpec.AT_MOST);
254            child.measure(childWidthSpec, childHeightSpec);
255
256            final int measuredWidth = child.getMeasuredWidth();
257            cellsUsed = measuredWidth / cellSize;
258            if (measuredWidth % cellSize != 0) cellsUsed++;
259        }
260
261        final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
262                (ActionMenuItemView) child : null;
263        final boolean expandable = !lp.isOverflowButton && itemView != null && itemView.hasText();
264        lp.expandable = expandable;
265
266        lp.cellsUsed = cellsUsed;
267        final int targetWidth = cellsUsed * cellSize;
268        child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
269                childHeightSpec);
270        return cellsUsed;
271    }
272
273    @Override
274    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
275        if (!mFormatItems) {
276            super.onLayout(changed, left, top, right, bottom);
277            return;
278        }
279
280        final int childCount = getChildCount();
281        final int midVertical = (top + bottom) / 2;
282        final int dividerWidth = getDividerWidth();
283        int overflowWidth = 0;
284        int nonOverflowWidth = 0;
285        int nonOverflowCount = 0;
286        int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
287        boolean hasOverflow = false;
288        for (int i = 0; i < childCount; i++) {
289            final View v = getChildAt(i);
290            if (v.getVisibility() == GONE) {
291                continue;
292            }
293
294            LayoutParams p = (LayoutParams) v.getLayoutParams();
295            if (p.isOverflowButton) {
296                overflowWidth = v.getMeasuredWidth();
297                if (hasDividerBeforeChildAt(i)) {
298                    overflowWidth += dividerWidth;
299                }
300
301                int height = v.getMeasuredHeight();
302                int r = getWidth() - getPaddingRight();
303                int l = r - overflowWidth;
304                int t = midVertical - (height / 2);
305                int b = t + height;
306                v.layout(l, t, r, b);
307
308                widthRemaining -= overflowWidth;
309                hasOverflow = true;
310            } else {
311                final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin;
312                nonOverflowWidth += size;
313                widthRemaining -= size;
314                if (hasDividerBeforeChildAt(i)) {
315                    nonOverflowWidth += dividerWidth;
316                }
317                nonOverflowCount++;
318            }
319        }
320
321        final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
322        final int spacerSize = spacerCount > 0 ? widthRemaining / spacerCount : 0;
323
324        int startLeft = getPaddingLeft();
325        for (int i = 0; i < childCount; i++) {
326            final View v = getChildAt(i);
327            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
328            if (v.getVisibility() == GONE || lp.isOverflowButton) {
329                continue;
330            }
331
332            startLeft += lp.leftMargin;
333            int width = v.getMeasuredWidth();
334            int height = v.getMeasuredHeight();
335            int t = midVertical - (height / 2);
336            v.layout(startLeft, t, startLeft + width, t + height);
337            startLeft += width + lp.rightMargin + spacerSize;
338        }
339    }
340
341    @Override
342    public void onDetachedFromWindow() {
343        super.onDetachedFromWindow();
344        mPresenter.dismissPopupMenus();
345    }
346
347    public boolean isOverflowReserved() {
348        return mReserveOverflow;
349    }
350
351    public void setOverflowReserved(boolean reserveOverflow) {
352        mReserveOverflow = reserveOverflow;
353    }
354
355    @Override
356    protected LayoutParams generateDefaultLayoutParams() {
357        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
358                LayoutParams.WRAP_CONTENT);
359        params.gravity = Gravity.CENTER_VERTICAL;
360        return params;
361    }
362
363    @Override
364    public LayoutParams generateLayoutParams(AttributeSet attrs) {
365        return new LayoutParams(getContext(), attrs);
366    }
367
368    @Override
369    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
370        if (p instanceof LayoutParams) {
371            LayoutParams result = new LayoutParams((LayoutParams) p);
372            if (result.gravity <= Gravity.NO_GRAVITY) {
373                result.gravity = Gravity.CENTER_VERTICAL;
374            }
375            return result;
376        }
377        return generateDefaultLayoutParams();
378    }
379
380    @Override
381    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
382        return p != null && p instanceof LayoutParams;
383    }
384
385    public LayoutParams generateOverflowButtonLayoutParams() {
386        LayoutParams result = generateDefaultLayoutParams();
387        result.isOverflowButton = true;
388        return result;
389    }
390
391    public boolean invokeItem(MenuItemImpl item) {
392        return mMenu.performItemAction(item, 0);
393    }
394
395    public int getWindowAnimations() {
396        return 0;
397    }
398
399    public void initialize(MenuBuilder menu) {
400        mMenu = menu;
401    }
402
403    @Override
404    protected boolean hasDividerBeforeChildAt(int childIndex) {
405        final View childBefore = getChildAt(childIndex - 1);
406        final View child = getChildAt(childIndex);
407        boolean result = false;
408        if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) {
409            result |= ((ActionMenuChildView) childBefore).needsDividerAfter();
410        }
411        if (childIndex > 0 && child instanceof ActionMenuChildView) {
412            result |= ((ActionMenuChildView) child).needsDividerBefore();
413        }
414        return result;
415    }
416
417    public interface ActionMenuChildView {
418        public boolean needsDividerBefore();
419        public boolean needsDividerAfter();
420    }
421
422    public static class LayoutParams extends LinearLayout.LayoutParams {
423        @ViewDebug.ExportedProperty(category = "layout")
424        public boolean isOverflowButton;
425        @ViewDebug.ExportedProperty(category = "layout")
426        public int cellsUsed;
427        @ViewDebug.ExportedProperty(category = "layout")
428        public int extraPixels;
429        @ViewDebug.ExportedProperty(category = "layout")
430        public boolean expandable;
431
432        public boolean expanded;
433
434        public LayoutParams(Context c, AttributeSet attrs) {
435            super(c, attrs);
436        }
437
438        public LayoutParams(LayoutParams other) {
439            super((LinearLayout.LayoutParams) other);
440            isOverflowButton = other.isOverflowButton;
441        }
442
443        public LayoutParams(int width, int height) {
444            super(width, height);
445            isOverflowButton = false;
446        }
447
448        public LayoutParams(int width, int height, boolean isOverflowButton) {
449            super(width, height);
450            this.isOverflowButton = isOverflowButton;
451        }
452    }
453}
454