blob: ca6fe619f528d0756b1dba18035ce711c14b5a1e [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;
Clara Bayarri6bc12242015-06-16 18:04:55 +010023import android.content.ComponentCallbacks;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000024import android.content.Context;
Clara Bayarri6bc12242015-06-16 18:04:55 +010025import android.content.res.Configuration;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -070026import android.content.res.TypedArray;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000027import android.graphics.Color;
28import android.graphics.Point;
29import android.graphics.Rect;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010030import android.graphics.Region;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000031import android.graphics.drawable.ColorDrawable;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010032import android.text.TextUtils;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010033import android.util.Size;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -070034import android.view.ContextThemeWrapper;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000035import android.view.Gravity;
36import android.view.LayoutInflater;
37import android.view.Menu;
38import android.view.MenuItem;
39import android.view.View;
40import android.view.View.MeasureSpec;
41import android.view.ViewGroup;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010042import android.view.ViewTreeObserver;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000043import android.view.Window;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010044import android.view.WindowManager;
45import android.view.animation.Animation;
46import android.view.animation.AnimationSet;
47import android.view.animation.Transformation;
48import android.widget.AdapterView;
49import android.widget.ArrayAdapter;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000050import android.widget.Button;
51import android.widget.ImageButton;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010052import android.widget.ImageView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000053import android.widget.LinearLayout;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010054import android.widget.ListView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000055import android.widget.PopupWindow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010056import android.widget.TextView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000057
58import java.util.ArrayList;
59import java.util.LinkedList;
60import java.util.List;
61
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010062import com.android.internal.R;
63import com.android.internal.util.Preconditions;
64
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000065/**
66 * A floating toolbar for showing contextual menu items.
67 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
68 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
69 * The horizontal toolbar morphs into the vertical overflow view.
70 */
71public final class FloatingToolbar {
72
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010073 // This class is responsible for the public API of the floating toolbar.
74 // It delegates rendering operations to the FloatingToolbarPopup.
75
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000076 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
77 new MenuItem.OnMenuItemClickListener() {
78 @Override
79 public boolean onMenuItemClick(MenuItem item) {
80 return false;
81 }
82 };
83
84 private final Context mContext;
85 private final FloatingToolbarPopup mPopup;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000086
87 private final Rect mContentRect = new Rect();
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -070088 private final Rect mPreviousContentRect = new Rect();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000089
90 private Menu mMenu;
Abodunrinwa Toki50471322015-06-01 14:37:24 +010091 private List<Object> mShowingMenuItems = new ArrayList<Object>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000092 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000093
94 private int mSuggestedWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010095 private boolean mWidthChanged = true;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000096
Clara Bayarri6bc12242015-06-16 18:04:55 +010097 private final ComponentCallbacks mOrientationChangeHandler = new ComponentCallbacks() {
98 @Override
99 public void onConfigurationChanged(Configuration newConfig) {
100 if (mPopup.isShowing() && mPopup.viewPortHasChanged()) {
101 mWidthChanged = true;
102 updateLayout();
103 }
104 }
105
106 @Override
107 public void onLowMemory() {}
108 };
109
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000110 /**
111 * Initializes a floating toolbar.
112 */
113 public FloatingToolbar(Context context, Window window) {
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700114 Preconditions.checkNotNull(context);
115 Preconditions.checkNotNull(window);
116 mContext = applyDefaultTheme(context);
117 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000118 }
119
120 /**
121 * Sets the menu to be shown in this floating toolbar.
122 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
123 * toolbar.
124 */
125 public FloatingToolbar setMenu(Menu menu) {
126 mMenu = Preconditions.checkNotNull(menu);
127 return this;
128 }
129
130 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100131 * Sets the custom listener for invocation of menu items in this floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000132 */
133 public FloatingToolbar setOnMenuItemClickListener(
134 MenuItem.OnMenuItemClickListener menuItemClickListener) {
135 if (menuItemClickListener != null) {
136 mMenuItemClickListener = menuItemClickListener;
137 } else {
138 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
139 }
140 return this;
141 }
142
143 /**
144 * Sets the content rectangle. This is the area of the interesting content that this toolbar
145 * should avoid obstructing.
146 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
147 * toolbar.
148 */
149 public FloatingToolbar setContentRect(Rect rect) {
150 mContentRect.set(Preconditions.checkNotNull(rect));
151 return this;
152 }
153
154 /**
155 * Sets the suggested width of this floating toolbar.
156 * The actual width will be about this size but there are no guarantees that it will be exactly
157 * the suggested width.
158 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
159 * toolbar.
160 */
161 public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100162 // Check if there's been a substantial width spec change.
163 int difference = Math.abs(suggestedWidth - mSuggestedWidth);
164 mWidthChanged = difference > (mSuggestedWidth * 0.2);
165
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000166 mSuggestedWidth = suggestedWidth;
167 return this;
168 }
169
170 /**
171 * Shows this floating toolbar.
172 */
173 public FloatingToolbar show() {
Clara Bayarri6bc12242015-06-16 18:04:55 +0100174 mContext.unregisterComponentCallbacks(mOrientationChangeHandler);
175 mContext.registerComponentCallbacks(mOrientationChangeHandler);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000176 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100177 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000178 mPopup.dismiss();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100179 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
Abodunrinwa Toki50471322015-06-01 14:37:24 +0100180 mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000181 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000182 if (!mPopup.isShowing()) {
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100183 mPopup.show(mContentRect);
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -0700184 } else if (!mPreviousContentRect.equals(mContentRect)) {
185 mPopup.updateCoordinates(mContentRect);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000186 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100187 mWidthChanged = false;
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -0700188 mPreviousContentRect.set(mContentRect);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000189 return this;
190 }
191
192 /**
193 * Updates this floating toolbar to reflect recent position and view updates.
194 * NOTE: This method is a no-op if the toolbar isn't showing.
195 */
196 public FloatingToolbar updateLayout() {
197 if (mPopup.isShowing()) {
198 // show() performs all the logic we need here.
199 show();
200 }
201 return this;
202 }
203
204 /**
205 * Dismisses this floating toolbar.
206 */
207 public void dismiss() {
Clara Bayarri6bc12242015-06-16 18:04:55 +0100208 mContext.unregisterComponentCallbacks(mOrientationChangeHandler);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000209 mPopup.dismiss();
210 }
211
212 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100213 * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
214 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
215 */
216 public void hide() {
217 mPopup.hide();
218 }
219
220 /**
221 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000222 */
223 public boolean isShowing() {
224 return mPopup.isShowing();
225 }
226
227 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100228 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
229 */
230 public boolean isHidden() {
231 return mPopup.isHidden();
232 }
233
234 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100235 * Returns true if this floating toolbar is currently showing the specified menu items.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000236 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100237 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
Abodunrinwa Toki50471322015-06-01 14:37:24 +0100238 return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000239 }
240
241 /**
242 * Returns the visible and enabled menu items in the specified menu.
243 * This method is recursive.
244 */
245 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
246 List<MenuItem> menuItems = new ArrayList<MenuItem>();
247 for (int i = 0; (menu != null) && (i < menu.size()); i++) {
248 MenuItem menuItem = menu.getItem(i);
249 if (menuItem.isVisible() && menuItem.isEnabled()) {
250 Menu subMenu = menuItem.getSubMenu();
251 if (subMenu != null) {
252 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
253 } else {
254 menuItems.add(menuItem);
255 }
256 }
257 }
258 return menuItems;
259 }
260
Abodunrinwa Toki50471322015-06-01 14:37:24 +0100261 private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) {
262 List<Object> references = new ArrayList<Object>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000263 for (MenuItem menuItem : menuItems) {
Abodunrinwa Toki50471322015-06-01 14:37:24 +0100264 if (isIconOnlyMenuItem(menuItem)) {
265 references.add(menuItem.getIcon());
266 } else {
267 references.add(menuItem.getTitle());
268 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000269 }
Abodunrinwa Toki50471322015-06-01 14:37:24 +0100270 return references;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000271 }
272
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000273
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100274 /**
275 * A popup window used by the floating toolbar.
276 *
277 * This class is responsible for the rendering/animation of the floating toolbar.
278 * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
279 * It delegates specific panel functionality to the appropriate panel.
280 */
281 private static final class FloatingToolbarPopup {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000282
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100283 public static final int OVERFLOW_DIRECTION_UP = 0;
284 public static final int OVERFLOW_DIRECTION_DOWN = 1;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000285
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700286 private final Context mContext;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100287 private final View mParent;
288 private final PopupWindow mPopupWindow;
289 private final ViewGroup mContentContainer;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100290 private final int mMarginHorizontal;
291 private final int mMarginVertical;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000292
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100293 private final Animation.AnimationListener mOnOverflowOpened =
294 new Animation.AnimationListener() {
295 @Override
296 public void onAnimationStart(Animation animation) {}
297
298 @Override
299 public void onAnimationEnd(Animation animation) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100300 setOverflowPanelAsContent();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100301 mOverflowPanel.fadeIn(true);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100302 }
303
304 @Override
305 public void onAnimationRepeat(Animation animation) {}
306 };
307 private final Animation.AnimationListener mOnOverflowClosed =
308 new Animation.AnimationListener() {
309 @Override
310 public void onAnimationStart(Animation animation) {}
311
312 @Override
313 public void onAnimationEnd(Animation animation) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100314 setMainPanelAsContent();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100315 mMainPanel.fadeIn(true);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100316 }
317
318 @Override
319 public void onAnimationRepeat(Animation animation) {
320 }
321 };
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100322 private final AnimatorSet mDismissAnimation;
323 private final AnimatorSet mHideAnimation;
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -0700324 private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true);
325 private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100326
327 private final Runnable mOpenOverflow = new Runnable() {
328 @Override
329 public void run() {
330 openOverflow();
331 }
332 };
333 private final Runnable mCloseOverflow = new Runnable() {
334 @Override
335 public void run() {
336 closeOverflow();
337 }
338 };
339
Yohei Yukawabafc9082015-07-14 05:59:05 -0700340 private final Rect mViewPortOnScreen = new Rect();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700341 private final Point mCoordsOnWindow = new Point();
342 private final int[] mTmpCoords = new int[2];
Clara Bayarri6bc12242015-06-16 18:04:55 +0100343 private final Rect mTmpRect = new Rect();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100344
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100345 private final Region mTouchableRegion = new Region();
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100346 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
347 new ViewTreeObserver.OnComputeInternalInsetsListener() {
348 public void onComputeInternalInsets(
349 ViewTreeObserver.InternalInsetsInfo info) {
350 info.contentInsets.setEmpty();
351 info.visibleInsets.setEmpty();
352 info.touchableRegion.set(mTouchableRegion);
353 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
354 .TOUCHABLE_INSETS_REGION);
355 }
356 };
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100357
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100358 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
359 private boolean mHidden; // tracks whether this popup is hidden or hiding.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100360
361 private FloatingToolbarOverflowPanel mOverflowPanel;
362 private FloatingToolbarMainPanel mMainPanel;
363 private int mOverflowDirection;
364
365 /**
366 * Initializes a new floating toolbar popup.
367 *
368 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
369 * from.
370 */
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700371 public FloatingToolbarPopup(Context context, View parent) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100372 mParent = Preconditions.checkNotNull(parent);
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700373 mContext = Preconditions.checkNotNull(context);
374 mContentContainer = createContentContainer(context);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100375 mPopupWindow = createPopupWindow(mContentContainer);
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100376 mDismissAnimation = createExitAnimation(
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100377 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100378 150, // startDelay
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100379 new AnimatorListenerAdapter() {
380 @Override
381 public void onAnimationEnd(Animator animation) {
382 mPopupWindow.dismiss();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100383 mContentContainer.removeAllViews();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100384 }
385 });
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100386 mHideAnimation = createExitAnimation(
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100387 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100388 0, // startDelay
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100389 new AnimatorListenerAdapter() {
390 @Override
391 public void onAnimationEnd(Animator animation) {
392 mPopupWindow.dismiss();
393 }
394 });
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100395 mMarginHorizontal = parent.getResources()
396 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
397 mMarginVertical = parent.getResources()
398 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100399 }
400
401 /**
402 * Lays out buttons for the specified menu items.
403 */
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100404 public void layoutMenuItems(
405 List<MenuItem> menuItems,
406 MenuItem.OnMenuItemClickListener menuItemClickListener,
407 int suggestedWidth) {
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100408 Preconditions.checkNotNull(menuItems);
409
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100410 mContentContainer.removeAllViews();
411 if (mMainPanel == null) {
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700412 mMainPanel = new FloatingToolbarMainPanel(mContext, mOpenOverflow);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100413 }
414 List<MenuItem> overflowMenuItems =
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100415 mMainPanel.layoutMenuItems(menuItems, getToolbarWidth(suggestedWidth));
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100416 mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
417 if (!overflowMenuItems.isEmpty()) {
418 if (mOverflowPanel == null) {
419 mOverflowPanel =
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700420 new FloatingToolbarOverflowPanel(mContext, mCloseOverflow);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100421 }
422 mOverflowPanel.setMenuItems(overflowMenuItems);
423 mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
424 }
425 updatePopupSize();
426 }
427
428 /**
429 * Shows this popup at the specified coordinates.
430 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
431 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700432 public void show(Rect contentRectOnScreen) {
433 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100434
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100435 if (isShowing()) {
436 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000437 }
438
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100439 mHidden = false;
440 mDismissed = false;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100441 cancelDismissAndHideAnimations();
442 cancelOverflowAnimations();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100443
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100444 // Make sure a panel is set as the content.
445 if (mContentContainer.getChildCount() == 0) {
446 setMainPanelAsContent();
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100447 // If we're yet to show the popup, set the container visibility to zero.
448 // The "show" animation will make this visible.
449 mContentContainer.setAlpha(0);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100450 }
Yohei Yukawabafc9082015-07-14 05:59:05 -0700451 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100452 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700453 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700454 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
455 // specify the popup poision in screen coordinates.
Yohei Yukawa4b269972015-07-15 19:01:32 -0700456 mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x,
457 mCoordsOnWindow.y);
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100458 setTouchableSurfaceInsetsComputer();
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100459 runShowAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100460 }
461
462 /**
463 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
464 */
465 public void dismiss() {
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100466 if (mDismissed) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100467 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000468 }
469
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100470 mHidden = false;
471 mDismissed = true;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100472 mHideAnimation.cancel();
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100473 runDismissAnimation();
474 setZeroTouchableSurface();
475 }
476
477 /**
478 * Hides this popup. This is a no-op if this popup is not showing.
479 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
480 */
481 public void hide() {
482 if (!isShowing()) {
483 return;
484 }
485
486 mHidden = true;
487 runHideAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100488 setZeroTouchableSurface();
489 }
490
491 /**
492 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
493 */
494 public boolean isShowing() {
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100495 return !mDismissed && !mHidden;
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100496 }
497
498 /**
499 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
500 */
501 public boolean isHidden() {
502 return mHidden;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100503 }
504
505 /**
506 * Updates the coordinates of this popup.
507 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100508 * This is a no-op if this popup is not showing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100509 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700510 public void updateCoordinates(Rect contentRectOnScreen) {
511 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100512
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100513 if (!isShowing() || !mPopupWindow.isShowing()) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100514 return;
515 }
516
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100517 cancelOverflowAnimations();
Yohei Yukawabafc9082015-07-14 05:59:05 -0700518 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100519 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700520 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700521 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
522 // specify the popup poision in screen coordinates.
Yohei Yukawa4b269972015-07-15 19:01:32 -0700523 mPopupWindow.update(mCoordsOnWindow.x, mCoordsOnWindow.y, getWidth(), getHeight());
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000524 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100525
526 /**
527 * Returns the width of this popup.
528 */
529 public int getWidth() {
530 return mPopupWindow.getWidth();
531 }
532
533 /**
534 * Returns the height of this popup.
535 */
536 public int getHeight() {
537 return mPopupWindow.getHeight();
538 }
539
540 /**
541 * Returns the context this popup is running in.
542 */
543 public Context getContext() {
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700544 return mContext;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100545 }
546
Yohei Yukawabafc9082015-07-14 05:59:05 -0700547 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100548 refreshViewPort();
549
Yohei Yukawabafc9082015-07-14 05:59:05 -0700550 int x = contentRectOnScreen.centerX() - getWidth() / 2;
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100551 // Update x so that the toolbar isn't rendered behind the nav bar in landscape.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700552 x = Math.max(0, Math.min(x, mViewPortOnScreen.right - getWidth()));
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100553
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100554 int y;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100555
Yohei Yukawabafc9082015-07-14 05:59:05 -0700556 int availableHeightAboveContent = contentRectOnScreen.top - mViewPortOnScreen.top;
557 int availableHeightBelowContent = mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100558
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100559 if (mOverflowPanel == null) { // There is no overflow.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100560 if (availableHeightAboveContent >= getToolbarHeightWithVerticalMargin()) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100561 // There is enough space at the top of the content.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700562 y = contentRectOnScreen.top - getToolbarHeightWithVerticalMargin();
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100563 } else if (availableHeightBelowContent >= getToolbarHeightWithVerticalMargin()) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100564 // There is enough space at the bottom of the content.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700565 y = contentRectOnScreen.bottom;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700566 } else if (availableHeightBelowContent >= getEstimatedToolbarHeight(mContext)) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100567 // Just enough space to fit the toolbar with no vertical margins.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700568 y = contentRectOnScreen.bottom - mMarginVertical;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100569 } else {
570 // Not enough space. Prefer to position as high as possible.
571 y = Math.max(
Yohei Yukawabafc9082015-07-14 05:59:05 -0700572 mViewPortOnScreen.top,
573 contentRectOnScreen.top - getToolbarHeightWithVerticalMargin());
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100574 }
575 } else { // There is an overflow.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100576 int margin = 2 * mMarginVertical;
577 int minimumOverflowHeightWithMargin = mOverflowPanel.getMinimumHeight() + margin;
Yohei Yukawabafc9082015-07-14 05:59:05 -0700578 int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
579 contentRectOnScreen.top + getToolbarHeightWithVerticalMargin();
580 int availableHeightThroughContentUp = contentRectOnScreen.bottom -
581 mViewPortOnScreen.top + getToolbarHeightWithVerticalMargin();
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100582
583 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100584 // There is enough space at the top of the content rect for the overflow.
585 // Position above and open upwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100586 updateOverflowHeight(availableHeightAboveContent - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700587 y = contentRectOnScreen.top - getHeight();
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100588 mOverflowDirection = OVERFLOW_DIRECTION_UP;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100589 } else if (availableHeightAboveContent >= getToolbarHeightWithVerticalMargin()
590 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100591 // There is enough space at the top of the content rect for the main panel
592 // but not the overflow.
593 // Position above but open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100594 updateOverflowHeight(availableHeightThroughContentDown - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700595 y = contentRectOnScreen.top - getToolbarHeightWithVerticalMargin();
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100596 mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100597 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100598 // There is enough space at the bottom of the content rect for the overflow.
599 // Position below and open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100600 updateOverflowHeight(availableHeightBelowContent - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700601 y = contentRectOnScreen.bottom;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100602 mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100603 } else if (availableHeightBelowContent >= getToolbarHeightWithVerticalMargin()
Yohei Yukawabafc9082015-07-14 05:59:05 -0700604 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100605 // There is enough space at the bottom of the content rect for the main panel
606 // but not the overflow.
607 // Position below but open upwards.
608 updateOverflowHeight(availableHeightThroughContentUp - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700609 y = contentRectOnScreen.bottom + getToolbarHeightWithVerticalMargin() -
610 getHeight();
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100611 mOverflowDirection = OVERFLOW_DIRECTION_UP;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100612 } else {
613 // Not enough space.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100614 // Position at the top of the view port and open downwards.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700615 updateOverflowHeight(mViewPortOnScreen.height() - margin);
616 y = mViewPortOnScreen.top;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100617 mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100618 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100619 mOverflowPanel.setOverflowDirection(mOverflowDirection);
620 }
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100621
Yohei Yukawa4b269972015-07-15 19:01:32 -0700622 // We later specify the location of PopupWindow relative to the attached window.
623 // The idea here is that 1) we can get the location of a View in both window coordinates
624 // and screen coordiantes, where the offset between them should be equal to the window
625 // origin, and 2) we can use an arbitrary for this calculation while calculating the
626 // location of the rootview is supposed to be least expensive.
627 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
628 // the following calculation.
629 mParent.getRootView().getLocationOnScreen(mTmpCoords);
630 int rootViewLeftOnScreen = mTmpCoords[0];
631 int rootViewTopOnScreen = mTmpCoords[1];
632 mParent.getRootView().getLocationInWindow(mTmpCoords);
633 int rootViewLeftOnWindow = mTmpCoords[0];
634 int rootViewTopOnWindow = mTmpCoords[1];
635 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
636 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
637 mCoordsOnWindow.set(x - windowLeftOnScreen, y - windowTopOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100638 }
639
640 private int getToolbarHeightWithVerticalMargin() {
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700641 return getEstimatedToolbarHeight(mContext) + mMarginVertical * 2;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100642 }
643
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100644 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100645 * Performs the "show" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100646 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100647 private void runShowAnimation() {
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100648 createEnterAnimation(mContentContainer).start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100649 }
650
651 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100652 * Performs the "dismiss" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100653 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100654 private void runDismissAnimation() {
655 mDismissAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100656 }
657
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100658 /**
659 * Performs the "hide" animation on the floating popup.
660 */
661 private void runHideAnimation() {
662 mHideAnimation.start();
663 }
664
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100665 private void cancelDismissAndHideAnimations() {
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100666 mDismissAnimation.cancel();
667 mHideAnimation.cancel();
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100668 }
669
670 private void cancelOverflowAnimations() {
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -0700671 if (mOpenOverflowAnimation.hasStarted()
672 && !mOpenOverflowAnimation.hasEnded()) {
673 // Remove the animation listener, stop the animation,
674 // then trigger the lister explicitly so it is not posted
675 // to the message queue.
676 mOpenOverflowAnimation.setAnimationListener(null);
677 mContentContainer.clearAnimation();
678 mOnOverflowOpened.onAnimationEnd(null);
679 }
680 if (mCloseOverflowAnimation.hasStarted()
681 && !mCloseOverflowAnimation.hasEnded()) {
682 // Remove the animation listener, stop the animation,
683 // then trigger the lister explicitly so it is not posted
684 // to the message queue.
685 mCloseOverflowAnimation.setAnimationListener(null);
686 mContentContainer.clearAnimation();
687 mOnOverflowClosed.onAnimationEnd(null);
688 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100689 }
690
691 /**
692 * Opens the floating toolbar overflow.
693 * This method should not be called if menu items have not been laid out with
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100694 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100695 *
696 * @throws IllegalStateException if called when menu items have not been laid out.
697 */
698 private void openOverflow() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100699 Preconditions.checkState(mMainPanel != null);
700 Preconditions.checkState(mOverflowPanel != null);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100701
702 mMainPanel.fadeOut(true);
703 Size overflowPanelSize = mOverflowPanel.measure();
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100704 final int targetWidth = overflowPanelSize.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100705 final int targetHeight = overflowPanelSize.getHeight();
706 final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
707 final int startWidth = mContentContainer.getWidth();
708 final int startHeight = mContentContainer.getHeight();
709 final float startY = mContentContainer.getY();
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100710 final float left = mContentContainer.getX();
711 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100712 Animation widthAnimation = new Animation() {
713 @Override
714 protected void applyTransformation(float interpolatedTime, Transformation t) {
715 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
716 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
717 params.width = startWidth + deltaWidth;
718 mContentContainer.setLayoutParams(params);
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100719 if (isRTL()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100720 mContentContainer.setX(left);
721 } else {
722 mContentContainer.setX(right - mContentContainer.getWidth());
723 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100724 }
725 };
726 Animation heightAnimation = new Animation() {
727 @Override
728 protected void applyTransformation(float interpolatedTime, Transformation t) {
729 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
730 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
731 params.height = startHeight + deltaHeight;
732 mContentContainer.setLayoutParams(params);
733 if (morphUpwards) {
734 float y = startY - (mContentContainer.getHeight() - startHeight);
735 mContentContainer.setY(y);
736 }
737 }
738 };
739 widthAnimation.setDuration(240);
740 heightAnimation.setDuration(180);
741 heightAnimation.setStartOffset(60);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100742 mOpenOverflowAnimation.getAnimations().clear();
743 mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
744 mOpenOverflowAnimation.addAnimation(widthAnimation);
745 mOpenOverflowAnimation.addAnimation(heightAnimation);
746 mContentContainer.startAnimation(mOpenOverflowAnimation);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100747 }
748
749 /**
750 * Opens the floating toolbar overflow.
751 * This method should not be called if menu items have not been laid out with
752 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
753 *
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100754 * @throws IllegalStateException if called when menu items have not been laid out.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100755 */
756 private void closeOverflow() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100757 Preconditions.checkState(mMainPanel != null);
758 Preconditions.checkState(mOverflowPanel != null);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100759
760 mOverflowPanel.fadeOut(true);
761 Size mainPanelSize = mMainPanel.measure();
762 final int targetWidth = mainPanelSize.getWidth();
763 final int targetHeight = mainPanelSize.getHeight();
764 final int startWidth = mContentContainer.getWidth();
765 final int startHeight = mContentContainer.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100766 final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
767 final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100768 final float left = mContentContainer.getX();
769 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100770 Animation widthAnimation = new Animation() {
771 @Override
772 protected void applyTransformation(float interpolatedTime, Transformation t) {
773 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
774 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
775 params.width = startWidth + deltaWidth;
776 mContentContainer.setLayoutParams(params);
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100777 if (isRTL()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100778 mContentContainer.setX(left);
779 } else {
780 mContentContainer.setX(right - mContentContainer.getWidth());
781 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100782 }
783 };
784 Animation heightAnimation = new Animation() {
785 @Override
786 protected void applyTransformation(float interpolatedTime, Transformation t) {
787 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
788 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
789 params.height = startHeight + deltaHeight;
790 mContentContainer.setLayoutParams(params);
791 if (morphedUpwards) {
792 mContentContainer.setY(bottom - mContentContainer.getHeight());
793 }
794 }
795 };
796 widthAnimation.setDuration(150);
797 widthAnimation.setStartOffset(150);
798 heightAnimation.setDuration(210);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100799 mCloseOverflowAnimation.getAnimations().clear();
800 mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
801 mCloseOverflowAnimation.addAnimation(widthAnimation);
802 mCloseOverflowAnimation.addAnimation(heightAnimation);
803 mContentContainer.startAnimation(mCloseOverflowAnimation);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100804 }
805
806 /**
807 * Prepares the content container for show and update calls.
808 */
809 private void preparePopupContent() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100810 // Reset visibility.
811 if (mMainPanel != null) {
812 mMainPanel.fadeIn(false);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100813 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100814 if (mOverflowPanel != null) {
815 mOverflowPanel.fadeIn(false);
816 }
817
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100818 // Reset position.
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100819 if (isMainPanelContent()) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100820 positionMainPanel();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100821 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100822 if (isOverflowPanelContent()) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100823 positionOverflowPanel();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100824 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100825 }
826
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100827 private boolean isMainPanelContent() {
828 return mMainPanel != null
829 && mContentContainer.getChildAt(0) == mMainPanel.getView();
830 }
831
832 private boolean isOverflowPanelContent() {
833 return mOverflowPanel != null
834 && mContentContainer.getChildAt(0) == mOverflowPanel.getView();
835 }
836
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100837 /**
838 * Sets the current content to be the main view panel.
839 */
840 private void setMainPanelAsContent() {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100841 // This should never be called if the main panel has not been initialized.
842 Preconditions.checkNotNull(mMainPanel);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100843 mContentContainer.removeAllViews();
844 Size mainPanelSize = mMainPanel.measure();
845 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
846 params.width = mainPanelSize.getWidth();
847 params.height = mainPanelSize.getHeight();
848 mContentContainer.setLayoutParams(params);
849 mContentContainer.addView(mMainPanel.getView());
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100850 setContentAreaAsTouchableSurface();
851 }
852
853 /**
854 * Sets the current content to be the overflow view panel.
855 */
856 private void setOverflowPanelAsContent() {
857 // This should never be called if the overflow panel has not been initialized.
858 Preconditions.checkNotNull(mOverflowPanel);
859 mContentContainer.removeAllViews();
860 Size overflowPanelSize = mOverflowPanel.measure();
861 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
862 params.width = overflowPanelSize.getWidth();
863 params.height = overflowPanelSize.getHeight();
864 mContentContainer.setLayoutParams(params);
865 mContentContainer.addView(mOverflowPanel.getView());
866 setContentAreaAsTouchableSurface();
867 }
868
869 /**
870 * Places the main view panel at the appropriate resting coordinates.
871 */
872 private void positionMainPanel() {
873 Preconditions.checkNotNull(mMainPanel);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100874 mContentContainer.setX(mMarginHorizontal);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100875
876 float y = mMarginVertical;
877 if (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
878 y = getHeight()
879 - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
880 }
881 mContentContainer.setY(y);
882 setContentAreaAsTouchableSurface();
883 }
884
885 /**
886 * Places the main view panel at the appropriate resting coordinates.
887 */
888 private void positionOverflowPanel() {
889 Preconditions.checkNotNull(mOverflowPanel);
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100890 float x;
891 if (isRTL()) {
892 x = mMarginHorizontal;
893 } else {
894 x = mPopupWindow.getWidth()
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100895 - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100896 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100897 mContentContainer.setX(x);
898 mContentContainer.setY(mMarginVertical);
899 setContentAreaAsTouchableSurface();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100900 }
901
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100902 private void updateOverflowHeight(int height) {
903 if (mOverflowPanel != null) {
904 mOverflowPanel.setSuggestedHeight(height);
905
906 // Re-measure the popup and it's contents.
907 boolean mainPanelContent = isMainPanelContent();
908 boolean overflowPanelContent = isOverflowPanelContent();
909 mContentContainer.removeAllViews(); // required to update popup size.
910 updatePopupSize();
911 // Reset the appropriate content.
912 if (mainPanelContent) {
913 setMainPanelAsContent();
914 }
915 if (overflowPanelContent) {
916 setOverflowPanelAsContent();
917 }
918 }
919 }
920
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100921 private void updatePopupSize() {
922 int width = 0;
923 int height = 0;
924 if (mMainPanel != null) {
925 Size mainPanelSize = mMainPanel.measure();
926 width = mainPanelSize.getWidth();
927 height = mainPanelSize.getHeight();
928 }
929 if (mOverflowPanel != null) {
930 Size overflowPanelSize = mOverflowPanel.measure();
931 width = Math.max(width, overflowPanelSize.getWidth());
932 height = Math.max(height, overflowPanelSize.getHeight());
933 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100934 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
935 mPopupWindow.setHeight(height + mMarginVertical * 2);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100936 }
937
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100938
939 private void refreshViewPort() {
Yohei Yukawabafc9082015-07-14 05:59:05 -0700940 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100941 }
942
Clara Bayarri6bc12242015-06-16 18:04:55 +0100943 private boolean viewPortHasChanged() {
944 mParent.getWindowVisibleDisplayFrame(mTmpRect);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700945 return !mTmpRect.equals(mViewPortOnScreen);
Clara Bayarri6bc12242015-06-16 18:04:55 +0100946 }
947
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100948 private int getToolbarWidth(int suggestedWidth) {
949 int width = suggestedWidth;
950 refreshViewPort();
Yohei Yukawabafc9082015-07-14 05:59:05 -0700951 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100952 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
953 if (width <= 0) {
954 width = mParent.getResources()
955 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
956 }
957 return Math.min(width, maximumWidth);
958 }
959
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100960 /**
961 * Sets the touchable region of this popup to be zero. This means that all touch events on
962 * this popup will go through to the surface behind it.
963 */
964 private void setZeroTouchableSurface() {
965 mTouchableRegion.setEmpty();
966 }
967
968 /**
969 * Sets the touchable region of this popup to be the area occupied by its content.
970 */
971 private void setContentAreaAsTouchableSurface() {
972 if (!mPopupWindow.isShowing()) {
973 mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
974 }
975 int width = mContentContainer.getMeasuredWidth();
976 int height = mContentContainer.getMeasuredHeight();
977 mTouchableRegion.set(
978 (int) mContentContainer.getX(),
979 (int) mContentContainer.getY(),
980 (int) mContentContainer.getX() + width,
981 (int) mContentContainer.getY() + height);
982 }
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100983
984 /**
985 * Make the touchable area of this popup be the area specified by mTouchableRegion.
986 * This should be called after the popup window has been dismissed (dismiss/hide)
987 * and is probably being re-shown with a new content root view.
988 */
989 private void setTouchableSurfaceInsetsComputer() {
990 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
991 .getRootView()
992 .getViewTreeObserver();
993 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
994 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
995 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100996
997 private boolean isRTL() {
998 return mContentContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
999 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001000 }
1001
1002 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001003 * A widget that holds the primary menu items in the floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001004 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001005 private static final class FloatingToolbarMainPanel {
1006
1007 private final Context mContext;
1008 private final ViewGroup mContentView;
1009 private final View.OnClickListener mMenuItemButtonOnClickListener =
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001010 new View.OnClickListener() {
1011 @Override
1012 public void onClick(View v) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001013 if (v.getTag() instanceof MenuItem) {
1014 if (mOnMenuItemClickListener != null) {
1015 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
1016 }
1017 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001018 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001019 };
1020 private final ViewFader viewFader;
1021 private final Runnable mOpenOverflow;
1022
1023 private View mOpenOverflowButton;
1024 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
1025
1026 /**
1027 * Initializes a floating toolbar popup main view panel.
1028 *
1029 * @param context
1030 * @param openOverflow The code that opens the toolbar popup overflow.
1031 */
1032 public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
1033 mContext = Preconditions.checkNotNull(context);
1034 mContentView = new LinearLayout(context);
1035 viewFader = new ViewFader(mContentView);
1036 mOpenOverflow = Preconditions.checkNotNull(openOverflow);
1037 }
1038
1039 /**
1040 * Fits as many menu items in the main panel and returns a list of the menu items that
1041 * were not fit in.
1042 *
1043 * @return The menu items that are not included in this main panel.
1044 */
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001045 public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int width) {
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001046 Preconditions.checkNotNull(menuItems);
1047
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001048 // Reserve space for the "open overflow" button.
1049 final int toolbarWidth = width - getEstimatedOpenOverflowButtonWidth(mContext);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001050
1051 int availableWidth = toolbarWidth;
1052 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1053
1054 mContentView.removeAllViews();
1055
1056 boolean isFirstItem = true;
1057 while (!remainingMenuItems.isEmpty()) {
1058 final MenuItem menuItem = remainingMenuItems.peek();
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001059 View menuItemButton = createMenuItemButton(mContext, menuItem);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001060
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001061 // Adding additional start padding for the first button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001062 if (isFirstItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001063 menuItemButton.setPaddingRelative(
1064 (int) (1.5 * menuItemButton.getPaddingStart()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001065 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001066 menuItemButton.getPaddingEnd(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001067 menuItemButton.getPaddingBottom());
1068 isFirstItem = false;
1069 }
1070
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001071 // Adding additional end padding for the last button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001072 if (remainingMenuItems.size() == 1) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001073 menuItemButton.setPaddingRelative(
1074 menuItemButton.getPaddingStart(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001075 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001076 (int) (1.5 * menuItemButton.getPaddingEnd()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001077 menuItemButton.getPaddingBottom());
1078 }
1079
1080 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1081 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1082 if (menuItemButtonWidth <= availableWidth) {
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001083 setButtonTagAndClickListener(menuItemButton, menuItem);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001084 mContentView.addView(menuItemButton);
1085 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1086 params.width = menuItemButtonWidth;
1087 menuItemButton.setLayoutParams(params);
1088 availableWidth -= menuItemButtonWidth;
1089 remainingMenuItems.pop();
1090 } else {
1091 if (mOpenOverflowButton == null) {
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001092 mOpenOverflowButton = LayoutInflater.from(mContext)
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001093 .inflate(R.layout.floating_popup_open_overflow_button, null);
1094 mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
1095 @Override
1096 public void onClick(View v) {
1097 if (mOpenOverflowButton != null) {
1098 mOpenOverflow.run();
1099 }
1100 }
1101 });
1102 }
1103 mContentView.addView(mOpenOverflowButton);
1104 break;
1105 }
1106 }
1107 return remainingMenuItems;
1108 }
1109
1110 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1111 mOnMenuItemClickListener = listener;
1112 }
1113
1114 public View getView() {
1115 return mContentView;
1116 }
1117
1118 public void fadeIn(boolean animate) {
1119 viewFader.fadeIn(animate);
1120 }
1121
1122 public void fadeOut(boolean animate) {
1123 viewFader.fadeOut(animate);
1124 }
1125
1126 /**
1127 * Returns how big this panel's view should be.
1128 * This method should only be called when the view has not been attached to a parent
1129 * otherwise it will throw an illegal state.
1130 */
1131 public Size measure() throws IllegalStateException {
1132 Preconditions.checkState(mContentView.getParent() == null);
1133 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1134 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1135 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001136
1137 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1138 View button = menuItemButton;
1139 if (isIconOnlyMenuItem(menuItem)) {
1140 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1141 }
1142 button.setTag(menuItem);
1143 button.setOnClickListener(mMenuItemButtonOnClickListener);
1144 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001145 }
1146
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001147
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001148 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001149 * A widget that holds the overflow items in the floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001150 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001151 private static final class FloatingToolbarOverflowPanel {
1152
1153 private final LinearLayout mContentView;
1154 private final ViewGroup mBackButtonContainer;
1155 private final View mBackButton;
1156 private final ListView mListView;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001157 private final TextView mListViewItemWidthCalculator;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001158 private final ViewFader mViewFader;
1159 private final Runnable mCloseOverflow;
1160
1161 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
Abodunrinwa Toki8747f372015-06-29 21:17:22 -07001162 private int mOverflowWidth;
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001163 private int mSuggestedHeight;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001164
1165 /**
1166 * Initializes a floating toolbar popup overflow view panel.
1167 *
1168 * @param context
1169 * @param closeOverflow The code that closes the toolbar popup's overflow.
1170 */
1171 public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
1172 mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
1173
1174 mContentView = new LinearLayout(context);
1175 mContentView.setOrientation(LinearLayout.VERTICAL);
1176 mViewFader = new ViewFader(mContentView);
1177
1178 mBackButton = LayoutInflater.from(context)
1179 .inflate(R.layout.floating_popup_close_overflow_button, null);
1180 mBackButton.setOnClickListener(new View.OnClickListener() {
1181 @Override
1182 public void onClick(View v) {
1183 mCloseOverflow.run();
1184 }
1185 });
1186 mBackButtonContainer = new LinearLayout(context);
1187 mBackButtonContainer.addView(mBackButton);
1188
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001189 mListView = createOverflowListView();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001190 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1191 @Override
1192 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1193 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
1194 if (mOnMenuItemClickListener != null) {
1195 mOnMenuItemClickListener.onMenuItemClick(menuItem);
1196 }
1197 }
1198 });
1199
1200 mContentView.addView(mListView);
1201 mContentView.addView(mBackButtonContainer);
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001202
1203 mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1204 mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1205 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001206 }
1207
1208 /**
1209 * Sets the menu items to be displayed in the overflow.
1210 */
1211 public void setMenuItems(List<MenuItem> menuItems) {
1212 ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1213 overflowListViewAdapter.clear();
1214 overflowListViewAdapter.addAll(menuItems);
1215 setListViewHeight();
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001216 setOverflowWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001217 }
1218
1219 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1220 mOnMenuItemClickListener = listener;
1221 }
1222
1223 /**
1224 * Notifies the overflow of the current direction in which the overflow will be opened.
1225 *
1226 * @param overflowDirection {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1227 * or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1228 */
1229 public void setOverflowDirection(int overflowDirection) {
1230 mContentView.removeView(mBackButtonContainer);
1231 int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1232 mContentView.addView(mBackButtonContainer, index);
1233 }
1234
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001235 public void setSuggestedHeight(int height) {
1236 mSuggestedHeight = height;
1237 setListViewHeight();
1238 }
1239
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +01001240 public int getMinimumHeight() {
1241 return mContentView.getContext().getResources().
1242 getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height)
1243 + getEstimatedToolbarHeight(mContentView.getContext());
1244 }
1245
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001246 /**
1247 * Returns the content view of the overflow.
1248 */
1249 public View getView() {
1250 return mContentView;
1251 }
1252
1253 public void fadeIn(boolean animate) {
1254 mViewFader.fadeIn(animate);
1255 }
1256
1257 public void fadeOut(boolean animate) {
1258 mViewFader.fadeOut(animate);
1259 }
1260
1261 /**
1262 * Returns how big this panel's view should be.
1263 * This method should only be called when the view has not been attached to a parent.
1264 *
1265 * @throws IllegalStateException
1266 */
1267 public Size measure() {
1268 Preconditions.checkState(mContentView.getParent() == null);
1269 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1270 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1271 }
1272
1273 private void setListViewHeight() {
1274 int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1275 int height = mListView.getAdapter().getCount() * itemHeight;
1276 int maxHeight = mContentView.getContext().getResources().
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001277 getDimensionPixelSize(R.dimen.floating_toolbar_maximum_overflow_height);
1278 int minHeight = mContentView.getContext().getResources().
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001279 getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +01001280 int suggestedListViewHeight = mSuggestedHeight - (mSuggestedHeight % itemHeight)
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001281 - itemHeight; // reserve space for the back button.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001282 ViewGroup.LayoutParams params = mListView.getLayoutParams();
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +01001283 if (suggestedListViewHeight <= 0) {
1284 // Invalid height. Use the maximum height available.
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001285 params.height = Math.min(maxHeight, height);
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +01001286 } else if (suggestedListViewHeight < minHeight) {
1287 // Height is smaller than minimum allowed. Use minimum height.
1288 params.height = minHeight;
1289 } else {
1290 // Use the suggested height. Cap it at the maximum available height.
1291 params.height = Math.min(Math.min(suggestedListViewHeight, maxHeight), height);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001292 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001293 mListView.setLayoutParams(params);
1294 }
1295
Abodunrinwa Toki8747f372015-06-29 21:17:22 -07001296 private void setOverflowWidth() {
1297 mOverflowWidth = 0;
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001298 for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1299 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1300 Preconditions.checkNotNull(menuItem);
1301 mListViewItemWidthCalculator.setText(menuItem.getTitle());
1302 mListViewItemWidthCalculator.measure(
1303 MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1304 mOverflowWidth = Math.max(
1305 mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1306 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001307 }
1308
1309 private ListView createOverflowListView() {
1310 final Context context = mContentView.getContext();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001311 final ListView overflowListView = new ListView(context);
1312 overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1313 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1314 overflowListView.setDivider(null);
1315 overflowListView.setDividerHeight(0);
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001316
1317 final int viewTypeCount = 2;
1318 final int stringLabelViewType = 0;
1319 final int iconOnlyViewType = 1;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001320 final ArrayAdapter overflowListViewAdapter =
1321 new ArrayAdapter<MenuItem>(context, 0) {
1322 @Override
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001323 public int getViewTypeCount() {
1324 return viewTypeCount;
1325 }
1326
1327 @Override
1328 public int getItemViewType(int position) {
1329 if (isIconOnlyMenuItem(getItem(position))) {
1330 return iconOnlyViewType;
1331 }
1332 return stringLabelViewType;
1333 }
1334
1335 @Override
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001336 public View getView(int position, View convertView, ViewGroup parent) {
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001337 if (getItemViewType(position) == iconOnlyViewType) {
1338 return getIconOnlyView(position, convertView);
1339 }
1340 return getStringTitleView(position, convertView);
1341 }
1342
1343 private View getStringTitleView(int position, View convertView) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001344 TextView menuButton;
1345 if (convertView != null) {
1346 menuButton = (TextView) convertView;
1347 } else {
1348 menuButton = createOverflowMenuItemButton(context);
1349 }
1350 MenuItem menuItem = getItem(position);
1351 menuButton.setText(menuItem.getTitle());
1352 menuButton.setContentDescription(menuItem.getTitle());
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001353 menuButton.setMinimumWidth(mOverflowWidth);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001354 return menuButton;
1355 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001356
1357 private View getIconOnlyView(int position, View convertView) {
1358 View menuButton;
1359 if (convertView != null) {
1360 menuButton = convertView;
1361 } else {
1362 menuButton = LayoutInflater.from(context).inflate(
1363 R.layout.floating_popup_overflow_image_list_item, null);
1364 }
1365 MenuItem menuItem = getItem(position);
1366 ((ImageView) menuButton
1367 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1368 .setImageDrawable(menuItem.getIcon());
1369 menuButton.setMinimumWidth(mOverflowWidth);
1370 return menuButton;
1371 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001372 };
1373 overflowListView.setAdapter(overflowListViewAdapter);
1374 return overflowListView;
1375 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001376 }
1377
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001378
1379 /**
1380 * A helper for fading in or out a view.
1381 */
1382 private static final class ViewFader {
1383
1384 private static final int FADE_OUT_DURATION = 250;
1385 private static final int FADE_IN_DURATION = 150;
1386
1387 private final View mView;
1388 private final ObjectAnimator mFadeOutAnimation;
1389 private final ObjectAnimator mFadeInAnimation;
1390
1391 private ViewFader(View view) {
1392 mView = Preconditions.checkNotNull(view);
1393 mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1394 .setDuration(FADE_OUT_DURATION);
1395 mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1396 .setDuration(FADE_IN_DURATION);
1397 }
1398
1399 public void fadeIn(boolean animate) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001400 cancelFadeAnimations();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001401 if (animate) {
1402 mFadeInAnimation.start();
1403 } else {
1404 mView.setAlpha(1);
1405 }
1406 }
1407
1408 public void fadeOut(boolean animate) {
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001409 cancelFadeAnimations();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001410 if (animate) {
1411 mFadeOutAnimation.start();
1412 } else {
1413 mView.setAlpha(0);
1414 }
1415 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001416
1417 private void cancelFadeAnimations() {
1418 mFadeInAnimation.cancel();
1419 mFadeOutAnimation.cancel();
1420 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001421 }
1422
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001423 /**
1424 * @return {@code true} if the menu item does not not have a string title but has an icon.
1425 * {@code false} otherwise.
1426 */
1427 private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1428 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1429 return true;
1430 }
1431 return false;
1432 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001433
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001434 /**
1435 * Creates and returns a menu button for the specified menu item.
1436 */
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001437 private static View createMenuItemButton(Context context, MenuItem menuItem) {
1438 if (isIconOnlyMenuItem(menuItem)) {
1439 View imageMenuItemButton = LayoutInflater.from(context)
1440 .inflate(R.layout.floating_popup_menu_image_button, null);
1441 ((ImageButton) imageMenuItemButton
1442 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1443 .setImageDrawable(menuItem.getIcon());
1444 return imageMenuItemButton;
1445 }
1446
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001447 Button menuItemButton = (Button) LayoutInflater.from(context)
1448 .inflate(R.layout.floating_popup_menu_button, null);
1449 menuItemButton.setText(menuItem.getTitle());
1450 menuItemButton.setContentDescription(menuItem.getTitle());
1451 return menuItemButton;
1452 }
1453
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001454 /**
1455 * Creates and returns a styled floating toolbar overflow list view item.
1456 */
1457 private static TextView createOverflowMenuItemButton(Context context) {
1458 return (TextView) LayoutInflater.from(context)
1459 .inflate(R.layout.floating_popup_overflow_list_item, null);
1460 }
1461
1462 private static ViewGroup createContentContainer(Context context) {
1463 return (ViewGroup) LayoutInflater.from(context)
1464 .inflate(R.layout.floating_popup_container, null);
1465 }
1466
1467 private static PopupWindow createPopupWindow(View content) {
1468 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1469 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
Yohei Yukawabafc9082015-07-14 05:59:05 -07001470 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1471 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1472 popupWindow.setClippingEnabled(false);
Abodunrinwa Toki103d48e2015-04-16 17:27:48 +01001473 popupWindow.setWindowLayoutType(
1474 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001475 popupWindow.setAnimationStyle(0);
1476 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1477 content.setLayoutParams(new ViewGroup.LayoutParams(
1478 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1479 popupContentHolder.addView(content);
1480 return popupWindow;
1481 }
1482
1483 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001484 * Creates an "appear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001485 *
1486 * @param view The view to animate
1487 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001488 private static AnimatorSet createEnterAnimation(View view) {
1489 AnimatorSet animation = new AnimatorSet();
1490 animation.playTogether(
1491 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(200),
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +01001492 // Make sure that view.x is always fixed throughout the duration of this animation.
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001493 ObjectAnimator.ofFloat(view, View.X, view.getX(), view.getX()));
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001494 animation.setStartDelay(50);
1495 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001496 }
1497
1498 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001499 * Creates a "disappear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001500 *
1501 * @param view The view to animate
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001502 * @param startDelay The start delay of the animation
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001503 * @param listener The animation listener
1504 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001505 private static AnimatorSet createExitAnimation(
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001506 View view, int startDelay, Animator.AnimatorListener listener) {
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001507 AnimatorSet animation = new AnimatorSet();
1508 animation.playTogether(
1509 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(200));
1510 animation.setStartDelay(startDelay);
1511 animation.addListener(listener);
1512 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001513 }
1514
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -07001515 /**
1516 * Returns a re-themed context with controlled look and feel for views.
1517 */
1518 private static Context applyDefaultTheme(Context originalContext) {
1519 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1520 boolean isLightTheme = a.getBoolean(0, true);
1521 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1522 a.recycle();
1523 return new ContextThemeWrapper(originalContext, themeId);
1524 }
1525
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001526 private static int getEstimatedToolbarHeight(Context context) {
1527 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001528 }
1529
1530 private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1531 return context.getResources()
1532 .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1533 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001534}