ActionMenuView.java revision bbbb8f39d1b1d1b317c5f9237f20fe6b1d9eb17f
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.appcompat.view.menu;
17
18import android.content.Context;
19import android.content.res.Configuration;
20import android.content.res.TypedArray;
21import android.support.appcompat.R;
22import android.util.AttributeSet;
23import android.view.Gravity;
24import android.view.View;
25import android.view.ViewDebug;
26import android.view.ViewGroup;
27import android.view.accessibility.AccessibilityEvent;
28import android.widget.LinearLayout;
29
30/**
31 * @hide
32 */
33public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
34  private static final String TAG = "ActionMenuView";
35
36  static final int MIN_CELL_SIZE = 56; // dips
37  static final int GENERATED_ITEM_PADDING = 4; // dips
38
39  private MenuBuilder mMenu;
40
41  private boolean mReserveOverflow;
42  private ActionMenuPresenter mPresenter;
43  private boolean mFormatItems;
44  private int mFormatItemsWidth;
45  private int mMinCellSize;
46  private int mGeneratedItemPadding;
47  private int mMeasuredExtraWidth;
48  private int mMaxItemHeight;
49
50  public ActionMenuView(Context context) {
51    this(context, null);
52  }
53
54  public ActionMenuView(Context context, AttributeSet attrs) {
55    super(context, attrs);
56    setBaselineAligned(false);
57    final float density = context.getResources().getDisplayMetrics().density;
58    mMinCellSize = (int) (MIN_CELL_SIZE * density);
59    mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density);
60
61    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar,
62        R.attr.actionBarStyle, 0);
63    mMaxItemHeight = a.getDimensionPixelSize(R.styleable.ActionBar_height, 0);
64    a.recycle();
65  }
66
67  public void setPresenter(ActionMenuPresenter presenter) {
68    mPresenter = presenter;
69  }
70
71  public boolean isExpandedFormat() {
72    return mFormatItems;
73  }
74
75  @Override
76  public void onConfigurationChanged(Configuration newConfig) {
77    super.onConfigurationChanged(newConfig);
78    mPresenter.updateMenuView(false);
79
80    if (mPresenter != null && mPresenter.isOverflowMenuShowing()) {
81      mPresenter.hideOverflowMenu();
82      mPresenter.showOverflowMenu();
83    }
84  }
85
86  @Override
87  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
88    // If we've been given an exact size to match, apply special formatting during layout.
89    final boolean wasFormatted = mFormatItems;
90    mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
91
92    if (wasFormatted != mFormatItems) {
93      mFormatItemsWidth = 0; // Reset this when switching modes
94    }
95
96    // Special formatting can change whether items can fit as action buttons.
97    // Kick the menu and update presenters when this changes.
98    final int widthSize = MeasureSpec.getMode(widthMeasureSpec);
99    if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) {
100      mFormatItemsWidth = widthSize;
101      mMenu.onItemsChanged(true);
102    }
103
104    if (mFormatItems) {
105      onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec);
106    } else {
107      // Previous measurement at exact format may have set margins - reset them.
108      final int childCount = getChildCount();
109      for (int i = 0; i < childCount; i++) {
110        final View child = getChildAt(i);
111        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
112        lp.leftMargin = lp.rightMargin = 0;
113      }
114      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
115    }
116  }
117
118  private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) {
119    // We already know the width mode is EXACTLY if we're here.
120    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
121    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
122    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
123
124    final int widthPadding = getPaddingLeft() + getPaddingRight();
125    final int heightPadding = getPaddingTop() + getPaddingBottom();
126
127    final int itemHeightSpec = heightMode == MeasureSpec.EXACTLY
128        ? MeasureSpec.makeMeasureSpec(heightSize - heightPadding, MeasureSpec.EXACTLY)
129        : MeasureSpec.makeMeasureSpec(
130            Math.min(mMaxItemHeight, heightSize - heightPadding), MeasureSpec.AT_MOST);
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          itemHeightSpec, 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      for (int i = 0; i < childCount; i++) {
315        final View child = getChildAt(i);
316        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
317
318        if (!lp.expanded) continue;
319
320        final int width = lp.cellsUsed * cellSize + lp.extraPixels;
321        child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
322            itemHeightSpec);
323      }
324    }
325
326    if (heightMode != MeasureSpec.EXACTLY) {
327      heightSize = maxChildHeight;
328    }
329
330    setMeasuredDimension(widthSize, heightSize);
331    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    final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
357        (ActionMenuItemView) child : null;
358    final boolean hasText = itemView != null && itemView.hasText();
359
360    int cellsUsed = 0;
361    if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) {
362      final int childWidthSpec = MeasureSpec.makeMeasureSpec(
363          cellSize * cellsRemaining, MeasureSpec.AT_MOST);
364      child.measure(childWidthSpec, childHeightSpec);
365
366      final int measuredWidth = child.getMeasuredWidth();
367      cellsUsed = measuredWidth / cellSize;
368      if (measuredWidth % cellSize != 0) cellsUsed++;
369      if (hasText && cellsUsed < 2) cellsUsed = 2;
370    }
371
372    final boolean expandable = !lp.isOverflowButton && hasText;
373    lp.expandable = expandable;
374
375    lp.cellsUsed = cellsUsed;
376    final int targetWidth = cellsUsed * cellSize;
377    child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
378        childHeightSpec);
379    return cellsUsed;
380  }
381
382  @Override
383  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
384    if (!mFormatItems) {
385      super.onLayout(changed, left, top, right, bottom);
386      return;
387    }
388
389    final int childCount = getChildCount();
390    final int midVertical = (top + bottom) / 2;
391    //final int dividerWidth = getDividerWidth();  No access outside framework
392    int overflowWidth = 0;
393    int nonOverflowWidth = 0;
394    int nonOverflowCount = 0;
395    int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
396    boolean hasOverflow = false;
397    for (int i = 0; i < childCount; i++) {
398      final View v = getChildAt(i);
399      if (v.getVisibility() == GONE) {
400        continue;
401      }
402
403      LayoutParams p = (LayoutParams) v.getLayoutParams();
404      if (p.isOverflowButton) {
405        overflowWidth = v.getMeasuredWidth();
406// No access to dividerWidth
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// No access to dividerWidth
425//        if (hasDividerBeforeChildAt(i)) {
426//          nonOverflowWidth += dividerWidth;
427//        }
428        nonOverflowCount++;
429      }
430    }
431
432    if (childCount == 1 && !hasOverflow) {
433      // Center a single child
434      final View v = getChildAt(0);
435      final int width = v.getMeasuredWidth();
436      final int height = v.getMeasuredHeight();
437      final int midHorizontal = (right - left) / 2;
438      final int l = midHorizontal - width / 2;
439      final int t = midVertical - height / 2;
440      v.layout(l, t, l + width, t + height);
441      return;
442    }
443
444    final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
445    final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0);
446
447    int startLeft = getPaddingLeft();
448    for (int i = 0; i < childCount; i++) {
449      final View v = getChildAt(i);
450      final LayoutParams lp = (LayoutParams) v.getLayoutParams();
451      if (v.getVisibility() == GONE || lp.isOverflowButton) {
452        continue;
453      }
454
455      startLeft += lp.leftMargin;
456      int width = v.getMeasuredWidth();
457      int height = v.getMeasuredHeight();
458      int t = midVertical - height / 2;
459      v.layout(startLeft, t, startLeft + width, t + height);
460      startLeft += width + lp.rightMargin + spacerSize;
461    }
462  }
463
464  @Override
465  public void onDetachedFromWindow() {
466    super.onDetachedFromWindow();
467    mPresenter.dismissPopupMenus();
468  }
469
470  public boolean isOverflowReserved() {
471    return mReserveOverflow;
472  }
473
474  public void setOverflowReserved(boolean reserveOverflow) {
475    mReserveOverflow = reserveOverflow;
476  }
477
478  @Override
479  protected LayoutParams generateDefaultLayoutParams() {
480    LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
481        LayoutParams.WRAP_CONTENT);
482    params.gravity = Gravity.CENTER_VERTICAL;
483    return params;
484  }
485
486  @Override
487  public LayoutParams generateLayoutParams(AttributeSet attrs) {
488    return new LayoutParams(getContext(), attrs);
489  }
490
491  @Override
492  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
493    if (p instanceof LayoutParams) {
494      LayoutParams result = new LayoutParams((LayoutParams) p);
495      if (result.gravity <= Gravity.NO_GRAVITY) {
496        result.gravity = Gravity.CENTER_VERTICAL;
497      }
498      return result;
499    }
500    return generateDefaultLayoutParams();
501  }
502
503  @Override
504  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
505    return p != null && p instanceof LayoutParams;
506  }
507
508  public LayoutParams generateOverflowButtonLayoutParams() {
509    LayoutParams result = generateDefaultLayoutParams();
510    result.isOverflowButton = true;
511    return result;
512  }
513
514  public boolean invokeItem(MenuItemImpl item) {
515    return mMenu.performItemAction(item, 0);
516  }
517
518  public int getWindowAnimations() {
519    return 0;
520  }
521
522  public void initialize(MenuBuilder menu) {
523    mMenu = menu;
524  }
525
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
539  public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
540    return false;
541  }
542
543  public interface ActionMenuChildView {
544    public boolean needsDividerBefore();
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
562    public LayoutParams(Context c, AttributeSet attrs) {
563      super(c, attrs);
564    }
565
566    public LayoutParams(LayoutParams other) {
567      super((LinearLayout.LayoutParams) other);
568      isOverflowButton = other.isOverflowButton;
569    }
570
571    public LayoutParams(int width, int height) {
572      super(width, height);
573      isOverflowButton = false;
574    }
575
576    public LayoutParams(int width, int height, boolean isOverflowButton) {
577      super(width, height);
578      this.isOverflowButton = isOverflowButton;
579    }
580  }
581}
582