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