blob: 42fb1f5c34e913b9932f7925718533aeb469a9d7 [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;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000023import android.animation.ValueAnimator;
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +010024import android.annotation.Nullable;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000025import android.content.Context;
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 Toki7e29d1b2015-10-30 16:41:45 +000031import android.graphics.drawable.AnimatedVectorDrawable;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000032import android.graphics.drawable.ColorDrawable;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000033import android.graphics.drawable.Drawable;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010034import android.text.TextUtils;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010035import android.util.Size;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +000036import android.util.TypedValue;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -070037import android.view.ContextThemeWrapper;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000038import android.view.Gravity;
39import android.view.LayoutInflater;
40import android.view.Menu;
41import android.view.MenuItem;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000042import android.view.MotionEvent;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000043import android.view.View;
44import android.view.View.MeasureSpec;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +000045import android.view.View.OnLayoutChangeListener;
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +000046import android.view.ViewConfiguration;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000047import android.view.ViewGroup;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010048import android.view.ViewTreeObserver;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000049import android.view.Window;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010050import android.view.WindowManager;
51import android.view.animation.Animation;
52import android.view.animation.AnimationSet;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000053import android.view.animation.AnimationUtils;
54import android.view.animation.Interpolator;
Siyamed Sinir484c2e22017-06-07 16:26:19 -070055import android.view.animation.Transformation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010056import android.widget.ArrayAdapter;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000057import android.widget.ImageButton;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010058import android.widget.ImageView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000059import android.widget.LinearLayout;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010060import android.widget.ListView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000061import android.widget.PopupWindow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010062import android.widget.TextView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000063
Siyamed Sinir484c2e22017-06-07 16:26:19 -070064import com.android.internal.R;
65import com.android.internal.util.Preconditions;
66
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000067import java.util.ArrayList;
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +010068import java.util.Comparator;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000069import java.util.LinkedList;
70import java.util.List;
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010071import java.util.Objects;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010072
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000073/**
74 * A floating toolbar for showing contextual menu items.
75 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
76 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
77 * The horizontal toolbar morphs into the vertical overflow view.
78 */
79public final class FloatingToolbar {
80
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010081 // This class is responsible for the public API of the floating toolbar.
82 // It delegates rendering operations to the FloatingToolbarPopup.
83
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +010084 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
85
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000086 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010087 item -> false;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000088
89 private final Context mContext;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +000090 private final Window mWindow;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000091 private final FloatingToolbarPopup mPopup;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000092
93 private final Rect mContentRect = new Rect();
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -070094 private final Rect mPreviousContentRect = new Rect();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000095
96 private Menu mMenu;
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010097 private List<MenuItem> mShowingMenuItems = new ArrayList<>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000098 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000099
100 private int mSuggestedWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100101 private boolean mWidthChanged = true;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000102
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000103 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
104
105 private final Rect mNewRect = new Rect();
106 private final Rect mOldRect = new Rect();
107
Clara Bayarri6bc12242015-06-16 18:04:55 +0100108 @Override
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000109 public void onLayoutChange(
110 View view,
111 int newLeft, int newRight, int newTop, int newBottom,
112 int oldLeft, int oldRight, int oldTop, int oldBottom) {
113 mNewRect.set(newLeft, newRight, newTop, newBottom);
114 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
115 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
Clara Bayarri6bc12242015-06-16 18:04:55 +0100116 mWidthChanged = true;
117 updateLayout();
118 }
119 }
Clara Bayarri6bc12242015-06-16 18:04:55 +0100120 };
121
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000122 /**
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +0100123 * Sorts the list of menu items to conform to certain requirements.
124 */
125 private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
126 // Ensure the assist menu item is always the first item:
127 if (menuItem1.getItemId() == android.R.id.textAssist) {
128 return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
129 }
130 if (menuItem2.getItemId() == android.R.id.textAssist) {
131 return 1;
132 }
133
134 // Order by SHOW_AS_ACTION type:
135 if (menuItem1.requiresActionButton()) {
136 return menuItem2.requiresActionButton() ? 0 : -1;
137 }
138 if (menuItem2.requiresActionButton()) {
139 return 1;
140 }
141 if (menuItem1.requiresOverflow()) {
142 return menuItem2.requiresOverflow() ? 0 : 1;
143 }
144 if (menuItem2.requiresOverflow()) {
145 return -1;
146 }
147
148 // Order by order value:
149 return menuItem1.getOrder() - menuItem2.getOrder();
150 };
151
152 /**
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000153 * Initializes a floating toolbar.
154 */
Tarandeep Singhc9c83a92017-08-29 14:39:22 -0700155 public FloatingToolbar(Window window) {
156 // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
157 // supports multi-display.
158 mContext = applyDefaultTheme(window.getContext());
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000159 mWindow = Preconditions.checkNotNull(window);
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700160 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000161 }
162
163 /**
164 * Sets the menu to be shown in this floating toolbar.
165 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
166 * toolbar.
167 */
168 public FloatingToolbar setMenu(Menu menu) {
169 mMenu = Preconditions.checkNotNull(menu);
170 return this;
171 }
172
173 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100174 * Sets the custom listener for invocation of menu items in this floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000175 */
176 public FloatingToolbar setOnMenuItemClickListener(
177 MenuItem.OnMenuItemClickListener menuItemClickListener) {
178 if (menuItemClickListener != null) {
179 mMenuItemClickListener = menuItemClickListener;
180 } else {
181 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
182 }
183 return this;
184 }
185
186 /**
187 * Sets the content rectangle. This is the area of the interesting content that this toolbar
188 * should avoid obstructing.
189 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
190 * toolbar.
191 */
192 public FloatingToolbar setContentRect(Rect rect) {
193 mContentRect.set(Preconditions.checkNotNull(rect));
194 return this;
195 }
196
197 /**
198 * Sets the suggested width of this floating toolbar.
199 * The actual width will be about this size but there are no guarantees that it will be exactly
200 * the suggested width.
201 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
202 * toolbar.
203 */
204 public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100205 // Check if there's been a substantial width spec change.
206 int difference = Math.abs(suggestedWidth - mSuggestedWidth);
207 mWidthChanged = difference > (mSuggestedWidth * 0.2);
208
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000209 mSuggestedWidth = suggestedWidth;
210 return this;
211 }
212
213 /**
214 * Shows this floating toolbar.
215 */
216 public FloatingToolbar show() {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000217 registerOrientationHandler();
218 doShow();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000219 return this;
220 }
221
222 /**
223 * Updates this floating toolbar to reflect recent position and view updates.
224 * NOTE: This method is a no-op if the toolbar isn't showing.
225 */
226 public FloatingToolbar updateLayout() {
227 if (mPopup.isShowing()) {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000228 doShow();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000229 }
230 return this;
231 }
232
233 /**
234 * Dismisses this floating toolbar.
235 */
236 public void dismiss() {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000237 unregisterOrientationHandler();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000238 mPopup.dismiss();
239 }
240
241 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100242 * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
243 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
244 */
245 public void hide() {
246 mPopup.hide();
247 }
248
249 /**
250 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000251 */
252 public boolean isShowing() {
253 return mPopup.isShowing();
254 }
255
256 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100257 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
258 */
259 public boolean isHidden() {
260 return mPopup.isHidden();
261 }
262
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +0100263 /**
264 * If this is set to true, the action mode view will dismiss itself on touch events outside of
265 * its window. If the toolbar is already showing, it will be re-shown so that this setting takes
266 * effect immediately.
267 *
268 * @param outsideTouchable whether or not this action mode is "outside touchable"
269 * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
270 */
271 public void setOutsideTouchable(
272 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
273 if (mPopup.setOutsideTouchable(outsideTouchable, onDismiss) && isShowing()) {
274 dismiss();
275 doShow();
276 }
277 }
278
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000279 private void doShow() {
280 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +0100281 menuItems.sort(mMenuItemComparator);
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000282 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
283 mPopup.dismiss();
284 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100285 mShowingMenuItems = menuItems;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000286 }
287 if (!mPopup.isShowing()) {
288 mPopup.show(mContentRect);
289 } else if (!mPreviousContentRect.equals(mContentRect)) {
290 mPopup.updateCoordinates(mContentRect);
291 }
292 mWidthChanged = false;
293 mPreviousContentRect.set(mContentRect);
294 }
295
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100296 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100297 * Returns true if this floating toolbar is currently showing the specified menu items.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000298 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100299 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100300 if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
301 return false;
302 }
303
304 final int size = menuItems.size();
305 for (int i = 0; i < size; i++) {
306 final MenuItem menuItem = menuItems.get(i);
307 final MenuItem showingItem = mShowingMenuItems.get(i);
308 if (menuItem.getItemId() != showingItem.getItemId()
309 || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
310 || !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
311 || menuItem.getGroupId() != showingItem.getGroupId()) {
312 return false;
313 }
314 }
315
316 return true;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000317 }
318
319 /**
320 * Returns the visible and enabled menu items in the specified menu.
321 * This method is recursive.
322 */
323 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100324 List<MenuItem> menuItems = new ArrayList<>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000325 for (int i = 0; (menu != null) && (i < menu.size()); i++) {
326 MenuItem menuItem = menu.getItem(i);
327 if (menuItem.isVisible() && menuItem.isEnabled()) {
328 Menu subMenu = menuItem.getSubMenu();
329 if (subMenu != null) {
330 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
331 } else {
332 menuItems.add(menuItem);
333 }
334 }
335 }
336 return menuItems;
337 }
338
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000339 private void registerOrientationHandler() {
340 unregisterOrientationHandler();
341 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
342 }
343
344 private void unregisterOrientationHandler() {
345 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
346 }
347
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000348
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100349 /**
350 * A popup window used by the floating toolbar.
351 *
352 * This class is responsible for the rendering/animation of the floating toolbar.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000353 * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
354 * to transition between panels.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100355 */
356 private static final class FloatingToolbarPopup {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000357
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000358 /* Minimum and maximum number of items allowed in the overflow. */
359 private static final int MIN_OVERFLOW_SIZE = 2;
360 private static final int MAX_OVERFLOW_SIZE = 4;
361
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700362 private final Context mContext;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000363 private final View mParent; // Parent for the popup window.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100364 private final PopupWindow mPopupWindow;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000365
366 /* Margins between the popup window and it's content. */
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100367 private final int mMarginHorizontal;
368 private final int mMarginVertical;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000369
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000370 /* View components */
371 private final ViewGroup mContentContainer; // holds all contents.
372 private final ViewGroup mMainPanel; // holds menu items that are initially displayed.
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +0000373 private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000374 private final ImageButton mOverflowButton; // opens/closes the overflow.
375 /* overflow button drawables. */
376 private final Drawable mArrow;
377 private final Drawable mOverflow;
378 private final AnimatedVectorDrawable mToArrow;
379 private final AnimatedVectorDrawable mToOverflow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100380
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000381 private final OverflowPanelViewHelper mOverflowPanelViewHelper;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100382
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000383 /* Animation interpolators. */
384 private final Interpolator mLogAccelerateInterpolator;
385 private final Interpolator mFastOutSlowInInterpolator;
386 private final Interpolator mLinearOutSlowInInterpolator;
387 private final Interpolator mFastOutLinearInInterpolator;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100388
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000389 /* Animations. */
390 private final AnimatorSet mShowAnimation;
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100391 private final AnimatorSet mDismissAnimation;
392 private final AnimatorSet mHideAnimation;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000393 private final AnimationSet mOpenOverflowAnimation;
394 private final AnimationSet mCloseOverflowAnimation;
395 private final Animation.AnimationListener mOverflowAnimationListener;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100396
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000397 private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in.
398 private final Point mCoordsOnWindow = new Point(); // popup window coordinates.
399 /* Temporary data holders. Reset values before using. */
Yohei Yukawa4b269972015-07-15 19:01:32 -0700400 private final int[] mTmpCoords = new int[2];
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100401
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100402 private final Region mTouchableRegion = new Region();
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100403 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100404 info -> {
405 info.contentInsets.setEmpty();
406 info.visibleInsets.setEmpty();
407 info.touchableRegion.set(mTouchableRegion);
408 info.setTouchableInsets(
409 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100410 };
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100411
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000412 private final int mLineHeight;
413 private final int mIconTextSpacing;
414
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000415 /**
416 * @see OverflowPanelViewHelper#preparePopupContent().
417 */
418 private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
419 @Override
420 public void run() {
421 setPanelsStatesAtRestingPosition();
422 setContentAreaAsTouchableSurface();
423 mContentContainer.setAlpha(1);
424 }
425 };
426
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100427 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
428 private boolean mHidden; // tracks whether this popup is hidden or hiding.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100429
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000430 /* Calculated sizes for panels and overflow button. */
431 private final Size mOverflowButtonSize;
432 private Size mOverflowPanelSize; // Should be null when there is no overflow.
433 private Size mMainPanelSize;
434
435 /* Item click listeners */
436 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
437 private final View.OnClickListener mMenuItemButtonOnClickListener =
438 new View.OnClickListener() {
439 @Override
440 public void onClick(View v) {
441 if (v.getTag() instanceof MenuItem) {
442 if (mOnMenuItemClickListener != null) {
443 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
444 }
445 }
446 }
447 };
448
449 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
450 private boolean mIsOverflowOpen;
451
452 private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100453
454 /**
455 * Initializes a new floating toolbar popup.
456 *
457 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
458 * from.
459 */
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700460 public FloatingToolbarPopup(Context context, View parent) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100461 mParent = Preconditions.checkNotNull(parent);
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700462 mContext = Preconditions.checkNotNull(context);
463 mContentContainer = createContentContainer(context);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100464 mPopupWindow = createPopupWindow(mContentContainer);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000465 mMarginHorizontal = parent.getResources()
466 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
467 mMarginVertical = parent.getResources()
468 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000469 mLineHeight = context.getResources()
470 .getDimensionPixelSize(R.dimen.floating_toolbar_height);
471 mIconTextSpacing = context.getResources()
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +0100472 .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000473
474 // Interpolators
475 mLogAccelerateInterpolator = new LogAccelerateInterpolator();
476 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
477 mContext, android.R.interpolator.fast_out_slow_in);
478 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
479 mContext, android.R.interpolator.linear_out_slow_in);
480 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
481 mContext, android.R.interpolator.fast_out_linear_in);
482
483 // Drawables. Needed for views.
484 mArrow = mContext.getResources()
485 .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
486 mArrow.setAutoMirrored(true);
487 mOverflow = mContext.getResources()
488 .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
489 mOverflow.setAutoMirrored(true);
490 mToArrow = (AnimatedVectorDrawable) mContext.getResources()
491 .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
492 mToArrow.setAutoMirrored(true);
493 mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
494 .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
495 mToOverflow.setAutoMirrored(true);
496
497 // Views
498 mOverflowButton = createOverflowButton();
499 mOverflowButtonSize = measure(mOverflowButton);
500 mMainPanel = createMainPanel();
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +0100501 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000502 mOverflowPanel = createOverflowPanel();
503
504 // Animation. Need views.
505 mOverflowAnimationListener = createOverflowAnimationListener();
506 mOpenOverflowAnimation = new AnimationSet(true);
507 mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
508 mCloseOverflowAnimation = new AnimationSet(true);
509 mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
510 mShowAnimation = createEnterAnimation(mContentContainer);
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100511 mDismissAnimation = createExitAnimation(
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100512 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100513 150, // startDelay
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100514 new AnimatorListenerAdapter() {
515 @Override
516 public void onAnimationEnd(Animator animation) {
517 mPopupWindow.dismiss();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100518 mContentContainer.removeAllViews();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100519 }
520 });
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100521 mHideAnimation = createExitAnimation(
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100522 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100523 0, // startDelay
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100524 new AnimatorListenerAdapter() {
525 @Override
526 public void onAnimationEnd(Animator animation) {
527 mPopupWindow.dismiss();
528 }
529 });
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100530 }
531
532 /**
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +0100533 * Makes this toolbar "outside touchable" and sets the onDismissListener.
534 * This will take effect the next time the toolbar is re-shown.
535 *
536 * @param outsideTouchable if true, the popup will be made "outside touchable" and
537 * "non focusable". The reverse will happen if false.
538 * @param onDismiss
539 *
540 * @return true if the "outsideTouchable" setting was modified. Otherwise returns false
541 *
542 * @see PopupWindow#setOutsideTouchable(boolean)
543 * @see PopupWindow#setFocusable(boolean)
544 * @see PopupWindow.OnDismissListener
545 */
546 public boolean setOutsideTouchable(
547 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
548 boolean ret = false;
549 if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
550 mPopupWindow.setOutsideTouchable(outsideTouchable);
551 mPopupWindow.setFocusable(!outsideTouchable);
552 ret = true;
553 }
554 mPopupWindow.setOnDismissListener(onDismiss);
555 return ret;
556 }
557
558 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100559 * Lays out buttons for the specified menu items.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000560 * Requires a subsequent call to {@link #show()} to show the items.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100561 */
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100562 public void layoutMenuItems(
563 List<MenuItem> menuItems,
564 MenuItem.OnMenuItemClickListener menuItemClickListener,
565 int suggestedWidth) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000566 mOnMenuItemClickListener = menuItemClickListener;
567 cancelOverflowAnimations();
568 clearPanels();
569 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
570 if (!menuItems.isEmpty()) {
571 // Add remaining items to the overflow.
572 layoutOverflowPanelItems(menuItems);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100573 }
574 updatePopupSize();
575 }
576
577 /**
578 * Shows this popup at the specified coordinates.
579 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
580 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700581 public void show(Rect contentRectOnScreen) {
582 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100583
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100584 if (isShowing()) {
585 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000586 }
587
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100588 mHidden = false;
589 mDismissed = false;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100590 cancelDismissAndHideAnimations();
591 cancelOverflowAnimations();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100592
Yohei Yukawabafc9082015-07-14 05:59:05 -0700593 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100594 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700595 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700596 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000597 // specify the popup position in screen coordinates.
598 mPopupWindow.showAtLocation(
599 mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100600 setTouchableSurfaceInsetsComputer();
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100601 runShowAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100602 }
603
604 /**
605 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
606 */
607 public void dismiss() {
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100608 if (mDismissed) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100609 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000610 }
611
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100612 mHidden = false;
613 mDismissed = true;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100614 mHideAnimation.cancel();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000615
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100616 runDismissAnimation();
617 setZeroTouchableSurface();
618 }
619
620 /**
621 * Hides this popup. This is a no-op if this popup is not showing.
622 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
623 */
624 public void hide() {
625 if (!isShowing()) {
626 return;
627 }
628
629 mHidden = true;
630 runHideAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100631 setZeroTouchableSurface();
632 }
633
634 /**
635 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
636 */
637 public boolean isShowing() {
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100638 return !mDismissed && !mHidden;
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100639 }
640
641 /**
642 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
643 */
644 public boolean isHidden() {
645 return mHidden;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100646 }
647
648 /**
649 * Updates the coordinates of this popup.
650 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100651 * This is a no-op if this popup is not showing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100652 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700653 public void updateCoordinates(Rect contentRectOnScreen) {
654 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100655
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100656 if (!isShowing() || !mPopupWindow.isShowing()) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100657 return;
658 }
659
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100660 cancelOverflowAnimations();
Yohei Yukawabafc9082015-07-14 05:59:05 -0700661 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100662 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700663 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700664 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000665 // specify the popup position in screen coordinates.
666 mPopupWindow.update(
667 mCoordsOnWindow.x, mCoordsOnWindow.y,
668 mPopupWindow.getWidth(), mPopupWindow.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100669 }
670
Yohei Yukawabafc9082015-07-14 05:59:05 -0700671 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100672 refreshViewPort();
673
Abodunrinwa Toki51a8af62016-04-28 19:59:57 +0100674 // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
675 // landscape.
676 final int x = Math.min(
677 contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
678 mViewPortOnScreen.right - mPopupWindow.getWidth());
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100679
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000680 final int y;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100681
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000682 final int availableHeightAboveContent =
683 contentRectOnScreen.top - mViewPortOnScreen.top;
684 final int availableHeightBelowContent =
685 mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100686
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000687 final int margin = 2 * mMarginVertical;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000688 final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000689
690 if (!hasOverflow()) {
691 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100692 // There is enough space at the top of the content.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000693 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
694 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100695 // There is enough space at the bottom of the content.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700696 y = contentRectOnScreen.bottom;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000697 } else if (availableHeightBelowContent >= mLineHeight) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100698 // Just enough space to fit the toolbar with no vertical margins.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700699 y = contentRectOnScreen.bottom - mMarginVertical;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100700 } else {
701 // Not enough space. Prefer to position as high as possible.
702 y = Math.max(
Yohei Yukawabafc9082015-07-14 05:59:05 -0700703 mViewPortOnScreen.top,
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000704 contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100705 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000706 } else {
707 // Has an overflow.
708 final int minimumOverflowHeightWithMargin =
709 calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
710 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
711 contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
712 final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
713 mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100714
715 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100716 // There is enough space at the top of the content rect for the overflow.
717 // Position above and open upwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100718 updateOverflowHeight(availableHeightAboveContent - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000719 y = contentRectOnScreen.top - mPopupWindow.getHeight();
720 mOpenOverflowUpwards = true;
721 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100722 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100723 // There is enough space at the top of the content rect for the main panel
724 // but not the overflow.
725 // Position above but open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100726 updateOverflowHeight(availableHeightThroughContentDown - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000727 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
728 mOpenOverflowUpwards = false;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100729 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100730 // There is enough space at the bottom of the content rect for the overflow.
731 // Position below and open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100732 updateOverflowHeight(availableHeightBelowContent - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700733 y = contentRectOnScreen.bottom;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000734 mOpenOverflowUpwards = false;
735 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
Yohei Yukawabafc9082015-07-14 05:59:05 -0700736 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100737 // There is enough space at the bottom of the content rect for the main panel
738 // but not the overflow.
739 // Position below but open upwards.
740 updateOverflowHeight(availableHeightThroughContentUp - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000741 y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
742 mPopupWindow.getHeight();
743 mOpenOverflowUpwards = true;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100744 } else {
745 // Not enough space.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100746 // Position at the top of the view port and open downwards.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700747 updateOverflowHeight(mViewPortOnScreen.height() - margin);
748 y = mViewPortOnScreen.top;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000749 mOpenOverflowUpwards = false;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100750 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100751 }
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100752
Yohei Yukawa4b269972015-07-15 19:01:32 -0700753 // We later specify the location of PopupWindow relative to the attached window.
754 // The idea here is that 1) we can get the location of a View in both window coordinates
755 // and screen coordiantes, where the offset between them should be equal to the window
756 // origin, and 2) we can use an arbitrary for this calculation while calculating the
757 // location of the rootview is supposed to be least expensive.
758 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
759 // the following calculation.
760 mParent.getRootView().getLocationOnScreen(mTmpCoords);
761 int rootViewLeftOnScreen = mTmpCoords[0];
762 int rootViewTopOnScreen = mTmpCoords[1];
763 mParent.getRootView().getLocationInWindow(mTmpCoords);
764 int rootViewLeftOnWindow = mTmpCoords[0];
765 int rootViewTopOnWindow = mTmpCoords[1];
766 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
767 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
Abodunrinwa Toki51a8af62016-04-28 19:59:57 +0100768 mCoordsOnWindow.set(
769 Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100770 }
771
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100772 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100773 * Performs the "show" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100774 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100775 private void runShowAnimation() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000776 mShowAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100777 }
778
779 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100780 * Performs the "dismiss" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100781 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100782 private void runDismissAnimation() {
783 mDismissAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100784 }
785
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100786 /**
787 * Performs the "hide" animation on the floating popup.
788 */
789 private void runHideAnimation() {
790 mHideAnimation.start();
791 }
792
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100793 private void cancelDismissAndHideAnimations() {
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100794 mDismissAnimation.cancel();
795 mHideAnimation.cancel();
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100796 }
797
798 private void cancelOverflowAnimations() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000799 mContentContainer.clearAnimation();
800 mMainPanel.animate().cancel();
801 mOverflowPanel.animate().cancel();
802 mToArrow.stop();
803 mToOverflow.stop();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100804 }
805
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100806 private void openOverflow() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000807 final int targetWidth = mOverflowPanelSize.getWidth();
808 final int targetHeight = mOverflowPanelSize.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100809 final int startWidth = mContentContainer.getWidth();
810 final int startHeight = mContentContainer.getHeight();
811 final float startY = mContentContainer.getY();
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100812 final float left = mContentContainer.getX();
813 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100814 Animation widthAnimation = new Animation() {
815 @Override
816 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100817 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000818 setWidth(mContentContainer, startWidth + deltaWidth);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100819 if (isInRTLMode()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100820 mContentContainer.setX(left);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000821
822 // Lock the panels in place.
823 mMainPanel.setX(0);
824 mOverflowPanel.setX(0);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100825 } else {
826 mContentContainer.setX(right - mContentContainer.getWidth());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000827
828 // Offset the panels' positions so they look like they're locked in place
829 // on the screen.
830 mMainPanel.setX(mContentContainer.getWidth() - startWidth);
831 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100832 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100833 }
834 };
835 Animation heightAnimation = new Animation() {
836 @Override
837 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100838 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000839 setHeight(mContentContainer, startHeight + deltaHeight);
840 if (mOpenOverflowUpwards) {
841 mContentContainer.setY(
842 startY - (mContentContainer.getHeight() - startHeight));
843 positionContentYCoordinatesIfOpeningOverflowUpwards();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100844 }
845 }
846 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000847 final float overflowButtonStartX = mOverflowButton.getX();
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100848 final float overflowButtonTargetX = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000849 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
850 overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
851 Animation overflowButtonAnimation = new Animation() {
852 @Override
853 protected void applyTransformation(float interpolatedTime, Transformation t) {
854 float overflowButtonX = overflowButtonStartX
855 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100856 float deltaContainerWidth = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000857 0 :
858 mContentContainer.getWidth() - startWidth;
859 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
860 mOverflowButton.setX(actualOverflowButtonX);
861 }
862 };
863 widthAnimation.setInterpolator(mLogAccelerateInterpolator);
864 widthAnimation.setDuration(getAdjustedDuration(250));
865 heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
866 heightAnimation.setDuration(getAdjustedDuration(250));
867 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
868 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100869 mOpenOverflowAnimation.getAnimations().clear();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000870 mOpenOverflowAnimation.getAnimations().clear();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100871 mOpenOverflowAnimation.addAnimation(widthAnimation);
872 mOpenOverflowAnimation.addAnimation(heightAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000873 mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100874 mContentContainer.startAnimation(mOpenOverflowAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000875 mIsOverflowOpen = true;
876 mMainPanel.animate()
877 .alpha(0).withLayer()
878 .setInterpolator(mLinearOutSlowInInterpolator)
879 .setDuration(250)
880 .start();
881 mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100882 }
883
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100884 private void closeOverflow() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000885 final int targetWidth = mMainPanelSize.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100886 final int startWidth = mContentContainer.getWidth();
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100887 final float left = mContentContainer.getX();
888 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100889 Animation widthAnimation = new Animation() {
890 @Override
891 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100892 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000893 setWidth(mContentContainer, startWidth + deltaWidth);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100894 if (isInRTLMode()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100895 mContentContainer.setX(left);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000896
897 // Lock the panels in place.
898 mMainPanel.setX(0);
899 mOverflowPanel.setX(0);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100900 } else {
901 mContentContainer.setX(right - mContentContainer.getWidth());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000902
903 // Offset the panels' positions so they look like they're locked in place
904 // on the screen.
905 mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
906 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100907 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100908 }
909 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000910 final int targetHeight = mMainPanelSize.getHeight();
911 final int startHeight = mContentContainer.getHeight();
912 final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100913 Animation heightAnimation = new Animation() {
914 @Override
915 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100916 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000917 setHeight(mContentContainer, startHeight + deltaHeight);
918 if (mOpenOverflowUpwards) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100919 mContentContainer.setY(bottom - mContentContainer.getHeight());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000920 positionContentYCoordinatesIfOpeningOverflowUpwards();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100921 }
922 }
923 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000924 final float overflowButtonStartX = mOverflowButton.getX();
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100925 final float overflowButtonTargetX = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000926 overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
927 overflowButtonStartX + startWidth - mOverflowButton.getWidth();
928 Animation overflowButtonAnimation = new Animation() {
929 @Override
930 protected void applyTransformation(float interpolatedTime, Transformation t) {
931 float overflowButtonX = overflowButtonStartX
932 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100933 float deltaContainerWidth = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000934 0 :
935 mContentContainer.getWidth() - startWidth;
936 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
937 mOverflowButton.setX(actualOverflowButtonX);
938 }
939 };
940 widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
941 widthAnimation.setDuration(getAdjustedDuration(250));
942 heightAnimation.setInterpolator(mLogAccelerateInterpolator);
943 heightAnimation.setDuration(getAdjustedDuration(250));
944 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
945 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100946 mCloseOverflowAnimation.getAnimations().clear();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100947 mCloseOverflowAnimation.addAnimation(widthAnimation);
948 mCloseOverflowAnimation.addAnimation(heightAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000949 mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100950 mContentContainer.startAnimation(mCloseOverflowAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000951 mIsOverflowOpen = false;
952 mMainPanel.animate()
953 .alpha(1).withLayer()
954 .setInterpolator(mFastOutLinearInInterpolator)
955 .setDuration(100)
956 .start();
957 mOverflowPanel.animate()
958 .alpha(0).withLayer()
959 .setInterpolator(mLinearOutSlowInInterpolator)
960 .setDuration(150)
961 .start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100962 }
963
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +0000964 /**
965 * Defines the position of the floating toolbar popup panels when transition animation has
966 * stopped.
967 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000968 private void setPanelsStatesAtRestingPosition() {
969 mOverflowButton.setEnabled(true);
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +0000970 mOverflowPanel.awakenScrollBars();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100971
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000972 if (mIsOverflowOpen) {
973 // Set open state.
974 final Size containerSize = mOverflowPanelSize;
975 setSize(mContentContainer, containerSize);
976 mMainPanel.setAlpha(0);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000977 mMainPanel.setVisibility(View.INVISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000978 mOverflowPanel.setAlpha(1);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000979 mOverflowPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000980 mOverflowButton.setImageDrawable(mArrow);
Phil Weaver1d4e1832016-05-16 09:41:14 -0700981 mOverflowButton.setContentDescription(mContext.getString(
982 R.string.floating_toolbar_close_overflow_description));
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100983
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000984 // Update x-coordinates depending on RTL state.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100985 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000986 mContentContainer.setX(mMarginHorizontal); // align left
987 mMainPanel.setX(0); // align left
988 mOverflowButton.setX( // align right
989 containerSize.getWidth() - mOverflowButtonSize.getWidth());
990 mOverflowPanel.setX(0); // align left
991 } else {
992 mContentContainer.setX( // align right
Abodunrinwa Toki0780f3a2016-05-03 16:25:41 +0100993 mPopupWindow.getWidth() -
994 containerSize.getWidth() - mMarginHorizontal);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000995 mMainPanel.setX(-mContentContainer.getX()); // align right
996 mOverflowButton.setX(0); // align left
997 mOverflowPanel.setX(0); // align left
998 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100999
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001000 // Update y-coordinates depending on overflow's open direction.
1001 if (mOpenOverflowUpwards) {
1002 mContentContainer.setY(mMarginVertical); // align top
1003 mMainPanel.setY( // align bottom
1004 containerSize.getHeight() - mContentContainer.getHeight());
1005 mOverflowButton.setY( // align bottom
1006 containerSize.getHeight() - mOverflowButtonSize.getHeight());
1007 mOverflowPanel.setY(0); // align top
1008 } else {
1009 // opens downwards.
1010 mContentContainer.setY(mMarginVertical); // align top
1011 mMainPanel.setY(0); // align top
1012 mOverflowButton.setY(0); // align top
1013 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
1014 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001015 } else {
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001016 // Overflow not open. Set closed state.
1017 final Size containerSize = mMainPanelSize;
1018 setSize(mContentContainer, containerSize);
1019 mMainPanel.setAlpha(1);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +00001020 mMainPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001021 mOverflowPanel.setAlpha(0);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +00001022 mOverflowPanel.setVisibility(View.INVISIBLE);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001023 mOverflowButton.setImageDrawable(mOverflow);
Phil Weaver1d4e1832016-05-16 09:41:14 -07001024 mOverflowButton.setContentDescription(mContext.getString(
1025 R.string.floating_toolbar_open_overflow_description));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001026
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001027 if (hasOverflow()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001028 // Update x-coordinates depending on RTL state.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +01001029 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001030 mContentContainer.setX(mMarginHorizontal); // align left
1031 mMainPanel.setX(0); // align left
1032 mOverflowButton.setX(0); // align left
1033 mOverflowPanel.setX(0); // align left
1034 } else {
Abodunrinwa Toki0780f3a2016-05-03 16:25:41 +01001035 mContentContainer.setX( // align right
1036 mPopupWindow.getWidth() -
1037 containerSize.getWidth() - mMarginHorizontal);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001038 mMainPanel.setX(0); // align left
1039 mOverflowButton.setX( // align right
1040 containerSize.getWidth() - mOverflowButtonSize.getWidth());
1041 mOverflowPanel.setX( // align right
1042 containerSize.getWidth() - mOverflowPanelSize.getWidth());
1043 }
1044
1045 // Update y-coordinates depending on overflow's open direction.
1046 if (mOpenOverflowUpwards) {
1047 mContentContainer.setY( // align bottom
1048 mMarginVertical +
1049 mOverflowPanelSize.getHeight() - containerSize.getHeight());
1050 mMainPanel.setY(0); // align top
1051 mOverflowButton.setY(0); // align top
1052 mOverflowPanel.setY( // align bottom
1053 containerSize.getHeight() - mOverflowPanelSize.getHeight());
1054 } else {
1055 // opens downwards.
1056 mContentContainer.setY(mMarginVertical); // align top
1057 mMainPanel.setY(0); // align top
1058 mOverflowButton.setY(0); // align top
1059 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
1060 }
1061 } else {
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001062 // No overflow.
1063 mContentContainer.setX(mMarginHorizontal); // align left
1064 mContentContainer.setY(mMarginVertical); // align top
1065 mMainPanel.setX(0); // align left
1066 mMainPanel.setY(0); // align top
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001067 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001068 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001069 }
1070
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001071 private void updateOverflowHeight(int suggestedHeight) {
1072 if (hasOverflow()) {
1073 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001074 mLineHeight;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001075 final int newHeight = calculateOverflowHeight(maxItemSize);
1076 if (mOverflowPanelSize.getHeight() != newHeight) {
1077 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
1078 }
1079 setSize(mOverflowPanel, mOverflowPanelSize);
1080 if (mIsOverflowOpen) {
1081 setSize(mContentContainer, mOverflowPanelSize);
1082 if (mOpenOverflowUpwards) {
1083 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1084 mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1085 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1086 }
1087 } else {
1088 setSize(mContentContainer, mMainPanelSize);
1089 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001090 updatePopupSize();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001091 }
1092 }
1093
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001094 private void updatePopupSize() {
1095 int width = 0;
1096 int height = 0;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001097 if (mMainPanelSize != null) {
1098 width = Math.max(width, mMainPanelSize.getWidth());
1099 height = Math.max(height, mMainPanelSize.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001100 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001101 if (mOverflowPanelSize != null) {
1102 width = Math.max(width, mOverflowPanelSize.getWidth());
1103 height = Math.max(height, mOverflowPanelSize.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001104 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001105 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1106 mPopupWindow.setHeight(height + mMarginVertical * 2);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001107 maybeComputeTransitionDurationScale();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001108 }
1109
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001110 private void refreshViewPort() {
Yohei Yukawabafc9082015-07-14 05:59:05 -07001111 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001112 }
1113
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001114 private int getAdjustedToolbarWidth(int suggestedWidth) {
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001115 int width = suggestedWidth;
1116 refreshViewPort();
Yohei Yukawabafc9082015-07-14 05:59:05 -07001117 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001118 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1119 if (width <= 0) {
1120 width = mParent.getResources()
1121 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1122 }
1123 return Math.min(width, maximumWidth);
1124 }
1125
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001126 /**
1127 * Sets the touchable region of this popup to be zero. This means that all touch events on
1128 * this popup will go through to the surface behind it.
1129 */
1130 private void setZeroTouchableSurface() {
1131 mTouchableRegion.setEmpty();
1132 }
1133
1134 /**
1135 * Sets the touchable region of this popup to be the area occupied by its content.
1136 */
1137 private void setContentAreaAsTouchableSurface() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001138 Preconditions.checkNotNull(mMainPanelSize);
1139 final int width;
1140 final int height;
1141 if (mIsOverflowOpen) {
1142 Preconditions.checkNotNull(mOverflowPanelSize);
1143 width = mOverflowPanelSize.getWidth();
1144 height = mOverflowPanelSize.getHeight();
1145 } else {
1146 width = mMainPanelSize.getWidth();
1147 height = mMainPanelSize.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001148 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001149 mTouchableRegion.set(
1150 (int) mContentContainer.getX(),
1151 (int) mContentContainer.getY(),
1152 (int) mContentContainer.getX() + width,
1153 (int) mContentContainer.getY() + height);
1154 }
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +01001155
1156 /**
1157 * Make the touchable area of this popup be the area specified by mTouchableRegion.
1158 * This should be called after the popup window has been dismissed (dismiss/hide)
1159 * and is probably being re-shown with a new content root view.
1160 */
1161 private void setTouchableSurfaceInsetsComputer() {
1162 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1163 .getRootView()
1164 .getViewTreeObserver();
1165 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1166 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1167 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001168
Abodunrinwa Toki49482f82016-07-01 19:57:50 +01001169 private boolean isInRTLMode() {
1170 return mContext.getApplicationInfo().hasRtlSupport()
1171 && mContext.getResources().getConfiguration().getLayoutDirection()
1172 == View.LAYOUT_DIRECTION_RTL;
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001173 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001174
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001175 private boolean hasOverflow() {
1176 return mOverflowPanelSize != null;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001177 }
1178
1179 /**
1180 * Fits as many menu items in the main panel and returns a list of the menu items that
1181 * were not fit in.
1182 *
1183 * @return The menu items that are not included in this main panel.
1184 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001185 public List<MenuItem> layoutMainPanelItems(
1186 List<MenuItem> menuItems, final int toolbarWidth) {
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001187 Preconditions.checkNotNull(menuItems);
1188
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001189 int availableWidth = toolbarWidth;
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001190
1191 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
1192 // add the overflow menu items to the end of the remainingMenuItems list.
1193 final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
1194 for (MenuItem menuItem : menuItems) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001195 if (menuItem.getItemId() != android.R.id.textAssist
1196 && menuItem.requiresOverflow()) {
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001197 overflowMenuItems.add(menuItem);
1198 } else {
1199 remainingMenuItems.add(menuItem);
1200 }
1201 }
1202 remainingMenuItems.addAll(overflowMenuItems);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001203
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001204 mMainPanel.removeAllViews();
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001205 mMainPanel.setPaddingRelative(0, 0, 0, 0);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001206
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001207 int lastGroupId = -1;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001208 boolean isFirstItem = true;
1209 while (!remainingMenuItems.isEmpty()) {
1210 final MenuItem menuItem = remainingMenuItems.peek();
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001211
1212 // if this is the first item, regardless of requiresOverflow(), it should be
1213 // displayed on the main panel. Otherwise all items including this one will be
1214 // overflow items, and should be displayed in overflow panel.
1215 if(!isFirstItem && menuItem.requiresOverflow()) {
1216 break;
1217 }
1218
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001219 final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
1220 final View menuItemButton = createMenuItemButton(
1221 mContext, menuItem, mIconTextSpacing, showIcon);
Mihai Popa66d41f82018-04-17 16:21:10 +01001222 if (!showIcon && menuItemButton instanceof LinearLayout) {
1223 ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
1224 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001225
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001226 // Adding additional start padding for the first button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001227 if (isFirstItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001228 menuItemButton.setPaddingRelative(
1229 (int) (1.5 * menuItemButton.getPaddingStart()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001230 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001231 menuItemButton.getPaddingEnd(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001232 menuItemButton.getPaddingBottom());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001233 }
1234
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001235 // Adding additional end padding for the last button to even out button spacing.
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001236 boolean isLastItem = remainingMenuItems.size() == 1;
1237 if (isLastItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001238 menuItemButton.setPaddingRelative(
1239 menuItemButton.getPaddingStart(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001240 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001241 (int) (1.5 * menuItemButton.getPaddingEnd()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001242 menuItemButton.getPaddingBottom());
1243 }
1244
1245 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001246 final int menuItemButtonWidth = Math.min(
1247 menuItemButton.getMeasuredWidth(), toolbarWidth);
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001248
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001249 // Check if we can fit an item while reserving space for the overflowButton.
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001250 final boolean canFitWithOverflow =
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001251 menuItemButtonWidth <=
Mihai Popa66d41f82018-04-17 16:21:10 +01001252 availableWidth - mOverflowButtonSize.getWidth();
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001253 final boolean canFitNoOverflow =
Mihai Popa66d41f82018-04-17 16:21:10 +01001254 isLastItem && menuItemButtonWidth <= availableWidth;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001255 if (canFitWithOverflow || canFitNoOverflow) {
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001256 setButtonTagAndClickListener(menuItemButton, menuItem);
Vladislav Kaznacheev2a00f982017-02-23 17:33:51 -08001257 // Set tooltips for main panel items, but not overflow items (b/35726766).
1258 menuItemButton.setTooltipText(menuItem.getTooltipText());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001259 mMainPanel.addView(menuItemButton);
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001260 final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
Mihai Popa66d41f82018-04-17 16:21:10 +01001261 params.width = menuItemButtonWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001262 menuItemButton.setLayoutParams(params);
Mihai Popa66d41f82018-04-17 16:21:10 +01001263 availableWidth -= menuItemButtonWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001264 remainingMenuItems.pop();
1265 } else {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001266 break;
1267 }
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001268 lastGroupId = menuItem.getGroupId();
1269 isFirstItem = false;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001270 }
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001271
1272 if (!remainingMenuItems.isEmpty()) {
1273 // Reserve space for overflowButton.
1274 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1275 }
1276
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001277 mMainPanelSize = measure(mMainPanel);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001278 return remainingMenuItems;
1279 }
1280
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001281 private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1282 ArrayAdapter<MenuItem> overflowPanelAdapter =
1283 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1284 overflowPanelAdapter.clear();
1285 final int size = menuItems.size();
1286 for (int i = 0; i < size; i++) {
1287 overflowPanelAdapter.add(menuItems.get(i));
1288 }
1289 mOverflowPanel.setAdapter(overflowPanelAdapter);
1290 if (mOpenOverflowUpwards) {
1291 mOverflowPanel.setY(0);
1292 } else {
1293 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1294 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001295
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001296 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1297 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1298 mOverflowPanelSize = new Size(width, height);
1299 setSize(mOverflowPanel, mOverflowPanelSize);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001300 }
1301
1302 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001303 * Resets the content container and appropriately position it's panels.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001304 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001305 private void preparePopupContent() {
1306 mContentContainer.removeAllViews();
1307
1308 // Add views in the specified order so they stack up as expected.
1309 // Order: overflowPanel, mainPanel, overflowButton.
1310 if (hasOverflow()) {
1311 mContentContainer.addView(mOverflowPanel);
1312 }
1313 mContentContainer.addView(mMainPanel);
1314 if (hasOverflow()) {
1315 mContentContainer.addView(mOverflowButton);
1316 }
1317 setPanelsStatesAtRestingPosition();
1318 setContentAreaAsTouchableSurface();
1319
1320 // The positioning of contents in RTL is wrong when the view is first rendered.
1321 // Hide the view and post a runnable to recalculate positions and render the view.
1322 // TODO: Investigate why this happens and fix.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +01001323 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001324 mContentContainer.setAlpha(0);
1325 mContentContainer.post(mPreparePopupContentRTLHelper);
1326 }
1327 }
1328
1329 /**
1330 * Clears out the panels and their container. Resets their calculated sizes.
1331 */
1332 private void clearPanels() {
1333 mOverflowPanelSize = null;
1334 mMainPanelSize = null;
1335 mIsOverflowOpen = false;
1336 mMainPanel.removeAllViews();
1337 ArrayAdapter<MenuItem> overflowPanelAdapter =
1338 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1339 overflowPanelAdapter.clear();
1340 mOverflowPanel.setAdapter(overflowPanelAdapter);
1341 mContentContainer.removeAllViews();
1342 }
1343
1344 private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1345 if (mOpenOverflowUpwards) {
1346 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1347 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1348 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1349 }
1350 }
1351
1352 private int getOverflowWidth() {
1353 int overflowWidth = 0;
1354 final int count = mOverflowPanel.getAdapter().getCount();
1355 for (int i = 0; i < count; i++) {
1356 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1357 overflowWidth =
1358 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1359 }
1360 return overflowWidth;
1361 }
1362
1363 private int calculateOverflowHeight(int maxItemSize) {
1364 // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1365 int actualSize = Math.min(
1366 MAX_OVERFLOW_SIZE,
1367 Math.min(
1368 Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1369 mOverflowPanel.getCount()));
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001370 int extension = 0;
1371 if (actualSize < mOverflowPanel.getCount()) {
1372 // The overflow will require scrolling to get to all the items.
1373 // Extend the height so that part of the hidden items is displayed.
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001374 extension = (int) (mLineHeight * 0.5f);
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001375 }
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001376 return actualSize * mLineHeight
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001377 + mOverflowButtonSize.getHeight()
1378 + extension;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001379 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001380
1381 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001382 menuItemButton.setTag(menuItem);
1383 menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001384 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001385
1386 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001387 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1388 * animations. See comment about this in the code.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001389 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001390 private int getAdjustedDuration(int originalDuration) {
1391 if (mTransitionDurationScale < 150) {
1392 // For smaller transition, decrease the time.
1393 return Math.max(originalDuration - 50, 0);
1394 } else if (mTransitionDurationScale > 300) {
1395 // For bigger transition, increase the time.
1396 return originalDuration + 50;
1397 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001398
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001399 // Scale the animation duration with getDurationScale(). This allows
1400 // android.view.animation.* animations to scale just like android.animation.* animations
1401 // when animator duration scale is adjusted in "Developer Options".
1402 // For this reason, do not use this method for android.animation.* animations.
1403 return (int) (originalDuration * ValueAnimator.getDurationScale());
1404 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001405
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001406 private void maybeComputeTransitionDurationScale() {
Abodunrinwa Tokid1cd7fd2016-01-27 22:27:31 +00001407 if (mMainPanelSize != null && mOverflowPanelSize != null) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001408 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1409 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1410 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1411 mContentContainer.getContext().getResources().getDisplayMetrics().density);
1412 }
1413 }
1414
1415 private ViewGroup createMainPanel() {
1416 ViewGroup mainPanel = new LinearLayout(mContext) {
1417 @Override
1418 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1419 if (isOverflowAnimating()) {
1420 // Update widthMeasureSpec to make sure that this view is not clipped
1421 // as we offset it's coordinates with respect to it's parent.
1422 widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1423 mMainPanelSize.getWidth(),
1424 MeasureSpec.EXACTLY);
1425 }
1426 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1427 }
1428
1429 @Override
1430 public boolean onInterceptTouchEvent(MotionEvent ev) {
1431 // Intercept the touch event while the overflow is animating.
1432 return isOverflowAnimating();
1433 }
1434 };
1435 return mainPanel;
1436 }
1437
1438 private ImageButton createOverflowButton() {
1439 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1440 .inflate(R.layout.floating_popup_overflow_button, null);
1441 overflowButton.setImageDrawable(mOverflow);
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001442 overflowButton.setOnClickListener(v -> {
1443 if (mIsOverflowOpen) {
1444 overflowButton.setImageDrawable(mToOverflow);
1445 mToOverflow.start();
1446 closeOverflow();
1447 } else {
1448 overflowButton.setImageDrawable(mToArrow);
1449 mToArrow.start();
1450 openOverflow();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001451 }
1452 });
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001453 return overflowButton;
1454 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001455
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001456 private OverflowPanel createOverflowPanel() {
1457 final OverflowPanel overflowPanel = new OverflowPanel(this);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001458 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1459 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1460 overflowPanel.setDivider(null);
1461 overflowPanel.setDividerHeight(0);
1462
1463 final ArrayAdapter adapter =
1464 new ArrayAdapter<MenuItem>(mContext, 0) {
1465 @Override
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001466 public View getView(int position, View convertView, ViewGroup parent) {
1467 return mOverflowPanelViewHelper.getView(
1468 getItem(position), mOverflowPanelSize.getWidth(), convertView);
1469 }
1470 };
1471 overflowPanel.setAdapter(adapter);
1472
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001473 overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
1474 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1475 if (mOnMenuItemClickListener != null) {
1476 mOnMenuItemClickListener.onMenuItemClick(menuItem);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001477 }
1478 });
1479
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001480 return overflowPanel;
1481 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001482
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001483 private boolean isOverflowAnimating() {
1484 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1485 && !mOpenOverflowAnimation.hasEnded();
1486 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1487 && !mCloseOverflowAnimation.hasEnded();
1488 return overflowOpening || overflowClosing;
1489 }
1490
1491 private Animation.AnimationListener createOverflowAnimationListener() {
1492 Animation.AnimationListener listener = new Animation.AnimationListener() {
1493 @Override
1494 public void onAnimationStart(Animation animation) {
1495 // Disable the overflow button while it's animating.
1496 // It will be re-enabled when the animation stops.
1497 mOverflowButton.setEnabled(false);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +00001498 // Ensure both panels have visibility turned on when the overflow animation
1499 // starts.
1500 mMainPanel.setVisibility(View.VISIBLE);
1501 mOverflowPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001502 }
1503
1504 @Override
1505 public void onAnimationEnd(Animation animation) {
1506 // Posting this because it seems like this is called before the animation
1507 // actually ends.
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001508 mContentContainer.post(() -> {
1509 setPanelsStatesAtRestingPosition();
1510 setContentAreaAsTouchableSurface();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001511 });
1512 }
1513
1514 @Override
1515 public void onAnimationRepeat(Animation animation) {
1516 }
1517 };
1518 return listener;
1519 }
1520
1521 private static Size measure(View view) {
1522 Preconditions.checkState(view.getParent() == null);
1523 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1524 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1525 }
1526
1527 private static void setSize(View view, int width, int height) {
1528 view.setMinimumWidth(width);
1529 view.setMinimumHeight(height);
1530 ViewGroup.LayoutParams params = view.getLayoutParams();
1531 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1532 params.width = width;
1533 params.height = height;
1534 view.setLayoutParams(params);
1535 }
1536
1537 private static void setSize(View view, Size size) {
1538 setSize(view, size.getWidth(), size.getHeight());
1539 }
1540
1541 private static void setWidth(View view, int width) {
1542 ViewGroup.LayoutParams params = view.getLayoutParams();
1543 setSize(view, width, params.height);
1544 }
1545
1546 private static void setHeight(View view, int height) {
1547 ViewGroup.LayoutParams params = view.getLayoutParams();
1548 setSize(view, params.width, height);
1549 }
1550
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001551 /**
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001552 * A custom ListView for the overflow panel.
1553 */
1554 private static final class OverflowPanel extends ListView {
1555
1556 private final FloatingToolbarPopup mPopup;
1557
1558 OverflowPanel(FloatingToolbarPopup popup) {
1559 super(Preconditions.checkNotNull(popup).mContext);
1560 this.mPopup = popup;
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001561 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
Abodunrinwa Toki8949faf2016-04-06 14:15:26 +01001562 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001563 }
1564
1565 @Override
1566 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1567 // Update heightMeasureSpec to make sure that this view is not clipped
1568 // as we offset it's coordinates with respect to it's parent.
1569 int height = mPopup.mOverflowPanelSize.getHeight()
1570 - mPopup.mOverflowButtonSize.getHeight();
1571 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1572 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1573 }
1574
1575 @Override
1576 public boolean dispatchTouchEvent(MotionEvent ev) {
1577 if (mPopup.isOverflowAnimating()) {
1578 // Eat the touch event.
1579 return true;
1580 }
1581 return super.dispatchTouchEvent(ev);
1582 }
1583
1584 @Override
1585 protected boolean awakenScrollBars() {
1586 return super.awakenScrollBars();
1587 }
1588 }
1589
1590 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001591 * A custom interpolator used for various floating toolbar animations.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001592 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001593 private static final class LogAccelerateInterpolator implements Interpolator {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001594
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001595 private static final int BASE = 100;
1596 private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001597
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001598 private static float computeLog(float t, int base) {
1599 return (float) (1 - Math.pow(base, -t));
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001600 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001601
1602 @Override
1603 public float getInterpolation(float t) {
1604 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1605 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001606 }
1607
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001608 /**
1609 * A helper for generating views for the overflow panel.
1610 */
1611 private static final class OverflowPanelViewHelper {
1612
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001613 private final View mCalculator;
1614 private final int mIconTextSpacing;
1615 private final int mSidePadding;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001616
1617 private final Context mContext;
1618
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +01001619 public OverflowPanelViewHelper(Context context, int iconTextSpacing) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001620 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +01001621 mIconTextSpacing = iconTextSpacing;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001622 mSidePadding = context.getResources()
1623 .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
1624 mCalculator = createMenuButton(null);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001625 }
1626
1627 public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001628 Preconditions.checkNotNull(menuItem);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001629 if (convertView != null) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001630 updateMenuItemButton(
1631 convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001632 } else {
1633 convertView = createMenuButton(menuItem);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001634 }
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001635 convertView.setMinimumWidth(minimumWidth);
1636 return convertView;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001637 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001638
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001639 public int calculateWidth(MenuItem menuItem) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001640 updateMenuItemButton(
1641 mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001642 mCalculator.measure(
1643 View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1644 return mCalculator.getMeasuredWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001645 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001646
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001647 private View createMenuButton(MenuItem menuItem) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001648 View button = createMenuItemButton(
1649 mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001650 button.setPadding(mSidePadding, 0, mSidePadding, 0);
1651 return button;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001652 }
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001653
1654 private boolean shouldShowIcon(MenuItem menuItem) {
1655 if (menuItem != null) {
1656 return menuItem.getGroupId() == android.R.id.textAssist;
1657 }
1658 return false;
1659 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001660 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001661 }
1662
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001663 /**
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001664 * Creates and returns a menu button for the specified menu item.
1665 */
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001666 private static View createMenuItemButton(
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001667 Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001668 final View menuItemButton = LayoutInflater.from(context)
1669 .inflate(R.layout.floating_popup_menu_button, null);
1670 if (menuItem != null) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001671 updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001672 }
1673 return menuItemButton;
1674 }
1675
1676 /**
1677 * Updates the specified menu item button with the specified menu item data.
1678 */
1679 private static void updateMenuItemButton(
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001680 View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
1681 final TextView buttonText = menuItemButton.findViewById(
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001682 R.id.floating_toolbar_menu_item_text);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001683 buttonText.setEllipsize(null);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001684 if (TextUtils.isEmpty(menuItem.getTitle())) {
1685 buttonText.setVisibility(View.GONE);
1686 } else {
1687 buttonText.setVisibility(View.VISIBLE);
1688 buttonText.setText(menuItem.getTitle());
1689 }
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001690 final ImageView buttonIcon = menuItemButton.findViewById(
1691 R.id.floating_toolbar_menu_item_image);
1692 if (menuItem.getIcon() == null || !showIcon) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001693 buttonIcon.setVisibility(View.GONE);
1694 if (buttonText != null) {
1695 buttonText.setPaddingRelative(0, 0, 0, 0);
Vladislav Kaznacheev7039cbc2017-01-04 10:15:31 -08001696 }
1697 } else {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001698 buttonIcon.setVisibility(View.VISIBLE);
1699 buttonIcon.setImageDrawable(menuItem.getIcon());
1700 if (buttonText != null) {
1701 buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
1702 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001703 }
Vladislav Kaznacheev7039cbc2017-01-04 10:15:31 -08001704 final CharSequence contentDescription = menuItem.getContentDescription();
1705 if (TextUtils.isEmpty(contentDescription)) {
1706 menuItemButton.setContentDescription(menuItem.getTitle());
1707 } else {
1708 menuItemButton.setContentDescription(contentDescription);
1709 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001710 }
1711
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001712 private static ViewGroup createContentContainer(Context context) {
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001713 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001714 .inflate(R.layout.floating_popup_container, null);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001715 contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1716 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001717 contentContainer.setTag(FLOATING_TOOLBAR_TAG);
Mihai Popa17f91d52018-04-10 16:10:16 +01001718 contentContainer.setClipToOutline(true);
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001719 return contentContainer;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001720 }
1721
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001722 private static PopupWindow createPopupWindow(ViewGroup content) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001723 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1724 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
Yohei Yukawabafc9082015-07-14 05:59:05 -07001725 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1726 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1727 popupWindow.setClippingEnabled(false);
Abodunrinwa Toki103d48e2015-04-16 17:27:48 +01001728 popupWindow.setWindowLayoutType(
1729 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001730 popupWindow.setAnimationStyle(0);
1731 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1732 content.setLayoutParams(new ViewGroup.LayoutParams(
1733 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1734 popupContentHolder.addView(content);
1735 return popupWindow;
1736 }
1737
1738 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001739 * Creates an "appear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001740 *
1741 * @param view The view to animate
1742 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001743 private static AnimatorSet createEnterAnimation(View view) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001744 AnimatorSet animation = new AnimatorSet();
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001745 animation.playTogether(
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001746 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001747 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001748 }
1749
1750 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001751 * Creates a "disappear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001752 *
1753 * @param view The view to animate
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001754 * @param startDelay The start delay of the animation
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001755 * @param listener The animation listener
1756 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001757 private static AnimatorSet createExitAnimation(
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001758 View view, int startDelay, Animator.AnimatorListener listener) {
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001759 AnimatorSet animation = new AnimatorSet();
1760 animation.playTogether(
Abodunrinwa Tokib9acbe42015-09-16 19:47:38 +01001761 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001762 animation.setStartDelay(startDelay);
1763 animation.addListener(listener);
1764 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001765 }
1766
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -07001767 /**
1768 * Returns a re-themed context with controlled look and feel for views.
1769 */
1770 private static Context applyDefaultTheme(Context originalContext) {
1771 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1772 boolean isLightTheme = a.getBoolean(0, true);
Mihai Popafb4b6b82018-03-01 16:08:14 +00001773 int themeId
1774 = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -07001775 a.recycle();
1776 return new ContextThemeWrapper(originalContext, themeId);
1777 }
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +00001778}