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