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