blob: a14e98d61f2e5ab7dbb2ed8967ed27ba1569d039 [file] [log] [blame]
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001/*
2 * Copyright (C) 2015 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
17package com.android.internal.widget;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.graphics.Color;
25import android.graphics.Point;
26import android.graphics.Rect;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010027import android.graphics.Region;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000028import android.graphics.drawable.ColorDrawable;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010029import android.util.Size;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000030import android.view.Gravity;
31import android.view.LayoutInflater;
32import android.view.Menu;
33import android.view.MenuItem;
34import android.view.View;
35import android.view.View.MeasureSpec;
36import android.view.ViewGroup;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010037import android.view.ViewTreeObserver;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000038import android.view.Window;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010039import android.view.WindowManager;
40import android.view.animation.Animation;
41import android.view.animation.AnimationSet;
42import android.view.animation.Transformation;
43import android.widget.AdapterView;
44import android.widget.ArrayAdapter;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000045import android.widget.Button;
46import android.widget.ImageButton;
47import android.widget.LinearLayout;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010048import android.widget.ListView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000049import android.widget.PopupWindow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010050import android.widget.TextView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000051
52import java.util.ArrayList;
53import java.util.LinkedList;
54import java.util.List;
55
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010056import com.android.internal.R;
57import com.android.internal.util.Preconditions;
58
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000059/**
60 * A floating toolbar for showing contextual menu items.
61 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
62 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
63 * The horizontal toolbar morphs into the vertical overflow view.
64 */
65public final class FloatingToolbar {
66
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010067 // This class is responsible for the public API of the floating toolbar.
68 // It delegates rendering operations to the FloatingToolbarPopup.
69
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000070 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
71 new MenuItem.OnMenuItemClickListener() {
72 @Override
73 public boolean onMenuItemClick(MenuItem item) {
74 return false;
75 }
76 };
77
78 private final Context mContext;
79 private final FloatingToolbarPopup mPopup;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000080
81 private final Rect mContentRect = new Rect();
82 private final Point mCoordinates = new Point();
83
84 private Menu mMenu;
85 private List<CharSequence> mShowingTitles = new ArrayList<CharSequence>();
86 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000087
88 private int mSuggestedWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010089 private boolean mWidthChanged = true;
90 private int mOverflowDirection;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000091
92 /**
93 * Initializes a floating toolbar.
94 */
95 public FloatingToolbar(Context context, Window window) {
96 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010097 mPopup = new FloatingToolbarPopup(window.getDecorView());
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000098 }
99
100 /**
101 * Sets the menu to be shown in this floating toolbar.
102 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
103 * toolbar.
104 */
105 public FloatingToolbar setMenu(Menu menu) {
106 mMenu = Preconditions.checkNotNull(menu);
107 return this;
108 }
109
110 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100111 * Sets the custom listener for invocation of menu items in this floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000112 */
113 public FloatingToolbar setOnMenuItemClickListener(
114 MenuItem.OnMenuItemClickListener menuItemClickListener) {
115 if (menuItemClickListener != null) {
116 mMenuItemClickListener = menuItemClickListener;
117 } else {
118 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
119 }
120 return this;
121 }
122
123 /**
124 * Sets the content rectangle. This is the area of the interesting content that this toolbar
125 * should avoid obstructing.
126 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
127 * toolbar.
128 */
129 public FloatingToolbar setContentRect(Rect rect) {
130 mContentRect.set(Preconditions.checkNotNull(rect));
131 return this;
132 }
133
134 /**
135 * Sets the suggested width of this floating toolbar.
136 * The actual width will be about this size but there are no guarantees that it will be exactly
137 * the suggested width.
138 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
139 * toolbar.
140 */
141 public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100142 // Check if there's been a substantial width spec change.
143 int difference = Math.abs(suggestedWidth - mSuggestedWidth);
144 mWidthChanged = difference > (mSuggestedWidth * 0.2);
145
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000146 mSuggestedWidth = suggestedWidth;
147 return this;
148 }
149
150 /**
151 * Shows this floating toolbar.
152 */
153 public FloatingToolbar show() {
154 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100155 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000156 mPopup.dismiss();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100157 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000158 mShowingTitles = getMenuItemTitles(menuItems);
159 }
160 refreshCoordinates();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100161 mPopup.setOverflowDirection(mOverflowDirection);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000162 mPopup.updateCoordinates(mCoordinates.x, mCoordinates.y);
163 if (!mPopup.isShowing()) {
164 mPopup.show(mCoordinates.x, mCoordinates.y);
165 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100166 mWidthChanged = false;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000167 return this;
168 }
169
170 /**
171 * Updates this floating toolbar to reflect recent position and view updates.
172 * NOTE: This method is a no-op if the toolbar isn't showing.
173 */
174 public FloatingToolbar updateLayout() {
175 if (mPopup.isShowing()) {
176 // show() performs all the logic we need here.
177 show();
178 }
179 return this;
180 }
181
182 /**
183 * Dismisses this floating toolbar.
184 */
185 public void dismiss() {
186 mPopup.dismiss();
187 }
188
189 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100190 * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
191 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
192 */
193 public void hide() {
194 mPopup.hide();
195 }
196
197 /**
198 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000199 */
200 public boolean isShowing() {
201 return mPopup.isShowing();
202 }
203
204 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100205 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
206 */
207 public boolean isHidden() {
208 return mPopup.isHidden();
209 }
210
211 /**
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000212 * Refreshes {@link #mCoordinates} with values based on {@link #mContentRect}.
213 */
214 private void refreshCoordinates() {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100215 int x = mContentRect.centerX() - mPopup.getWidth() / 2;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000216 int y;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100217 if (mContentRect.top > mPopup.getHeight()) {
218 y = mContentRect.top - mPopup.getHeight();
219 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100220 } else if (mContentRect.top > mPopup.getToolbarHeightWithVerticalMargin()) {
221 y = mContentRect.top - mPopup.getToolbarHeightWithVerticalMargin();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100222 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000223 } else {
224 y = mContentRect.bottom;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100225 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000226 }
227 mCoordinates.set(x, y);
228 }
229
230 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100231 * Returns true if this floating toolbar is currently showing the specified menu items.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000232 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100233 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
234 return mShowingTitles.equals(getMenuItemTitles(menuItems));
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000235 }
236
237 /**
238 * Returns the visible and enabled menu items in the specified menu.
239 * This method is recursive.
240 */
241 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
242 List<MenuItem> menuItems = new ArrayList<MenuItem>();
243 for (int i = 0; (menu != null) && (i < menu.size()); i++) {
244 MenuItem menuItem = menu.getItem(i);
245 if (menuItem.isVisible() && menuItem.isEnabled()) {
246 Menu subMenu = menuItem.getSubMenu();
247 if (subMenu != null) {
248 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
249 } else {
250 menuItems.add(menuItem);
251 }
252 }
253 }
254 return menuItems;
255 }
256
257 private List<CharSequence> getMenuItemTitles(List<MenuItem> menuItems) {
258 List<CharSequence> titles = new ArrayList<CharSequence>();
259 for (MenuItem menuItem : menuItems) {
260 titles.add(menuItem.getTitle());
261 }
262 return titles;
263 }
264
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000265
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100266 /**
267 * A popup window used by the floating toolbar.
268 *
269 * This class is responsible for the rendering/animation of the floating toolbar.
270 * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
271 * It delegates specific panel functionality to the appropriate panel.
272 */
273 private static final class FloatingToolbarPopup {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000274
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100275 public static final int OVERFLOW_DIRECTION_UP = 0;
276 public static final int OVERFLOW_DIRECTION_DOWN = 1;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000277
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100278 private final View mParent;
279 private final PopupWindow mPopupWindow;
280 private final ViewGroup mContentContainer;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100281 private final int mMarginHorizontal;
282 private final int mMarginVertical;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000283
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100284 private final Animation.AnimationListener mOnOverflowOpened =
285 new Animation.AnimationListener() {
286 @Override
287 public void onAnimationStart(Animation animation) {}
288
289 @Override
290 public void onAnimationEnd(Animation animation) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100291 setOverflowPanelAsContent();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100292 mOverflowPanel.fadeIn(true);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100293 }
294
295 @Override
296 public void onAnimationRepeat(Animation animation) {}
297 };
298 private final Animation.AnimationListener mOnOverflowClosed =
299 new Animation.AnimationListener() {
300 @Override
301 public void onAnimationStart(Animation animation) {}
302
303 @Override
304 public void onAnimationEnd(Animation animation) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100305 setMainPanelAsContent();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100306 mMainPanel.fadeIn(true);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100307 }
308
309 @Override
310 public void onAnimationRepeat(Animation animation) {
311 }
312 };
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100313 private final AnimatorSet mShowAnimation;
314 private final AnimatorSet mDismissAnimation;
315 private final AnimatorSet mHideAnimation;
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100316 private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true) {
317 @Override
318 public void cancel() {
319 if (hasStarted() && !hasEnded()) {
320 super.cancel();
321 setOverflowPanelAsContent();
322 }
323 }
324 };
325 private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true) {
326 @Override
327 public void cancel() {
328 if (hasStarted() && !hasEnded()) {
329 super.cancel();
330 setMainPanelAsContent();
331 }
332 }
333 };
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100334
335 private final Runnable mOpenOverflow = new Runnable() {
336 @Override
337 public void run() {
338 openOverflow();
339 }
340 };
341 private final Runnable mCloseOverflow = new Runnable() {
342 @Override
343 public void run() {
344 closeOverflow();
345 }
346 };
347
348 private final Region mTouchableRegion = new Region();
349
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100350 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
351 private boolean mHidden; // tracks whether this popup is hidden or hiding.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100352
353 private FloatingToolbarOverflowPanel mOverflowPanel;
354 private FloatingToolbarMainPanel mMainPanel;
355 private int mOverflowDirection;
356
357 /**
358 * Initializes a new floating toolbar popup.
359 *
360 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
361 * from.
362 */
363 public FloatingToolbarPopup(View parent) {
364 mParent = Preconditions.checkNotNull(parent);
365 mContentContainer = createContentContainer(parent.getContext());
366 mPopupWindow = createPopupWindow(mContentContainer);
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100367 mShowAnimation = createGrowFadeInFromBottom(mContentContainer);
368 mDismissAnimation = createShrinkFadeOutFromBottomAnimation(
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100369 mContentContainer,
370 new AnimatorListenerAdapter() {
371 @Override
372 public void onAnimationEnd(Animator animation) {
373 mPopupWindow.dismiss();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100374 mContentContainer.removeAllViews();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100375 }
376 });
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100377 mHideAnimation = createShrinkFadeOutFromBottomAnimation(
378 mContentContainer,
379 new AnimatorListenerAdapter() {
380 @Override
381 public void onAnimationEnd(Animator animation) {
382 mPopupWindow.dismiss();
383 }
384 });
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100385 // Make the touchable area of this popup be the area specified by mTouchableRegion.
386 mPopupWindow.getContentView()
387 .getRootView()
388 .getViewTreeObserver()
389 .addOnComputeInternalInsetsListener(
390 new ViewTreeObserver.OnComputeInternalInsetsListener() {
391 public void onComputeInternalInsets(
392 ViewTreeObserver.InternalInsetsInfo info) {
393 info.contentInsets.setEmpty();
394 info.visibleInsets.setEmpty();
395 info.touchableRegion.set(mTouchableRegion);
396 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
397 .TOUCHABLE_INSETS_REGION);
398 }
399 });
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100400 mMarginHorizontal = parent.getResources()
401 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
402 mMarginVertical = parent.getResources()
403 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100404 }
405
406 /**
407 * Lays out buttons for the specified menu items.
408 */
409 public void layoutMenuItems(List<MenuItem> menuItems,
410 MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth) {
411 mContentContainer.removeAllViews();
412 if (mMainPanel == null) {
413 mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow);
414 }
415 List<MenuItem> overflowMenuItems =
416 mMainPanel.layoutMenuItems(menuItems, suggestedWidth);
417 mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
418 if (!overflowMenuItems.isEmpty()) {
419 if (mOverflowPanel == null) {
420 mOverflowPanel =
421 new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow);
422 }
423 mOverflowPanel.setMenuItems(overflowMenuItems);
424 mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
425 }
426 updatePopupSize();
427 }
428
429 /**
430 * Shows this popup at the specified coordinates.
431 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
432 */
433 public void show(int x, int y) {
434 if (isShowing()) {
435 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000436 }
437
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100438 mHidden = false;
439 mDismissed = false;
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100440 cancelAllAnimations();
441 // Make sure a panel is set as the content.
442 if (mContentContainer.getChildCount() == 0) {
443 setMainPanelAsContent();
444 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100445 preparePopupContent();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100446 // If we're yet to show the popup, set the container visibility to zero.
447 // The "show" animation will make this visible.
448 mContentContainer.setAlpha(0);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100449 mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, x, y);
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100450 runShowAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100451 }
452
453 /**
454 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
455 */
456 public void dismiss() {
457 if (!isShowing()) {
458 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000459 }
460
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100461 mHidden = false;
462 mDismissed = true;
463 runDismissAnimation();
464 setZeroTouchableSurface();
465 }
466
467 /**
468 * Hides this popup. This is a no-op if this popup is not showing.
469 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
470 */
471 public void hide() {
472 if (!isShowing()) {
473 return;
474 }
475
476 mHidden = true;
477 runHideAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100478 setZeroTouchableSurface();
479 }
480
481 /**
482 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
483 */
484 public boolean isShowing() {
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100485 return mPopupWindow.isShowing() && !mDismissed && !mHidden;
486 }
487
488 /**
489 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
490 */
491 public boolean isHidden() {
492 return mHidden;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100493 }
494
495 /**
496 * Updates the coordinates of this popup.
497 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100498 * This is a no-op if this popup is not showing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100499 */
500 public void updateCoordinates(int x, int y) {
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100501 if (!isShowing()) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100502 return;
503 }
504
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100505 cancelAllAnimations();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100506 preparePopupContent();
507 mPopupWindow.update(x, y, getWidth(), getHeight());
508 }
509
510 /**
511 * Sets the direction in which the overflow will open. i.e. up or down.
512 *
513 * @param overflowDirection Either {@link #OVERFLOW_DIRECTION_UP}
514 * or {@link #OVERFLOW_DIRECTION_DOWN}.
515 */
516 public void setOverflowDirection(int overflowDirection) {
517 mOverflowDirection = overflowDirection;
518 if (mOverflowPanel != null) {
519 mOverflowPanel.setOverflowDirection(mOverflowDirection);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000520 }
521 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100522
523 /**
524 * Returns the width of this popup.
525 */
526 public int getWidth() {
527 return mPopupWindow.getWidth();
528 }
529
530 /**
531 * Returns the height of this popup.
532 */
533 public int getHeight() {
534 return mPopupWindow.getHeight();
535 }
536
537 /**
538 * Returns the context this popup is running in.
539 */
540 public Context getContext() {
541 return mContentContainer.getContext();
542 }
543
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100544 int getToolbarHeightWithVerticalMargin() {
545 return getEstimatedToolbarHeight(mParent.getContext()) + mMarginVertical * 2;
546 }
547
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100548 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100549 * Performs the "show" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100550 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100551 private void runShowAnimation() {
552 mShowAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100553 }
554
555 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100556 * Performs the "dismiss" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100557 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100558 private void runDismissAnimation() {
559 mDismissAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100560 }
561
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100562 /**
563 * Performs the "hide" animation on the floating popup.
564 */
565 private void runHideAnimation() {
566 mHideAnimation.start();
567 }
568
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100569 private void cancelAllAnimations() {
570 mShowAnimation.cancel();
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100571 mDismissAnimation.cancel();
572 mHideAnimation.cancel();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100573 mOpenOverflowAnimation.cancel();
574 mCloseOverflowAnimation.cancel();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100575 }
576
577 /**
578 * Opens the floating toolbar overflow.
579 * This method should not be called if menu items have not been laid out with
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100580 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100581 *
582 * @throws IllegalStateException if called when menu items have not been laid out.
583 */
584 private void openOverflow() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100585 Preconditions.checkState(mMainPanel != null);
586 Preconditions.checkState(mOverflowPanel != null);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100587
588 mMainPanel.fadeOut(true);
589 Size overflowPanelSize = mOverflowPanel.measure();
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100590 final int targetWidth = overflowPanelSize.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100591 final int targetHeight = overflowPanelSize.getHeight();
592 final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
593 final int startWidth = mContentContainer.getWidth();
594 final int startHeight = mContentContainer.getHeight();
595 final float startY = mContentContainer.getY();
596 final float right = mContentContainer.getX() + mContentContainer.getWidth();
597 Animation widthAnimation = new Animation() {
598 @Override
599 protected void applyTransformation(float interpolatedTime, Transformation t) {
600 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
601 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
602 params.width = startWidth + deltaWidth;
603 mContentContainer.setLayoutParams(params);
604 mContentContainer.setX(right - mContentContainer.getWidth());
605 }
606 };
607 Animation heightAnimation = new Animation() {
608 @Override
609 protected void applyTransformation(float interpolatedTime, Transformation t) {
610 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
611 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
612 params.height = startHeight + deltaHeight;
613 mContentContainer.setLayoutParams(params);
614 if (morphUpwards) {
615 float y = startY - (mContentContainer.getHeight() - startHeight);
616 mContentContainer.setY(y);
617 }
618 }
619 };
620 widthAnimation.setDuration(240);
621 heightAnimation.setDuration(180);
622 heightAnimation.setStartOffset(60);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100623 mOpenOverflowAnimation.getAnimations().clear();
624 mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
625 mOpenOverflowAnimation.addAnimation(widthAnimation);
626 mOpenOverflowAnimation.addAnimation(heightAnimation);
627 mContentContainer.startAnimation(mOpenOverflowAnimation);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100628 }
629
630 /**
631 * Opens the floating toolbar overflow.
632 * This method should not be called if menu items have not been laid out with
633 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
634 *
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100635 * @throws IllegalStateException if called when menu items have not been laid out.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100636 */
637 private void closeOverflow() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100638 Preconditions.checkState(mMainPanel != null);
639 Preconditions.checkState(mOverflowPanel != null);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100640
641 mOverflowPanel.fadeOut(true);
642 Size mainPanelSize = mMainPanel.measure();
643 final int targetWidth = mainPanelSize.getWidth();
644 final int targetHeight = mainPanelSize.getHeight();
645 final int startWidth = mContentContainer.getWidth();
646 final int startHeight = mContentContainer.getHeight();
647 final float right = mContentContainer.getX() + mContentContainer.getWidth();
648 final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
649 final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
650 Animation widthAnimation = new Animation() {
651 @Override
652 protected void applyTransformation(float interpolatedTime, Transformation t) {
653 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
654 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
655 params.width = startWidth + deltaWidth;
656 mContentContainer.setLayoutParams(params);
657 mContentContainer.setX(right - mContentContainer.getWidth());
658 }
659 };
660 Animation heightAnimation = new Animation() {
661 @Override
662 protected void applyTransformation(float interpolatedTime, Transformation t) {
663 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
664 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
665 params.height = startHeight + deltaHeight;
666 mContentContainer.setLayoutParams(params);
667 if (morphedUpwards) {
668 mContentContainer.setY(bottom - mContentContainer.getHeight());
669 }
670 }
671 };
672 widthAnimation.setDuration(150);
673 widthAnimation.setStartOffset(150);
674 heightAnimation.setDuration(210);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100675 mCloseOverflowAnimation.getAnimations().clear();
676 mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
677 mCloseOverflowAnimation.addAnimation(widthAnimation);
678 mCloseOverflowAnimation.addAnimation(heightAnimation);
679 mContentContainer.startAnimation(mCloseOverflowAnimation);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100680 }
681
682 /**
683 * Prepares the content container for show and update calls.
684 */
685 private void preparePopupContent() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100686 // Reset visibility.
687 if (mMainPanel != null) {
688 mMainPanel.fadeIn(false);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100689 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100690 if (mOverflowPanel != null) {
691 mOverflowPanel.fadeIn(false);
692 }
693
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100694 // Reset position.
695 if (mMainPanel != null
696 && mContentContainer.getChildAt(0) == mMainPanel.getView()) {
697 positionMainPanel();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100698 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100699 if (mOverflowPanel != null
700 && mContentContainer.getChildAt(0) == mOverflowPanel.getView()) {
701 positionOverflowPanel();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100702 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100703 }
704
705 /**
706 * Sets the current content to be the main view panel.
707 */
708 private void setMainPanelAsContent() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100709 // This should never be called if the main panel has not been initialized.
710 Preconditions.checkNotNull(mMainPanel);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100711 mContentContainer.removeAllViews();
712 Size mainPanelSize = mMainPanel.measure();
713 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
714 params.width = mainPanelSize.getWidth();
715 params.height = mainPanelSize.getHeight();
716 mContentContainer.setLayoutParams(params);
717 mContentContainer.addView(mMainPanel.getView());
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100718 setContentAreaAsTouchableSurface();
719 }
720
721 /**
722 * Sets the current content to be the overflow view panel.
723 */
724 private void setOverflowPanelAsContent() {
725 // This should never be called if the overflow panel has not been initialized.
726 Preconditions.checkNotNull(mOverflowPanel);
727 mContentContainer.removeAllViews();
728 Size overflowPanelSize = mOverflowPanel.measure();
729 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
730 params.width = overflowPanelSize.getWidth();
731 params.height = overflowPanelSize.getHeight();
732 mContentContainer.setLayoutParams(params);
733 mContentContainer.addView(mOverflowPanel.getView());
734 setContentAreaAsTouchableSurface();
735 }
736
737 /**
738 * Places the main view panel at the appropriate resting coordinates.
739 */
740 private void positionMainPanel() {
741 Preconditions.checkNotNull(mMainPanel);
742 float x = mPopupWindow.getWidth()
743 - (mMainPanel.getView().getMeasuredWidth() + mMarginHorizontal);
744 mContentContainer.setX(x);
745
746 float y = mMarginVertical;
747 if (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
748 y = getHeight()
749 - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
750 }
751 mContentContainer.setY(y);
752 setContentAreaAsTouchableSurface();
753 }
754
755 /**
756 * Places the main view panel at the appropriate resting coordinates.
757 */
758 private void positionOverflowPanel() {
759 Preconditions.checkNotNull(mOverflowPanel);
760 float x = mPopupWindow.getWidth()
761 - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
762 mContentContainer.setX(x);
763 mContentContainer.setY(mMarginVertical);
764 setContentAreaAsTouchableSurface();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100765 }
766
767 private void updatePopupSize() {
768 int width = 0;
769 int height = 0;
770 if (mMainPanel != null) {
771 Size mainPanelSize = mMainPanel.measure();
772 width = mainPanelSize.getWidth();
773 height = mainPanelSize.getHeight();
774 }
775 if (mOverflowPanel != null) {
776 Size overflowPanelSize = mOverflowPanel.measure();
777 width = Math.max(width, overflowPanelSize.getWidth());
778 height = Math.max(height, overflowPanelSize.getHeight());
779 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100780 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
781 mPopupWindow.setHeight(height + mMarginVertical * 2);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100782 }
783
784 /**
785 * Sets the touchable region of this popup to be zero. This means that all touch events on
786 * this popup will go through to the surface behind it.
787 */
788 private void setZeroTouchableSurface() {
789 mTouchableRegion.setEmpty();
790 }
791
792 /**
793 * Sets the touchable region of this popup to be the area occupied by its content.
794 */
795 private void setContentAreaAsTouchableSurface() {
796 if (!mPopupWindow.isShowing()) {
797 mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
798 }
799 int width = mContentContainer.getMeasuredWidth();
800 int height = mContentContainer.getMeasuredHeight();
801 mTouchableRegion.set(
802 (int) mContentContainer.getX(),
803 (int) mContentContainer.getY(),
804 (int) mContentContainer.getX() + width,
805 (int) mContentContainer.getY() + height);
806 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000807 }
808
809 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100810 * A widget that holds the primary menu items in the floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000811 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100812 private static final class FloatingToolbarMainPanel {
813
814 private final Context mContext;
815 private final ViewGroup mContentView;
816 private final View.OnClickListener mMenuItemButtonOnClickListener =
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000817 new View.OnClickListener() {
818 @Override
819 public void onClick(View v) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100820 if (v.getTag() instanceof MenuItem) {
821 if (mOnMenuItemClickListener != null) {
822 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
823 }
824 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000825 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100826 };
827 private final ViewFader viewFader;
828 private final Runnable mOpenOverflow;
829
830 private View mOpenOverflowButton;
831 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
832
833 /**
834 * Initializes a floating toolbar popup main view panel.
835 *
836 * @param context
837 * @param openOverflow The code that opens the toolbar popup overflow.
838 */
839 public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
840 mContext = Preconditions.checkNotNull(context);
841 mContentView = new LinearLayout(context);
842 viewFader = new ViewFader(mContentView);
843 mOpenOverflow = Preconditions.checkNotNull(openOverflow);
844 }
845
846 /**
847 * Fits as many menu items in the main panel and returns a list of the menu items that
848 * were not fit in.
849 *
850 * @return The menu items that are not included in this main panel.
851 */
852 public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) {
853 final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth)
854 // Reserve space for the "open overflow" button.
855 - getEstimatedOpenOverflowButtonWidth(mContext);
856
857 int availableWidth = toolbarWidth;
858 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
859
860 mContentView.removeAllViews();
861
862 boolean isFirstItem = true;
863 while (!remainingMenuItems.isEmpty()) {
864 final MenuItem menuItem = remainingMenuItems.peek();
865 Button menuItemButton = createMenuItemButton(mContext, menuItem);
866
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100867 // Adding additional start padding for the first button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100868 if (isFirstItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100869 menuItemButton.setPaddingRelative(
870 (int) (1.5 * menuItemButton.getPaddingStart()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100871 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100872 menuItemButton.getPaddingEnd(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100873 menuItemButton.getPaddingBottom());
874 isFirstItem = false;
875 }
876
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100877 // Adding additional end padding for the last button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100878 if (remainingMenuItems.size() == 1) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100879 menuItemButton.setPaddingRelative(
880 menuItemButton.getPaddingStart(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100881 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100882 (int) (1.5 * menuItemButton.getPaddingEnd()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100883 menuItemButton.getPaddingBottom());
884 }
885
886 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
887 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
888 if (menuItemButtonWidth <= availableWidth) {
889 menuItemButton.setTag(menuItem);
890 menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
891 mContentView.addView(menuItemButton);
892 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
893 params.width = menuItemButtonWidth;
894 menuItemButton.setLayoutParams(params);
895 availableWidth -= menuItemButtonWidth;
896 remainingMenuItems.pop();
897 } else {
898 if (mOpenOverflowButton == null) {
899 mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext)
900 .inflate(R.layout.floating_popup_open_overflow_button, null);
901 mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
902 @Override
903 public void onClick(View v) {
904 if (mOpenOverflowButton != null) {
905 mOpenOverflow.run();
906 }
907 }
908 });
909 }
910 mContentView.addView(mOpenOverflowButton);
911 break;
912 }
913 }
914 return remainingMenuItems;
915 }
916
917 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
918 mOnMenuItemClickListener = listener;
919 }
920
921 public View getView() {
922 return mContentView;
923 }
924
925 public void fadeIn(boolean animate) {
926 viewFader.fadeIn(animate);
927 }
928
929 public void fadeOut(boolean animate) {
930 viewFader.fadeOut(animate);
931 }
932
933 /**
934 * Returns how big this panel's view should be.
935 * This method should only be called when the view has not been attached to a parent
936 * otherwise it will throw an illegal state.
937 */
938 public Size measure() throws IllegalStateException {
939 Preconditions.checkState(mContentView.getParent() == null);
940 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
941 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
942 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000943 }
944
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100945
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000946 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100947 * A widget that holds the overflow items in the floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000948 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100949 private static final class FloatingToolbarOverflowPanel {
950
951 private final LinearLayout mContentView;
952 private final ViewGroup mBackButtonContainer;
953 private final View mBackButton;
954 private final ListView mListView;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100955 private final TextView mListViewItemWidthCalculator;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100956 private final ViewFader mViewFader;
957 private final Runnable mCloseOverflow;
958
959 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100960 private int mOverflowWidth = 0;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100961
962 /**
963 * Initializes a floating toolbar popup overflow view panel.
964 *
965 * @param context
966 * @param closeOverflow The code that closes the toolbar popup's overflow.
967 */
968 public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
969 mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
970
971 mContentView = new LinearLayout(context);
972 mContentView.setOrientation(LinearLayout.VERTICAL);
973 mViewFader = new ViewFader(mContentView);
974
975 mBackButton = LayoutInflater.from(context)
976 .inflate(R.layout.floating_popup_close_overflow_button, null);
977 mBackButton.setOnClickListener(new View.OnClickListener() {
978 @Override
979 public void onClick(View v) {
980 mCloseOverflow.run();
981 }
982 });
983 mBackButtonContainer = new LinearLayout(context);
984 mBackButtonContainer.addView(mBackButton);
985
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100986 mListView = createOverflowListView();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100987 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
988 @Override
989 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
990 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
991 if (mOnMenuItemClickListener != null) {
992 mOnMenuItemClickListener.onMenuItemClick(menuItem);
993 }
994 }
995 });
996
997 mContentView.addView(mListView);
998 mContentView.addView(mBackButtonContainer);
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100999
1000 mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1001 mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1002 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001003 }
1004
1005 /**
1006 * Sets the menu items to be displayed in the overflow.
1007 */
1008 public void setMenuItems(List<MenuItem> menuItems) {
1009 ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1010 overflowListViewAdapter.clear();
1011 overflowListViewAdapter.addAll(menuItems);
1012 setListViewHeight();
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001013 setOverflowWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001014 }
1015
1016 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1017 mOnMenuItemClickListener = listener;
1018 }
1019
1020 /**
1021 * Notifies the overflow of the current direction in which the overflow will be opened.
1022 *
1023 * @param overflowDirection {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1024 * or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1025 */
1026 public void setOverflowDirection(int overflowDirection) {
1027 mContentView.removeView(mBackButtonContainer);
1028 int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1029 mContentView.addView(mBackButtonContainer, index);
1030 }
1031
1032 /**
1033 * Returns the content view of the overflow.
1034 */
1035 public View getView() {
1036 return mContentView;
1037 }
1038
1039 public void fadeIn(boolean animate) {
1040 mViewFader.fadeIn(animate);
1041 }
1042
1043 public void fadeOut(boolean animate) {
1044 mViewFader.fadeOut(animate);
1045 }
1046
1047 /**
1048 * Returns how big this panel's view should be.
1049 * This method should only be called when the view has not been attached to a parent.
1050 *
1051 * @throws IllegalStateException
1052 */
1053 public Size measure() {
1054 Preconditions.checkState(mContentView.getParent() == null);
1055 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1056 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1057 }
1058
1059 private void setListViewHeight() {
1060 int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1061 int height = mListView.getAdapter().getCount() * itemHeight;
1062 int maxHeight = mContentView.getContext().getResources().
1063 getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
1064 ViewGroup.LayoutParams params = mListView.getLayoutParams();
1065 params.height = Math.min(height, maxHeight);
1066 mListView.setLayoutParams(params);
1067 }
1068
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001069 private int setOverflowWidth() {
1070 for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1071 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1072 Preconditions.checkNotNull(menuItem);
1073 mListViewItemWidthCalculator.setText(menuItem.getTitle());
1074 mListViewItemWidthCalculator.measure(
1075 MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1076 mOverflowWidth = Math.max(
1077 mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1078 }
1079 return mOverflowWidth;
1080 }
1081
1082 private ListView createOverflowListView() {
1083 final Context context = mContentView.getContext();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001084 final ListView overflowListView = new ListView(context);
1085 overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1086 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1087 overflowListView.setDivider(null);
1088 overflowListView.setDividerHeight(0);
1089 final ArrayAdapter overflowListViewAdapter =
1090 new ArrayAdapter<MenuItem>(context, 0) {
1091 @Override
1092 public View getView(int position, View convertView, ViewGroup parent) {
1093 TextView menuButton;
1094 if (convertView != null) {
1095 menuButton = (TextView) convertView;
1096 } else {
1097 menuButton = createOverflowMenuItemButton(context);
1098 }
1099 MenuItem menuItem = getItem(position);
1100 menuButton.setText(menuItem.getTitle());
1101 menuButton.setContentDescription(menuItem.getTitle());
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001102 menuButton.setMinimumWidth(mOverflowWidth);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001103 return menuButton;
1104 }
1105 };
1106 overflowListView.setAdapter(overflowListViewAdapter);
1107 return overflowListView;
1108 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001109 }
1110
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001111
1112 /**
1113 * A helper for fading in or out a view.
1114 */
1115 private static final class ViewFader {
1116
1117 private static final int FADE_OUT_DURATION = 250;
1118 private static final int FADE_IN_DURATION = 150;
1119
1120 private final View mView;
1121 private final ObjectAnimator mFadeOutAnimation;
1122 private final ObjectAnimator mFadeInAnimation;
1123
1124 private ViewFader(View view) {
1125 mView = Preconditions.checkNotNull(view);
1126 mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1127 .setDuration(FADE_OUT_DURATION);
1128 mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1129 .setDuration(FADE_IN_DURATION);
1130 }
1131
1132 public void fadeIn(boolean animate) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001133 cancelFadeAnimations();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001134 if (animate) {
1135 mFadeInAnimation.start();
1136 } else {
1137 mView.setAlpha(1);
1138 }
1139 }
1140
1141 public void fadeOut(boolean animate) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001142 cancelFadeAnimations();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001143 if (animate) {
1144 mFadeOutAnimation.start();
1145 } else {
1146 mView.setAlpha(0);
1147 }
1148 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001149
1150 private void cancelFadeAnimations() {
1151 mFadeInAnimation.cancel();
1152 mFadeOutAnimation.cancel();
1153 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001154 }
1155
1156
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001157 /**
1158 * Creates and returns a menu button for the specified menu item.
1159 */
1160 private static Button createMenuItemButton(Context context, MenuItem menuItem) {
1161 Button menuItemButton = (Button) LayoutInflater.from(context)
1162 .inflate(R.layout.floating_popup_menu_button, null);
1163 menuItemButton.setText(menuItem.getTitle());
1164 menuItemButton.setContentDescription(menuItem.getTitle());
1165 return menuItemButton;
1166 }
1167
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001168 /**
1169 * Creates and returns a styled floating toolbar overflow list view item.
1170 */
1171 private static TextView createOverflowMenuItemButton(Context context) {
1172 return (TextView) LayoutInflater.from(context)
1173 .inflate(R.layout.floating_popup_overflow_list_item, null);
1174 }
1175
1176 private static ViewGroup createContentContainer(Context context) {
1177 return (ViewGroup) LayoutInflater.from(context)
1178 .inflate(R.layout.floating_popup_container, null);
1179 }
1180
1181 private static PopupWindow createPopupWindow(View content) {
1182 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1183 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
Abodunrinwa Toki103d48e2015-04-16 17:27:48 +01001184 popupWindow.setWindowLayoutType(
1185 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001186 popupWindow.setAnimationStyle(0);
1187 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1188 content.setLayoutParams(new ViewGroup.LayoutParams(
1189 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1190 popupContentHolder.addView(content);
1191 return popupWindow;
1192 }
1193
1194 /**
1195 * Creates a "grow and fade in from the bottom" animation for the specified view.
1196 *
1197 * @param view The view to animate
1198 */
1199 private static AnimatorSet createGrowFadeInFromBottom(View view) {
1200 AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet();
1201 growFadeInFromBottomAnimation.playTogether(
1202 ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125),
1203 ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125),
1204 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75));
1205 growFadeInFromBottomAnimation.setStartDelay(50);
1206 return growFadeInFromBottomAnimation;
1207 }
1208
1209 /**
1210 * Creates a "shrink and fade out from bottom" animation for the specified view.
1211 *
1212 * @param view The view to animate
1213 * @param listener The animation listener
1214 */
1215 private static AnimatorSet createShrinkFadeOutFromBottomAnimation(
1216 View view, Animator.AnimatorListener listener) {
1217 AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet();
1218 shrinkFadeOutFromBottomAnimation.playTogether(
1219 ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125),
1220 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75));
1221 shrinkFadeOutFromBottomAnimation.setStartDelay(150);
1222 shrinkFadeOutFromBottomAnimation.addListener(listener);
1223 return shrinkFadeOutFromBottomAnimation;
1224 }
1225
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001226 private static int getEstimatedToolbarHeight(Context context) {
1227 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001228 }
1229
1230 private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1231 return context.getResources()
1232 .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1233 }
1234
1235 private static int getAdjustedToolbarWidth(Context context, int width) {
1236 if (width <= 0 || width > getScreenWidth(context)) {
1237 width = context.getResources()
1238 .getDimensionPixelSize(R.dimen.floating_toolbar_default_width);
1239 }
1240 return width;
1241 }
1242
1243 /**
1244 * Returns the device's screen width.
1245 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001246 private static int getScreenWidth(Context context) {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001247 return context.getResources().getDisplayMetrics().widthPixels;
1248 }
1249
1250 /**
1251 * Returns the device's screen height.
1252 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001253 private static int getScreenHeight(Context context) {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001254 return context.getResources().getDisplayMetrics().heightPixels;
1255 }
1256
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001257 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001258 * Returns value, restricted to the range min->max (inclusive).
1259 * If maximum is less than minimum, the result is undefined.
1260 *
1261 * @param value The value to clamp.
1262 * @param minimum The minimum value in the range.
1263 * @param maximum The maximum value in the range. Must not be less than minimum.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001264 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001265 private static int clamp(int value, int minimum, int maximum) {
1266 return Math.max(minimum, Math.min(value, maximum));
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001267 }
1268}