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