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 android.support.v7.internal.view.menu;
17
18import android.content.Context;
19import android.content.res.Configuration;
20import android.content.res.TypedArray;
21import android.os.Build;
22import android.support.v7.appcompat.R;
23import android.support.v7.internal.widget.LinearLayoutICS;
24import android.util.AttributeSet;
25import android.view.Gravity;
26import android.view.View;
27import android.view.ViewDebug;
28import android.view.ViewGroup;
29import android.view.accessibility.AccessibilityEvent;
30import android.widget.LinearLayout;
31
32/**
33 * @hide
34 */
35public class ActionMenuView extends LinearLayoutICS implements MenuBuilder.ItemInvoker, MenuView {
36
37    private static final String TAG = "ActionMenuView";
38
39    static final int MIN_CELL_SIZE = 56; // dips
40    static final int GENERATED_ITEM_PADDING = 4; // dips
41
42    private MenuBuilder mMenu;
43
44    private boolean mReserveOverflow;
45    private ActionMenuPresenter mPresenter;
46    private boolean mFormatItems;
47    private int mFormatItemsWidth;
48    private int mMinCellSize;
49    private int mGeneratedItemPadding;
50    private int mMeasuredExtraWidth;
51    private int mMaxItemHeight;
52
53    public ActionMenuView(Context context) {
54        this(context, null);
55    }
56
57    public ActionMenuView(Context context, AttributeSet attrs) {
58        super(context, attrs);
59        setBaselineAligned(false);
60        final float density = context.getResources().getDisplayMetrics().density;
61        mMinCellSize = (int) (MIN_CELL_SIZE * density);
62        mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density);
63
64        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
65                R.attr.actionBarStyle, 0);
66        mMaxItemHeight = a.getDimensionPixelSize(R.styleable.ActionBar_height, 0);
67        a.recycle();
68    }
69
70    public void setPresenter(ActionMenuPresenter presenter) {
71        mPresenter = presenter;
72    }
73
74    public boolean isExpandedFormat() {
75        return mFormatItems;
76    }
77
78    @Override
79    public void onConfigurationChanged(Configuration newConfig) {
80        if (Build.VERSION.SDK_INT >= 8) {
81            super.onConfigurationChanged(newConfig);
82        }
83
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) {
166                continue;
167            }
168
169            final boolean isGeneratedItem = child instanceof ActionMenuItemView;
170            visibleItemCount++;
171
172            if (isGeneratedItem) {
173                // Reset padding for generated menu item views; it may change below
174                // and views are recycled.
175                child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0);
176            }
177
178            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
179            lp.expanded = false;
180            lp.extraPixels = 0;
181            lp.cellsUsed = 0;
182            lp.expandable = false;
183            lp.leftMargin = 0;
184            lp.rightMargin = 0;
185            lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText();
186
187            // Overflow always gets 1 cell. No more, no less.
188            final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining;
189
190            final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable,
191                    itemHeightSpec, heightPadding);
192
193            maxCellsUsed = Math.max(maxCellsUsed, cellsUsed);
194            if (lp.expandable) {
195                expandableItemCount++;
196            }
197            if (lp.isOverflowButton) {
198                hasOverflow = true;
199            }
200
201            cellsRemaining -= cellsUsed;
202            maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
203            if (cellsUsed == 1) {
204                smallestItemsAt |= (1 << i);
205            }
206        }
207
208        // When we have overflow and a single expanded (text) item, we want to try centering it
209        // visually in the available space even though overflow consumes some of it.
210        final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2;
211
212        // Divide space for remaining cells if we have items that can expand.
213        // Try distributing whole leftover cells to smaller items first.
214
215        boolean needsExpansion = false;
216        while (expandableItemCount > 0 && cellsRemaining > 0) {
217            int minCells = Integer.MAX_VALUE;
218            long minCellsAt = 0; // Bit locations are indices of relevant child views
219            int minCellsItemCount = 0;
220            for (int i = 0; i < childCount; i++) {
221                final View child = getChildAt(i);
222                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
223
224                // Don't try to expand items that shouldn't.
225                if (!lp.expandable) {
226                    continue;
227                }
228
229                // Mark indices of children that can receive an extra cell.
230                if (lp.cellsUsed < minCells) {
231                    minCells = lp.cellsUsed;
232                    minCellsAt = 1 << i;
233                    minCellsItemCount = 1;
234                } else if (lp.cellsUsed == minCells) {
235                    minCellsAt |= 1 << i;
236                    minCellsItemCount++;
237                }
238            }
239
240            // Items that get expanded will always be in the set of smallest items when we're done.
241            smallestItemsAt |= minCellsAt;
242
243            if (minCellsItemCount > cellsRemaining) {
244                break; // Couldn't expand anything evenly. Stop.
245            }
246
247            // We have enough cells, all minimum size items will be incremented.
248            minCells++;
249
250            for (int i = 0; i < childCount; i++) {
251                final View child = getChildAt(i);
252                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
253                if ((minCellsAt & (1 << i)) == 0) {
254                    // If this item is already at our small item count, mark it for later.
255                    if (lp.cellsUsed == minCells) {
256                        smallestItemsAt |= 1 << i;
257                    }
258                    continue;
259                }
260
261                if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) {
262                    // Add padding to this item such that it centers.
263                    child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0);
264                }
265                lp.cellsUsed++;
266                lp.expanded = true;
267                cellsRemaining--;
268            }
269
270            needsExpansion = true;
271        }
272
273        // Divide any space left that wouldn't divide along cell boundaries
274        // evenly among the smallest items
275
276        final boolean singleItem = !hasOverflow && visibleItemCount == 1;
277        if (cellsRemaining > 0 && smallestItemsAt != 0 &&
278                (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) {
279            float expandCount = Long.bitCount(smallestItemsAt);
280
281            if (!singleItem) {
282                // The items at the far edges may only expand by half in order to pin to either side.
283                if ((smallestItemsAt & 1) != 0) {
284                    LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams();
285                    if (!lp.preventEdgeOffset) {
286                        expandCount -= 0.5f;
287                    }
288                }
289                if ((smallestItemsAt & (1 << (childCount - 1))) != 0) {
290                    LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams());
291                    if (!lp.preventEdgeOffset) {
292                        expandCount -= 0.5f;
293                    }
294                }
295            }
296
297            final int extraPixels = expandCount > 0 ?
298                    (int) (cellsRemaining * cellSize / expandCount) : 0;
299
300            for (int i = 0; i < childCount; i++) {
301                if ((smallestItemsAt & (1 << i)) == 0) {
302                    continue;
303                }
304
305                final View child = getChildAt(i);
306                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
307                if (child instanceof ActionMenuItemView) {
308                    // If this is one of our views, expand and measure at the larger size.
309                    lp.extraPixels = extraPixels;
310                    lp.expanded = true;
311                    if (i == 0 && !lp.preventEdgeOffset) {
312                        // First item gets part of its new padding pushed out of sight.
313                        // The last item will get this implicitly from layout.
314                        lp.leftMargin = -extraPixels / 2;
315                    }
316                    needsExpansion = true;
317                } else if (lp.isOverflowButton) {
318                    lp.extraPixels = extraPixels;
319                    lp.expanded = true;
320                    lp.rightMargin = -extraPixels / 2;
321                    needsExpansion = true;
322                } else {
323                    // If we don't know what it is, give it some margins instead
324                    // and let it center within its space. We still want to pin
325                    // against the edges.
326                    if (i != 0) {
327                        lp.leftMargin = extraPixels / 2;
328                    }
329                    if (i != childCount - 1) {
330                        lp.rightMargin = extraPixels / 2;
331                    }
332                }
333            }
334
335            cellsRemaining = 0;
336        }
337
338        // Remeasure any items that have had extra space allocated to them.
339        if (needsExpansion) {
340            for (int i = 0; i < childCount; i++) {
341                final View child = getChildAt(i);
342                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
343
344                if (!lp.expanded) {
345                    continue;
346                }
347
348                final int width = lp.cellsUsed * cellSize + lp.extraPixels;
349                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
350                        itemHeightSpec);
351            }
352        }
353
354        if (heightMode != MeasureSpec.EXACTLY) {
355            heightSize = maxChildHeight;
356        }
357
358        setMeasuredDimension(widthSize, heightSize);
359        mMeasuredExtraWidth = cellsRemaining * cellSize;
360    }
361
362    /**
363     * Measure a child view to fit within cell-based formatting. The child's width will be measured
364     * to a whole multiple of cellSize.
365     *
366     * <p>Sets the expandable and cellsUsed fields of LayoutParams.
367     *
368     * @param child                   Child to measure
369     * @param cellSize                Size of one cell
370     * @param cellsRemaining          Number of cells remaining that this view can expand to fill
371     * @param parentHeightMeasureSpec MeasureSpec used by the parent view
372     * @param parentHeightPadding     Padding present in the parent view
373     * @return Number of cells this child was measured to occupy
374     */
375    static int measureChildForCells(View child, int cellSize, int cellsRemaining,
376            int parentHeightMeasureSpec, int parentHeightPadding) {
377        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
378
379        final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) -
380                parentHeightPadding;
381        final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec);
382        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
383
384        final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
385                (ActionMenuItemView) child : null;
386        final boolean hasText = itemView != null && itemView.hasText();
387
388        int cellsUsed = 0;
389        if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) {
390            final int childWidthSpec = MeasureSpec.makeMeasureSpec(
391                    cellSize * cellsRemaining, MeasureSpec.AT_MOST);
392            child.measure(childWidthSpec, childHeightSpec);
393
394            final int measuredWidth = child.getMeasuredWidth();
395            cellsUsed = measuredWidth / cellSize;
396            if (measuredWidth % cellSize != 0) {
397                cellsUsed++;
398            }
399            if (hasText && cellsUsed < 2) {
400                cellsUsed = 2;
401            }
402        }
403
404        final boolean expandable = !lp.isOverflowButton && hasText;
405        lp.expandable = expandable;
406
407        lp.cellsUsed = cellsUsed;
408        final int targetWidth = cellsUsed * cellSize;
409        child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
410                childHeightSpec);
411        return cellsUsed;
412    }
413
414    @Override
415    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
416        if (!mFormatItems) {
417            super.onLayout(changed, left, top, right, bottom);
418            return;
419        }
420
421        final int childCount = getChildCount();
422        final int midVertical = (top + bottom) / 2;
423        final int dividerWidth = getSupportDividerWidth();
424        int overflowWidth = 0;
425        int nonOverflowWidth = 0;
426        int nonOverflowCount = 0;
427        int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
428        boolean hasOverflow = false;
429        for (int i = 0; i < childCount; i++) {
430            final View v = getChildAt(i);
431            if (v.getVisibility() == GONE) {
432                continue;
433            }
434
435            LayoutParams p = (LayoutParams) v.getLayoutParams();
436            if (p.isOverflowButton) {
437                overflowWidth = v.getMeasuredWidth();
438                if (hasSupportDividerBeforeChildAt(i)) {
439                    overflowWidth += dividerWidth;
440                }
441                int height = v.getMeasuredHeight();
442                int r = getWidth() - getPaddingRight() - p.rightMargin;
443                int l = r - overflowWidth;
444                int t = midVertical - (height / 2);
445                int b = t + height;
446                v.layout(l, t, r, b);
447
448                widthRemaining -= overflowWidth;
449                hasOverflow = true;
450            } else {
451                final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin;
452                nonOverflowWidth += size;
453                widthRemaining -= size;
454                if (hasSupportDividerBeforeChildAt(i)) {
455                    nonOverflowWidth += dividerWidth;
456                }
457                nonOverflowCount++;
458            }
459        }
460
461        if (childCount == 1 && !hasOverflow) {
462            // Center a single child
463            final View v = getChildAt(0);
464            final int width = v.getMeasuredWidth();
465            final int height = v.getMeasuredHeight();
466            final int midHorizontal = (right - left) / 2;
467            final int l = midHorizontal - width / 2;
468            final int t = midVertical - height / 2;
469            v.layout(l, t, l + width, t + height);
470            return;
471        }
472
473        final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
474        final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0);
475
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    @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 instanceof LayoutParams) {
523            LayoutParams result = new LayoutParams((LayoutParams) p);
524            if (result.gravity <= Gravity.NO_GRAVITY) {
525                result.gravity = Gravity.CENTER_VERTICAL;
526            }
527            return result;
528        }
529        return generateDefaultLayoutParams();
530    }
531
532    @Override
533    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
534        return p != null && p instanceof LayoutParams;
535    }
536
537    public LayoutParams generateOverflowButtonLayoutParams() {
538        LayoutParams result = generateDefaultLayoutParams();
539        result.isOverflowButton = true;
540        return result;
541    }
542
543    public boolean invokeItem(MenuItemImpl item) {
544        return mMenu.performItemAction(item, 0);
545    }
546
547    public int getWindowAnimations() {
548        return 0;
549    }
550
551    public void initialize(MenuBuilder menu) {
552        mMenu = menu;
553    }
554
555    protected boolean hasSupportDividerBeforeChildAt(int childIndex) {
556        final View childBefore = getChildAt(childIndex - 1);
557        final View child = getChildAt(childIndex);
558        boolean result = false;
559        if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) {
560            result |= ((ActionMenuChildView) childBefore).needsDividerAfter();
561        }
562        if (childIndex > 0 && child instanceof ActionMenuChildView) {
563            result |= ((ActionMenuChildView) child).needsDividerBefore();
564        }
565        return result;
566    }
567
568    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
569        return false;
570    }
571
572    public interface ActionMenuChildView {
573
574        public boolean needsDividerBefore();
575
576        public boolean needsDividerAfter();
577    }
578
579    public static class LayoutParams extends LinearLayout.LayoutParams {
580
581        @ViewDebug.ExportedProperty()
582        public boolean isOverflowButton;
583
584        @ViewDebug.ExportedProperty()
585        public int cellsUsed;
586
587        @ViewDebug.ExportedProperty()
588        public int extraPixels;
589
590        @ViewDebug.ExportedProperty()
591        public boolean expandable;
592
593        @ViewDebug.ExportedProperty()
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(LayoutParams other) {
603            super((LinearLayout.LayoutParams) other);
604            isOverflowButton = other.isOverflowButton;
605        }
606
607        public LayoutParams(int width, int height) {
608            super(width, height);
609            isOverflowButton = false;
610        }
611
612        public LayoutParams(int width, int height, boolean isOverflowButton) {
613            super(width, height);
614            this.isOverflowButton = isOverflowButton;
615        }
616    }
617}
618