blob: f70c554d9bf0194713620c155a8ee06c7b28c847 [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 Toki0c7ed282015-03-27 15:02:03 +000024import android.content.Context;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -070025import android.content.res.TypedArray;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000026import android.graphics.Color;
27import android.graphics.Point;
28import android.graphics.Rect;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010029import android.graphics.Region;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000030import android.graphics.drawable.AnimatedVectorDrawable;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000031import android.graphics.drawable.ColorDrawable;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000032import android.graphics.drawable.Drawable;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010033import android.text.TextUtils;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010034import android.util.Size;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +000035import android.util.TypedValue;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -070036import android.view.ContextThemeWrapper;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000037import android.view.Gravity;
38import android.view.LayoutInflater;
39import android.view.Menu;
40import android.view.MenuItem;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000041import android.view.MotionEvent;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000042import android.view.View;
43import android.view.View.MeasureSpec;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +000044import android.view.View.OnLayoutChangeListener;
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +000045import android.view.ViewConfiguration;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000046import android.view.ViewGroup;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010047import android.view.ViewTreeObserver;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000048import android.view.Window;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010049import android.view.WindowManager;
50import android.view.animation.Animation;
51import android.view.animation.AnimationSet;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +000052import android.view.animation.AnimationUtils;
53import android.view.animation.Interpolator;
Siyamed Sinir484c2e22017-06-07 16:26:19 -070054import android.view.animation.Transformation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010055import android.widget.ArrayAdapter;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000056import android.widget.ImageButton;
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +010057import android.widget.ImageView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000058import android.widget.LinearLayout;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010059import android.widget.ListView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000060import android.widget.PopupWindow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010061import android.widget.TextView;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000062
Siyamed Sinir484c2e22017-06-07 16:26:19 -070063import com.android.internal.R;
64import com.android.internal.util.Preconditions;
65
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000066import java.util.ArrayList;
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +010067import java.util.Comparator;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000068import java.util.LinkedList;
69import java.util.List;
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010070import java.util.Objects;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010071
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000072/**
73 * A floating toolbar for showing contextual menu items.
74 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
75 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
76 * The horizontal toolbar morphs into the vertical overflow view.
77 */
78public final class FloatingToolbar {
79
Abodunrinwa Toki517adad2015-04-07 22:13:51 +010080 // This class is responsible for the public API of the floating toolbar.
81 // It delegates rendering operations to the FloatingToolbarPopup.
82
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +010083 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
84
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000085 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010086 item -> false;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000087
88 private final Context mContext;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +000089 private final Window mWindow;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000090 private final FloatingToolbarPopup mPopup;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000091
92 private final Rect mContentRect = new Rect();
Abodunrinwa Tokic107b0e2015-06-25 21:33:51 -070093 private final Rect mPreviousContentRect = new Rect();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000094
95 private Menu mMenu;
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +010096 private List<MenuItem> mShowingMenuItems = new ArrayList<>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000097 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +000098
99 private int mSuggestedWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100100 private boolean mWidthChanged = true;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000101
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000102 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
103
104 private final Rect mNewRect = new Rect();
105 private final Rect mOldRect = new Rect();
106
Clara Bayarri6bc12242015-06-16 18:04:55 +0100107 @Override
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000108 public void onLayoutChange(
109 View view,
110 int newLeft, int newRight, int newTop, int newBottom,
111 int oldLeft, int oldRight, int oldTop, int oldBottom) {
112 mNewRect.set(newLeft, newRight, newTop, newBottom);
113 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
114 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
Clara Bayarri6bc12242015-06-16 18:04:55 +0100115 mWidthChanged = true;
116 updateLayout();
117 }
118 }
Clara Bayarri6bc12242015-06-16 18:04:55 +0100119 };
120
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000121 /**
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +0100122 * Sorts the list of menu items to conform to certain requirements.
123 */
124 private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
125 // Ensure the assist menu item is always the first item:
126 if (menuItem1.getItemId() == android.R.id.textAssist) {
127 return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
128 }
129 if (menuItem2.getItemId() == android.R.id.textAssist) {
130 return 1;
131 }
132
133 // Order by SHOW_AS_ACTION type:
134 if (menuItem1.requiresActionButton()) {
135 return menuItem2.requiresActionButton() ? 0 : -1;
136 }
137 if (menuItem2.requiresActionButton()) {
138 return 1;
139 }
140 if (menuItem1.requiresOverflow()) {
141 return menuItem2.requiresOverflow() ? 0 : 1;
142 }
143 if (menuItem2.requiresOverflow()) {
144 return -1;
145 }
146
147 // Order by order value:
148 return menuItem1.getOrder() - menuItem2.getOrder();
149 };
150
151 /**
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000152 * Initializes a floating toolbar.
153 */
Tarandeep Singhc9c83a92017-08-29 14:39:22 -0700154 public FloatingToolbar(Window window) {
155 // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
156 // supports multi-display.
157 mContext = applyDefaultTheme(window.getContext());
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000158 mWindow = Preconditions.checkNotNull(window);
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700159 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000160 }
161
162 /**
163 * Sets the menu to be shown in this floating toolbar.
164 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
165 * toolbar.
166 */
167 public FloatingToolbar setMenu(Menu menu) {
168 mMenu = Preconditions.checkNotNull(menu);
169 return this;
170 }
171
172 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100173 * Sets the custom listener for invocation of menu items in this floating toolbar.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000174 */
175 public FloatingToolbar setOnMenuItemClickListener(
176 MenuItem.OnMenuItemClickListener menuItemClickListener) {
177 if (menuItemClickListener != null) {
178 mMenuItemClickListener = menuItemClickListener;
179 } else {
180 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
181 }
182 return this;
183 }
184
185 /**
186 * Sets the content rectangle. This is the area of the interesting content that this toolbar
187 * should avoid obstructing.
188 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
189 * toolbar.
190 */
191 public FloatingToolbar setContentRect(Rect rect) {
192 mContentRect.set(Preconditions.checkNotNull(rect));
193 return this;
194 }
195
196 /**
197 * Sets the suggested width of this floating toolbar.
198 * The actual width will be about this size but there are no guarantees that it will be exactly
199 * the suggested width.
200 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
201 * toolbar.
202 */
203 public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100204 // Check if there's been a substantial width spec change.
205 int difference = Math.abs(suggestedWidth - mSuggestedWidth);
206 mWidthChanged = difference > (mSuggestedWidth * 0.2);
207
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000208 mSuggestedWidth = suggestedWidth;
209 return this;
210 }
211
212 /**
213 * Shows this floating toolbar.
214 */
215 public FloatingToolbar show() {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000216 registerOrientationHandler();
217 doShow();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000218 return this;
219 }
220
221 /**
222 * Updates this floating toolbar to reflect recent position and view updates.
223 * NOTE: This method is a no-op if the toolbar isn't showing.
224 */
225 public FloatingToolbar updateLayout() {
226 if (mPopup.isShowing()) {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000227 doShow();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000228 }
229 return this;
230 }
231
232 /**
233 * Dismisses this floating toolbar.
234 */
235 public void dismiss() {
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000236 unregisterOrientationHandler();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000237 mPopup.dismiss();
238 }
239
240 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100241 * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
242 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
243 */
244 public void hide() {
245 mPopup.hide();
246 }
247
248 /**
249 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000250 */
251 public boolean isShowing() {
252 return mPopup.isShowing();
253 }
254
255 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100256 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
257 */
258 public boolean isHidden() {
259 return mPopup.isHidden();
260 }
261
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000262 private void doShow() {
263 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +0100264 menuItems.sort(mMenuItemComparator);
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000265 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
266 mPopup.dismiss();
267 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100268 mShowingMenuItems = menuItems;
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000269 }
270 if (!mPopup.isShowing()) {
271 mPopup.show(mContentRect);
272 } else if (!mPreviousContentRect.equals(mContentRect)) {
273 mPopup.updateCoordinates(mContentRect);
274 }
275 mWidthChanged = false;
276 mPreviousContentRect.set(mContentRect);
277 }
278
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100279 /**
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100280 * Returns true if this floating toolbar is currently showing the specified menu items.
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000281 */
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100282 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100283 if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
284 return false;
285 }
286
287 final int size = menuItems.size();
288 for (int i = 0; i < size; i++) {
289 final MenuItem menuItem = menuItems.get(i);
290 final MenuItem showingItem = mShowingMenuItems.get(i);
291 if (menuItem.getItemId() != showingItem.getItemId()
292 || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
293 || !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
294 || menuItem.getGroupId() != showingItem.getGroupId()) {
295 return false;
296 }
297 }
298
299 return true;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000300 }
301
302 /**
303 * Returns the visible and enabled menu items in the specified menu.
304 * This method is recursive.
305 */
306 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100307 List<MenuItem> menuItems = new ArrayList<>();
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000308 for (int i = 0; (menu != null) && (i < menu.size()); i++) {
309 MenuItem menuItem = menu.getItem(i);
310 if (menuItem.isVisible() && menuItem.isEnabled()) {
311 Menu subMenu = menuItem.getSubMenu();
312 if (subMenu != null) {
313 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
314 } else {
315 menuItems.add(menuItem);
316 }
317 }
318 }
319 return menuItems;
320 }
321
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +0000322 private void registerOrientationHandler() {
323 unregisterOrientationHandler();
324 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
325 }
326
327 private void unregisterOrientationHandler() {
328 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
329 }
330
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000331
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100332 /**
333 * A popup window used by the floating toolbar.
334 *
335 * This class is responsible for the rendering/animation of the floating toolbar.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000336 * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
337 * to transition between panels.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100338 */
339 private static final class FloatingToolbarPopup {
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000340
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000341 /* Minimum and maximum number of items allowed in the overflow. */
342 private static final int MIN_OVERFLOW_SIZE = 2;
343 private static final int MAX_OVERFLOW_SIZE = 4;
344
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700345 private final Context mContext;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000346 private final View mParent; // Parent for the popup window.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100347 private final PopupWindow mPopupWindow;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000348
349 /* Margins between the popup window and it's content. */
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +0100350 private final int mMarginHorizontal;
351 private final int mMarginVertical;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000352
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000353 /* View components */
354 private final ViewGroup mContentContainer; // holds all contents.
355 private final ViewGroup mMainPanel; // holds menu items that are initially displayed.
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +0000356 private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000357 private final ImageButton mOverflowButton; // opens/closes the overflow.
358 /* overflow button drawables. */
359 private final Drawable mArrow;
360 private final Drawable mOverflow;
361 private final AnimatedVectorDrawable mToArrow;
362 private final AnimatedVectorDrawable mToOverflow;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100363
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000364 private final OverflowPanelViewHelper mOverflowPanelViewHelper;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100365
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000366 /* Animation interpolators. */
367 private final Interpolator mLogAccelerateInterpolator;
368 private final Interpolator mFastOutSlowInInterpolator;
369 private final Interpolator mLinearOutSlowInInterpolator;
370 private final Interpolator mFastOutLinearInInterpolator;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100371
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000372 /* Animations. */
373 private final AnimatorSet mShowAnimation;
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100374 private final AnimatorSet mDismissAnimation;
375 private final AnimatorSet mHideAnimation;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000376 private final AnimationSet mOpenOverflowAnimation;
377 private final AnimationSet mCloseOverflowAnimation;
378 private final Animation.AnimationListener mOverflowAnimationListener;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100379
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000380 private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in.
381 private final Point mCoordsOnWindow = new Point(); // popup window coordinates.
382 /* Temporary data holders. Reset values before using. */
Yohei Yukawa4b269972015-07-15 19:01:32 -0700383 private final int[] mTmpCoords = new int[2];
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100384
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100385 private final Region mTouchableRegion = new Region();
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100386 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +0100387 info -> {
388 info.contentInsets.setEmpty();
389 info.visibleInsets.setEmpty();
390 info.touchableRegion.set(mTouchableRegion);
391 info.setTouchableInsets(
392 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100393 };
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100394
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000395 private final int mLineHeight;
396 private final int mIconTextSpacing;
397
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000398 /**
399 * @see OverflowPanelViewHelper#preparePopupContent().
400 */
401 private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
402 @Override
403 public void run() {
404 setPanelsStatesAtRestingPosition();
405 setContentAreaAsTouchableSurface();
406 mContentContainer.setAlpha(1);
407 }
408 };
409
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100410 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
411 private boolean mHidden; // tracks whether this popup is hidden or hiding.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100412
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000413 /* Calculated sizes for panels and overflow button. */
414 private final Size mOverflowButtonSize;
415 private Size mOverflowPanelSize; // Should be null when there is no overflow.
416 private Size mMainPanelSize;
417
418 /* Item click listeners */
419 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
420 private final View.OnClickListener mMenuItemButtonOnClickListener =
421 new View.OnClickListener() {
422 @Override
423 public void onClick(View v) {
424 if (v.getTag() instanceof MenuItem) {
425 if (mOnMenuItemClickListener != null) {
426 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
427 }
428 }
429 }
430 };
431
432 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
433 private boolean mIsOverflowOpen;
434
435 private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100436
437 /**
438 * Initializes a new floating toolbar popup.
439 *
440 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
441 * from.
442 */
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700443 public FloatingToolbarPopup(Context context, View parent) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100444 mParent = Preconditions.checkNotNull(parent);
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -0700445 mContext = Preconditions.checkNotNull(context);
446 mContentContainer = createContentContainer(context);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100447 mPopupWindow = createPopupWindow(mContentContainer);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000448 mMarginHorizontal = parent.getResources()
449 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
450 mMarginVertical = parent.getResources()
451 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000452 mLineHeight = context.getResources()
453 .getDimensionPixelSize(R.dimen.floating_toolbar_height);
454 mIconTextSpacing = context.getResources()
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +0100455 .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000456
457 // Interpolators
458 mLogAccelerateInterpolator = new LogAccelerateInterpolator();
459 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
460 mContext, android.R.interpolator.fast_out_slow_in);
461 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
462 mContext, android.R.interpolator.linear_out_slow_in);
463 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
464 mContext, android.R.interpolator.fast_out_linear_in);
465
466 // Drawables. Needed for views.
467 mArrow = mContext.getResources()
468 .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
469 mArrow.setAutoMirrored(true);
470 mOverflow = mContext.getResources()
471 .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
472 mOverflow.setAutoMirrored(true);
473 mToArrow = (AnimatedVectorDrawable) mContext.getResources()
474 .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
475 mToArrow.setAutoMirrored(true);
476 mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
477 .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
478 mToOverflow.setAutoMirrored(true);
479
480 // Views
481 mOverflowButton = createOverflowButton();
482 mOverflowButtonSize = measure(mOverflowButton);
483 mMainPanel = createMainPanel();
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +0100484 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000485 mOverflowPanel = createOverflowPanel();
486
487 // Animation. Need views.
488 mOverflowAnimationListener = createOverflowAnimationListener();
489 mOpenOverflowAnimation = new AnimationSet(true);
490 mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
491 mCloseOverflowAnimation = new AnimationSet(true);
492 mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
493 mShowAnimation = createEnterAnimation(mContentContainer);
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100494 mDismissAnimation = createExitAnimation(
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100495 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100496 150, // startDelay
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100497 new AnimatorListenerAdapter() {
498 @Override
499 public void onAnimationEnd(Animator animation) {
500 mPopupWindow.dismiss();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100501 mContentContainer.removeAllViews();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100502 }
503 });
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +0100504 mHideAnimation = createExitAnimation(
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100505 mContentContainer,
Abodunrinwa Toki4ce050b2015-05-19 17:36:55 +0100506 0, // startDelay
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100507 new AnimatorListenerAdapter() {
508 @Override
509 public void onAnimationEnd(Animator animation) {
510 mPopupWindow.dismiss();
511 }
512 });
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100513 }
514
515 /**
516 * Lays out buttons for the specified menu items.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000517 * Requires a subsequent call to {@link #show()} to show the items.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100518 */
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100519 public void layoutMenuItems(
520 List<MenuItem> menuItems,
521 MenuItem.OnMenuItemClickListener menuItemClickListener,
522 int suggestedWidth) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000523 mOnMenuItemClickListener = menuItemClickListener;
524 cancelOverflowAnimations();
525 clearPanels();
526 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
527 if (!menuItems.isEmpty()) {
528 // Add remaining items to the overflow.
529 layoutOverflowPanelItems(menuItems);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100530 }
531 updatePopupSize();
532 }
533
534 /**
535 * Shows this popup at the specified coordinates.
536 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
537 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700538 public void show(Rect contentRectOnScreen) {
539 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100540
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100541 if (isShowing()) {
542 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000543 }
544
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100545 mHidden = false;
546 mDismissed = false;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100547 cancelDismissAndHideAnimations();
548 cancelOverflowAnimations();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100549
Yohei Yukawabafc9082015-07-14 05:59:05 -0700550 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100551 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700552 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700553 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000554 // specify the popup position in screen coordinates.
555 mPopupWindow.showAtLocation(
556 mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +0100557 setTouchableSurfaceInsetsComputer();
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100558 runShowAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100559 }
560
561 /**
562 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
563 */
564 public void dismiss() {
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100565 if (mDismissed) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100566 return;
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +0000567 }
568
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100569 mHidden = false;
570 mDismissed = true;
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100571 mHideAnimation.cancel();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000572
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100573 runDismissAnimation();
574 setZeroTouchableSurface();
575 }
576
577 /**
578 * Hides this popup. This is a no-op if this popup is not showing.
579 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
580 */
581 public void hide() {
582 if (!isShowing()) {
583 return;
584 }
585
586 mHidden = true;
587 runHideAnimation();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100588 setZeroTouchableSurface();
589 }
590
591 /**
592 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
593 */
594 public boolean isShowing() {
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100595 return !mDismissed && !mHidden;
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100596 }
597
598 /**
599 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
600 */
601 public boolean isHidden() {
602 return mHidden;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100603 }
604
605 /**
606 * Updates the coordinates of this popup.
607 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100608 * This is a no-op if this popup is not showing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100609 */
Yohei Yukawabafc9082015-07-14 05:59:05 -0700610 public void updateCoordinates(Rect contentRectOnScreen) {
611 Preconditions.checkNotNull(contentRectOnScreen);
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100612
Abodunrinwa Tokic23ac322015-04-25 00:28:56 +0100613 if (!isShowing() || !mPopupWindow.isShowing()) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100614 return;
615 }
616
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100617 cancelOverflowAnimations();
Yohei Yukawabafc9082015-07-14 05:59:05 -0700618 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100619 preparePopupContent();
Yohei Yukawa4b269972015-07-15 19:01:32 -0700620 // We need to specify the position in window coordinates.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700621 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000622 // specify the popup position in screen coordinates.
623 mPopupWindow.update(
624 mCoordsOnWindow.x, mCoordsOnWindow.y,
625 mPopupWindow.getWidth(), mPopupWindow.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100626 }
627
Yohei Yukawabafc9082015-07-14 05:59:05 -0700628 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100629 refreshViewPort();
630
Abodunrinwa Toki51a8af62016-04-28 19:59:57 +0100631 // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
632 // landscape.
633 final int x = Math.min(
634 contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
635 mViewPortOnScreen.right - mPopupWindow.getWidth());
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +0100636
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000637 final int y;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100638
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000639 final int availableHeightAboveContent =
640 contentRectOnScreen.top - mViewPortOnScreen.top;
641 final int availableHeightBelowContent =
642 mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100643
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000644 final int margin = 2 * mMarginVertical;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000645 final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000646
647 if (!hasOverflow()) {
648 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100649 // There is enough space at the top of the content.
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000650 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
651 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100652 // There is enough space at the bottom of the content.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700653 y = contentRectOnScreen.bottom;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +0000654 } else if (availableHeightBelowContent >= mLineHeight) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100655 // Just enough space to fit the toolbar with no vertical margins.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700656 y = contentRectOnScreen.bottom - mMarginVertical;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100657 } else {
658 // Not enough space. Prefer to position as high as possible.
659 y = Math.max(
Yohei Yukawabafc9082015-07-14 05:59:05 -0700660 mViewPortOnScreen.top,
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000661 contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100662 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000663 } else {
664 // Has an overflow.
665 final int minimumOverflowHeightWithMargin =
666 calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
667 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
668 contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
669 final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
670 mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100671
672 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100673 // There is enough space at the top of the content rect for the overflow.
674 // Position above and open upwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100675 updateOverflowHeight(availableHeightAboveContent - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000676 y = contentRectOnScreen.top - mPopupWindow.getHeight();
677 mOpenOverflowUpwards = true;
678 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100679 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100680 // There is enough space at the top of the content rect for the main panel
681 // but not the overflow.
682 // Position above but open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100683 updateOverflowHeight(availableHeightThroughContentDown - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000684 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
685 mOpenOverflowUpwards = false;
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100686 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100687 // There is enough space at the bottom of the content rect for the overflow.
688 // Position below and open downwards.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100689 updateOverflowHeight(availableHeightBelowContent - margin);
Yohei Yukawabafc9082015-07-14 05:59:05 -0700690 y = contentRectOnScreen.bottom;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000691 mOpenOverflowUpwards = false;
692 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
Yohei Yukawabafc9082015-07-14 05:59:05 -0700693 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100694 // There is enough space at the bottom of the content rect for the main panel
695 // but not the overflow.
696 // Position below but open upwards.
697 updateOverflowHeight(availableHeightThroughContentUp - margin);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000698 y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
699 mPopupWindow.getHeight();
700 mOpenOverflowUpwards = true;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100701 } else {
702 // Not enough space.
Abodunrinwa Toki9ae95df2015-06-19 03:04:50 +0100703 // Position at the top of the view port and open downwards.
Yohei Yukawabafc9082015-07-14 05:59:05 -0700704 updateOverflowHeight(mViewPortOnScreen.height() - margin);
705 y = mViewPortOnScreen.top;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000706 mOpenOverflowUpwards = false;
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100707 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100708 }
Abodunrinwa Toki6d2b75c2015-06-12 15:35:51 +0100709
Yohei Yukawa4b269972015-07-15 19:01:32 -0700710 // We later specify the location of PopupWindow relative to the attached window.
711 // The idea here is that 1) we can get the location of a View in both window coordinates
712 // and screen coordiantes, where the offset between them should be equal to the window
713 // origin, and 2) we can use an arbitrary for this calculation while calculating the
714 // location of the rootview is supposed to be least expensive.
715 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
716 // the following calculation.
717 mParent.getRootView().getLocationOnScreen(mTmpCoords);
718 int rootViewLeftOnScreen = mTmpCoords[0];
719 int rootViewTopOnScreen = mTmpCoords[1];
720 mParent.getRootView().getLocationInWindow(mTmpCoords);
721 int rootViewLeftOnWindow = mTmpCoords[0];
722 int rootViewTopOnWindow = mTmpCoords[1];
723 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
724 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
Abodunrinwa Toki51a8af62016-04-28 19:59:57 +0100725 mCoordsOnWindow.set(
726 Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100727 }
728
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100729 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100730 * Performs the "show" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100731 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100732 private void runShowAnimation() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000733 mShowAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100734 }
735
736 /**
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100737 * Performs the "dismiss" animation on the floating popup.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100738 */
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100739 private void runDismissAnimation() {
740 mDismissAnimation.start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100741 }
742
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100743 /**
744 * Performs the "hide" animation on the floating popup.
745 */
746 private void runHideAnimation() {
747 mHideAnimation.start();
748 }
749
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100750 private void cancelDismissAndHideAnimations() {
Abodunrinwa Toki7270d072015-04-17 20:31:34 +0100751 mDismissAnimation.cancel();
752 mHideAnimation.cancel();
Abodunrinwa Toki0ce3e082015-04-21 20:33:21 +0100753 }
754
755 private void cancelOverflowAnimations() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000756 mContentContainer.clearAnimation();
757 mMainPanel.animate().cancel();
758 mOverflowPanel.animate().cancel();
759 mToArrow.stop();
760 mToOverflow.stop();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100761 }
762
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100763 private void openOverflow() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000764 final int targetWidth = mOverflowPanelSize.getWidth();
765 final int targetHeight = mOverflowPanelSize.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100766 final int startWidth = mContentContainer.getWidth();
767 final int startHeight = mContentContainer.getHeight();
768 final float startY = mContentContainer.getY();
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100769 final float left = mContentContainer.getX();
770 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100771 Animation widthAnimation = new Animation() {
772 @Override
773 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100774 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000775 setWidth(mContentContainer, startWidth + deltaWidth);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100776 if (isInRTLMode()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100777 mContentContainer.setX(left);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000778
779 // Lock the panels in place.
780 mMainPanel.setX(0);
781 mOverflowPanel.setX(0);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100782 } else {
783 mContentContainer.setX(right - mContentContainer.getWidth());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000784
785 // Offset the panels' positions so they look like they're locked in place
786 // on the screen.
787 mMainPanel.setX(mContentContainer.getWidth() - startWidth);
788 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100789 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100790 }
791 };
792 Animation heightAnimation = new Animation() {
793 @Override
794 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100795 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000796 setHeight(mContentContainer, startHeight + deltaHeight);
797 if (mOpenOverflowUpwards) {
798 mContentContainer.setY(
799 startY - (mContentContainer.getHeight() - startHeight));
800 positionContentYCoordinatesIfOpeningOverflowUpwards();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100801 }
802 }
803 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000804 final float overflowButtonStartX = mOverflowButton.getX();
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100805 final float overflowButtonTargetX = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000806 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
807 overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
808 Animation overflowButtonAnimation = new Animation() {
809 @Override
810 protected void applyTransformation(float interpolatedTime, Transformation t) {
811 float overflowButtonX = overflowButtonStartX
812 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100813 float deltaContainerWidth = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000814 0 :
815 mContentContainer.getWidth() - startWidth;
816 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
817 mOverflowButton.setX(actualOverflowButtonX);
818 }
819 };
820 widthAnimation.setInterpolator(mLogAccelerateInterpolator);
821 widthAnimation.setDuration(getAdjustedDuration(250));
822 heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
823 heightAnimation.setDuration(getAdjustedDuration(250));
824 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
825 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100826 mOpenOverflowAnimation.getAnimations().clear();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000827 mOpenOverflowAnimation.getAnimations().clear();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100828 mOpenOverflowAnimation.addAnimation(widthAnimation);
829 mOpenOverflowAnimation.addAnimation(heightAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000830 mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100831 mContentContainer.startAnimation(mOpenOverflowAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000832 mIsOverflowOpen = true;
833 mMainPanel.animate()
834 .alpha(0).withLayer()
835 .setInterpolator(mLinearOutSlowInInterpolator)
836 .setDuration(250)
837 .start();
838 mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100839 }
840
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100841 private void closeOverflow() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000842 final int targetWidth = mMainPanelSize.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100843 final int startWidth = mContentContainer.getWidth();
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100844 final float left = mContentContainer.getX();
845 final float right = left + mContentContainer.getWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100846 Animation widthAnimation = new Animation() {
847 @Override
848 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100849 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000850 setWidth(mContentContainer, startWidth + deltaWidth);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100851 if (isInRTLMode()) {
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100852 mContentContainer.setX(left);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000853
854 // Lock the panels in place.
855 mMainPanel.setX(0);
856 mOverflowPanel.setX(0);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100857 } else {
858 mContentContainer.setX(right - mContentContainer.getWidth());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000859
860 // Offset the panels' positions so they look like they're locked in place
861 // on the screen.
862 mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
863 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
Abodunrinwa Toki6c5ac8e2015-06-01 17:35:34 +0100864 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100865 }
866 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000867 final int targetHeight = mMainPanelSize.getHeight();
868 final int startHeight = mContentContainer.getHeight();
869 final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100870 Animation heightAnimation = new Animation() {
871 @Override
872 protected void applyTransformation(float interpolatedTime, Transformation t) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100873 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000874 setHeight(mContentContainer, startHeight + deltaHeight);
875 if (mOpenOverflowUpwards) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100876 mContentContainer.setY(bottom - mContentContainer.getHeight());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000877 positionContentYCoordinatesIfOpeningOverflowUpwards();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100878 }
879 }
880 };
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000881 final float overflowButtonStartX = mOverflowButton.getX();
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100882 final float overflowButtonTargetX = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000883 overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
884 overflowButtonStartX + startWidth - mOverflowButton.getWidth();
885 Animation overflowButtonAnimation = new Animation() {
886 @Override
887 protected void applyTransformation(float interpolatedTime, Transformation t) {
888 float overflowButtonX = overflowButtonStartX
889 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100890 float deltaContainerWidth = isInRTLMode() ?
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000891 0 :
892 mContentContainer.getWidth() - startWidth;
893 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
894 mOverflowButton.setX(actualOverflowButtonX);
895 }
896 };
897 widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
898 widthAnimation.setDuration(getAdjustedDuration(250));
899 heightAnimation.setInterpolator(mLogAccelerateInterpolator);
900 heightAnimation.setDuration(getAdjustedDuration(250));
901 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
902 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100903 mCloseOverflowAnimation.getAnimations().clear();
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100904 mCloseOverflowAnimation.addAnimation(widthAnimation);
905 mCloseOverflowAnimation.addAnimation(heightAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000906 mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
Abodunrinwa Tokib9044372015-04-19 18:55:42 +0100907 mContentContainer.startAnimation(mCloseOverflowAnimation);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000908 mIsOverflowOpen = false;
909 mMainPanel.animate()
910 .alpha(1).withLayer()
911 .setInterpolator(mFastOutLinearInInterpolator)
912 .setDuration(100)
913 .start();
914 mOverflowPanel.animate()
915 .alpha(0).withLayer()
916 .setInterpolator(mLinearOutSlowInInterpolator)
917 .setDuration(150)
918 .start();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100919 }
920
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +0000921 /**
922 * Defines the position of the floating toolbar popup panels when transition animation has
923 * stopped.
924 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000925 private void setPanelsStatesAtRestingPosition() {
926 mOverflowButton.setEnabled(true);
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +0000927 mOverflowPanel.awakenScrollBars();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100928
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000929 if (mIsOverflowOpen) {
930 // Set open state.
931 final Size containerSize = mOverflowPanelSize;
932 setSize(mContentContainer, containerSize);
933 mMainPanel.setAlpha(0);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000934 mMainPanel.setVisibility(View.INVISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000935 mOverflowPanel.setAlpha(1);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000936 mOverflowPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000937 mOverflowButton.setImageDrawable(mArrow);
Phil Weaver1d4e1832016-05-16 09:41:14 -0700938 mOverflowButton.setContentDescription(mContext.getString(
939 R.string.floating_toolbar_close_overflow_description));
Abodunrinwa Toki517adad2015-04-07 22:13:51 +0100940
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000941 // Update x-coordinates depending on RTL state.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100942 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000943 mContentContainer.setX(mMarginHorizontal); // align left
944 mMainPanel.setX(0); // align left
945 mOverflowButton.setX( // align right
946 containerSize.getWidth() - mOverflowButtonSize.getWidth());
947 mOverflowPanel.setX(0); // align left
948 } else {
949 mContentContainer.setX( // align right
Abodunrinwa Toki0780f3a2016-05-03 16:25:41 +0100950 mPopupWindow.getWidth() -
951 containerSize.getWidth() - mMarginHorizontal);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000952 mMainPanel.setX(-mContentContainer.getX()); // align right
953 mOverflowButton.setX(0); // align left
954 mOverflowPanel.setX(0); // align left
955 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +0100956
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000957 // Update y-coordinates depending on overflow's open direction.
958 if (mOpenOverflowUpwards) {
959 mContentContainer.setY(mMarginVertical); // align top
960 mMainPanel.setY( // align bottom
961 containerSize.getHeight() - mContentContainer.getHeight());
962 mOverflowButton.setY( // align bottom
963 containerSize.getHeight() - mOverflowButtonSize.getHeight());
964 mOverflowPanel.setY(0); // align top
965 } else {
966 // opens downwards.
967 mContentContainer.setY(mMarginVertical); // align top
968 mMainPanel.setY(0); // align top
969 mOverflowButton.setY(0); // align top
970 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
971 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +0100972 } else {
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +0000973 // Overflow not open. Set closed state.
974 final Size containerSize = mMainPanelSize;
975 setSize(mContentContainer, containerSize);
976 mMainPanel.setAlpha(1);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000977 mMainPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +0000978 mOverflowPanel.setAlpha(0);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +0000979 mOverflowPanel.setVisibility(View.INVISIBLE);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +0000980 mOverflowButton.setImageDrawable(mOverflow);
Phil Weaver1d4e1832016-05-16 09:41:14 -0700981 mOverflowButton.setContentDescription(mContext.getString(
982 R.string.floating_toolbar_open_overflow_description));
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000983
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +0000984 if (hasOverflow()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000985 // Update x-coordinates depending on RTL state.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +0100986 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000987 mContentContainer.setX(mMarginHorizontal); // align left
988 mMainPanel.setX(0); // align left
989 mOverflowButton.setX(0); // align left
990 mOverflowPanel.setX(0); // align left
991 } else {
Abodunrinwa Toki0780f3a2016-05-03 16:25:41 +0100992 mContentContainer.setX( // align right
993 mPopupWindow.getWidth() -
994 containerSize.getWidth() - mMarginHorizontal);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +0000995 mMainPanel.setX(0); // align left
996 mOverflowButton.setX( // align right
997 containerSize.getWidth() - mOverflowButtonSize.getWidth());
998 mOverflowPanel.setX( // align right
999 containerSize.getWidth() - mOverflowPanelSize.getWidth());
1000 }
1001
1002 // Update y-coordinates depending on overflow's open direction.
1003 if (mOpenOverflowUpwards) {
1004 mContentContainer.setY( // align bottom
1005 mMarginVertical +
1006 mOverflowPanelSize.getHeight() - containerSize.getHeight());
1007 mMainPanel.setY(0); // align top
1008 mOverflowButton.setY(0); // align top
1009 mOverflowPanel.setY( // align bottom
1010 containerSize.getHeight() - mOverflowPanelSize.getHeight());
1011 } else {
1012 // opens downwards.
1013 mContentContainer.setY(mMarginVertical); // align top
1014 mMainPanel.setY(0); // align top
1015 mOverflowButton.setY(0); // align top
1016 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
1017 }
1018 } else {
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001019 // No overflow.
1020 mContentContainer.setX(mMarginHorizontal); // align left
1021 mContentContainer.setY(mMarginVertical); // align top
1022 mMainPanel.setX(0); // align left
1023 mMainPanel.setY(0); // align top
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001024 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001025 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001026 }
1027
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001028 private void updateOverflowHeight(int suggestedHeight) {
1029 if (hasOverflow()) {
1030 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001031 mLineHeight;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001032 final int newHeight = calculateOverflowHeight(maxItemSize);
1033 if (mOverflowPanelSize.getHeight() != newHeight) {
1034 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
1035 }
1036 setSize(mOverflowPanel, mOverflowPanelSize);
1037 if (mIsOverflowOpen) {
1038 setSize(mContentContainer, mOverflowPanelSize);
1039 if (mOpenOverflowUpwards) {
1040 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1041 mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1042 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1043 }
1044 } else {
1045 setSize(mContentContainer, mMainPanelSize);
1046 }
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001047 updatePopupSize();
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001048 }
1049 }
1050
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001051 private void updatePopupSize() {
1052 int width = 0;
1053 int height = 0;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001054 if (mMainPanelSize != null) {
1055 width = Math.max(width, mMainPanelSize.getWidth());
1056 height = Math.max(height, mMainPanelSize.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001057 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001058 if (mOverflowPanelSize != null) {
1059 width = Math.max(width, mOverflowPanelSize.getWidth());
1060 height = Math.max(height, mOverflowPanelSize.getHeight());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001061 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001062 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1063 mPopupWindow.setHeight(height + mMarginVertical * 2);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001064 maybeComputeTransitionDurationScale();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001065 }
1066
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001067 private void refreshViewPort() {
Yohei Yukawabafc9082015-07-14 05:59:05 -07001068 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001069 }
1070
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001071 private int getAdjustedToolbarWidth(int suggestedWidth) {
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001072 int width = suggestedWidth;
1073 refreshViewPort();
Yohei Yukawabafc9082015-07-14 05:59:05 -07001074 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
Abodunrinwa Tokie3eb1832015-05-27 20:31:01 +01001075 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1076 if (width <= 0) {
1077 width = mParent.getResources()
1078 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1079 }
1080 return Math.min(width, maximumWidth);
1081 }
1082
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001083 /**
1084 * Sets the touchable region of this popup to be zero. This means that all touch events on
1085 * this popup will go through to the surface behind it.
1086 */
1087 private void setZeroTouchableSurface() {
1088 mTouchableRegion.setEmpty();
1089 }
1090
1091 /**
1092 * Sets the touchable region of this popup to be the area occupied by its content.
1093 */
1094 private void setContentAreaAsTouchableSurface() {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001095 Preconditions.checkNotNull(mMainPanelSize);
1096 final int width;
1097 final int height;
1098 if (mIsOverflowOpen) {
1099 Preconditions.checkNotNull(mOverflowPanelSize);
1100 width = mOverflowPanelSize.getWidth();
1101 height = mOverflowPanelSize.getHeight();
1102 } else {
1103 width = mMainPanelSize.getWidth();
1104 height = mMainPanelSize.getHeight();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001105 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001106 mTouchableRegion.set(
1107 (int) mContentContainer.getX(),
1108 (int) mContentContainer.getY(),
1109 (int) mContentContainer.getX() + width,
1110 (int) mContentContainer.getY() + height);
1111 }
Abodunrinwa Tokid5358ff2015-04-22 07:08:29 +01001112
1113 /**
1114 * Make the touchable area of this popup be the area specified by mTouchableRegion.
1115 * This should be called after the popup window has been dismissed (dismiss/hide)
1116 * and is probably being re-shown with a new content root view.
1117 */
1118 private void setTouchableSurfaceInsetsComputer() {
1119 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1120 .getRootView()
1121 .getViewTreeObserver();
1122 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1123 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1124 }
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001125
Abodunrinwa Toki49482f82016-07-01 19:57:50 +01001126 private boolean isInRTLMode() {
1127 return mContext.getApplicationInfo().hasRtlSupport()
1128 && mContext.getResources().getConfiguration().getLayoutDirection()
1129 == View.LAYOUT_DIRECTION_RTL;
Abodunrinwa Toki6cb5cc12015-06-03 11:28:12 +01001130 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001131
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001132 private boolean hasOverflow() {
1133 return mOverflowPanelSize != null;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001134 }
1135
1136 /**
1137 * Fits as many menu items in the main panel and returns a list of the menu items that
1138 * were not fit in.
1139 *
1140 * @return The menu items that are not included in this main panel.
1141 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001142 public List<MenuItem> layoutMainPanelItems(
1143 List<MenuItem> menuItems, final int toolbarWidth) {
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001144 Preconditions.checkNotNull(menuItems);
1145
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001146 int availableWidth = toolbarWidth;
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001147
1148 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
1149 // add the overflow menu items to the end of the remainingMenuItems list.
1150 final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
1151 for (MenuItem menuItem : menuItems) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001152 if (menuItem.getItemId() != android.R.id.textAssist
1153 && menuItem.requiresOverflow()) {
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001154 overflowMenuItems.add(menuItem);
1155 } else {
1156 remainingMenuItems.add(menuItem);
1157 }
1158 }
1159 remainingMenuItems.addAll(overflowMenuItems);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001160
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001161 mMainPanel.removeAllViews();
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001162 mMainPanel.setPaddingRelative(0, 0, 0, 0);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001163
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001164 int lastGroupId = -1;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001165 boolean isFirstItem = true;
1166 while (!remainingMenuItems.isEmpty()) {
1167 final MenuItem menuItem = remainingMenuItems.peek();
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001168
1169 // if this is the first item, regardless of requiresOverflow(), it should be
1170 // displayed on the main panel. Otherwise all items including this one will be
1171 // overflow items, and should be displayed in overflow panel.
1172 if(!isFirstItem && menuItem.requiresOverflow()) {
1173 break;
1174 }
1175
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001176 final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
1177 final View menuItemButton = createMenuItemButton(
1178 mContext, menuItem, mIconTextSpacing, showIcon);
Mihai Popa66d41f82018-04-17 16:21:10 +01001179 if (!showIcon && menuItemButton instanceof LinearLayout) {
1180 ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
1181 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001182
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001183 // Adding additional start padding for the first button to even out button spacing.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001184 if (isFirstItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001185 menuItemButton.setPaddingRelative(
1186 (int) (1.5 * menuItemButton.getPaddingStart()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001187 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001188 menuItemButton.getPaddingEnd(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001189 menuItemButton.getPaddingBottom());
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001190 }
1191
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001192 // Adding additional end padding for the last button to even out button spacing.
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001193 boolean isLastItem = remainingMenuItems.size() == 1;
1194 if (isLastItem) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001195 menuItemButton.setPaddingRelative(
1196 menuItemButton.getPaddingStart(),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001197 menuItemButton.getPaddingTop(),
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001198 (int) (1.5 * menuItemButton.getPaddingEnd()),
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001199 menuItemButton.getPaddingBottom());
1200 }
1201
1202 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001203 final int menuItemButtonWidth = Math.min(
1204 menuItemButton.getMeasuredWidth(), toolbarWidth);
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001205
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001206 // Check if we can fit an item while reserving space for the overflowButton.
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001207 final boolean canFitWithOverflow =
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001208 menuItemButtonWidth <=
Mihai Popa66d41f82018-04-17 16:21:10 +01001209 availableWidth - mOverflowButtonSize.getWidth();
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001210 final boolean canFitNoOverflow =
Mihai Popa66d41f82018-04-17 16:21:10 +01001211 isLastItem && menuItemButtonWidth <= availableWidth;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001212 if (canFitWithOverflow || canFitNoOverflow) {
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001213 setButtonTagAndClickListener(menuItemButton, menuItem);
Vladislav Kaznacheev2a00f982017-02-23 17:33:51 -08001214 // Set tooltips for main panel items, but not overflow items (b/35726766).
1215 menuItemButton.setTooltipText(menuItem.getTooltipText());
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001216 mMainPanel.addView(menuItemButton);
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001217 final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
Mihai Popa66d41f82018-04-17 16:21:10 +01001218 params.width = menuItemButtonWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001219 menuItemButton.setLayoutParams(params);
Mihai Popa66d41f82018-04-17 16:21:10 +01001220 availableWidth -= menuItemButtonWidth;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001221 remainingMenuItems.pop();
1222 } else {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001223 break;
1224 }
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +00001225 lastGroupId = menuItem.getGroupId();
1226 isFirstItem = false;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001227 }
Siyamed Sinir484c2e22017-06-07 16:26:19 -07001228
1229 if (!remainingMenuItems.isEmpty()) {
1230 // Reserve space for overflowButton.
1231 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1232 }
1233
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001234 mMainPanelSize = measure(mMainPanel);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001235 return remainingMenuItems;
1236 }
1237
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001238 private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1239 ArrayAdapter<MenuItem> overflowPanelAdapter =
1240 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1241 overflowPanelAdapter.clear();
1242 final int size = menuItems.size();
1243 for (int i = 0; i < size; i++) {
1244 overflowPanelAdapter.add(menuItems.get(i));
1245 }
1246 mOverflowPanel.setAdapter(overflowPanelAdapter);
1247 if (mOpenOverflowUpwards) {
1248 mOverflowPanel.setY(0);
1249 } else {
1250 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1251 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001252
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001253 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1254 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1255 mOverflowPanelSize = new Size(width, height);
1256 setSize(mOverflowPanel, mOverflowPanelSize);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001257 }
1258
1259 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001260 * Resets the content container and appropriately position it's panels.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001261 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001262 private void preparePopupContent() {
1263 mContentContainer.removeAllViews();
1264
1265 // Add views in the specified order so they stack up as expected.
1266 // Order: overflowPanel, mainPanel, overflowButton.
1267 if (hasOverflow()) {
1268 mContentContainer.addView(mOverflowPanel);
1269 }
1270 mContentContainer.addView(mMainPanel);
1271 if (hasOverflow()) {
1272 mContentContainer.addView(mOverflowButton);
1273 }
1274 setPanelsStatesAtRestingPosition();
1275 setContentAreaAsTouchableSurface();
1276
1277 // The positioning of contents in RTL is wrong when the view is first rendered.
1278 // Hide the view and post a runnable to recalculate positions and render the view.
1279 // TODO: Investigate why this happens and fix.
Abodunrinwa Toki49482f82016-07-01 19:57:50 +01001280 if (isInRTLMode()) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001281 mContentContainer.setAlpha(0);
1282 mContentContainer.post(mPreparePopupContentRTLHelper);
1283 }
1284 }
1285
1286 /**
1287 * Clears out the panels and their container. Resets their calculated sizes.
1288 */
1289 private void clearPanels() {
1290 mOverflowPanelSize = null;
1291 mMainPanelSize = null;
1292 mIsOverflowOpen = false;
1293 mMainPanel.removeAllViews();
1294 ArrayAdapter<MenuItem> overflowPanelAdapter =
1295 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1296 overflowPanelAdapter.clear();
1297 mOverflowPanel.setAdapter(overflowPanelAdapter);
1298 mContentContainer.removeAllViews();
1299 }
1300
1301 private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1302 if (mOpenOverflowUpwards) {
1303 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1304 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1305 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1306 }
1307 }
1308
1309 private int getOverflowWidth() {
1310 int overflowWidth = 0;
1311 final int count = mOverflowPanel.getAdapter().getCount();
1312 for (int i = 0; i < count; i++) {
1313 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1314 overflowWidth =
1315 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1316 }
1317 return overflowWidth;
1318 }
1319
1320 private int calculateOverflowHeight(int maxItemSize) {
1321 // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1322 int actualSize = Math.min(
1323 MAX_OVERFLOW_SIZE,
1324 Math.min(
1325 Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1326 mOverflowPanel.getCount()));
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001327 int extension = 0;
1328 if (actualSize < mOverflowPanel.getCount()) {
1329 // The overflow will require scrolling to get to all the items.
1330 // Extend the height so that part of the hidden items is displayed.
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001331 extension = (int) (mLineHeight * 0.5f);
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001332 }
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001333 return actualSize * mLineHeight
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001334 + mOverflowButtonSize.getHeight()
1335 + extension;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001336 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001337
1338 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001339 menuItemButton.setTag(menuItem);
1340 menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001341 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001342
1343 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001344 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1345 * animations. See comment about this in the code.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001346 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001347 private int getAdjustedDuration(int originalDuration) {
1348 if (mTransitionDurationScale < 150) {
1349 // For smaller transition, decrease the time.
1350 return Math.max(originalDuration - 50, 0);
1351 } else if (mTransitionDurationScale > 300) {
1352 // For bigger transition, increase the time.
1353 return originalDuration + 50;
1354 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001355
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001356 // Scale the animation duration with getDurationScale(). This allows
1357 // android.view.animation.* animations to scale just like android.animation.* animations
1358 // when animator duration scale is adjusted in "Developer Options".
1359 // For this reason, do not use this method for android.animation.* animations.
1360 return (int) (originalDuration * ValueAnimator.getDurationScale());
1361 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001362
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001363 private void maybeComputeTransitionDurationScale() {
Abodunrinwa Tokid1cd7fd2016-01-27 22:27:31 +00001364 if (mMainPanelSize != null && mOverflowPanelSize != null) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001365 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1366 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1367 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1368 mContentContainer.getContext().getResources().getDisplayMetrics().density);
1369 }
1370 }
1371
1372 private ViewGroup createMainPanel() {
1373 ViewGroup mainPanel = new LinearLayout(mContext) {
1374 @Override
1375 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1376 if (isOverflowAnimating()) {
1377 // Update widthMeasureSpec to make sure that this view is not clipped
1378 // as we offset it's coordinates with respect to it's parent.
1379 widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1380 mMainPanelSize.getWidth(),
1381 MeasureSpec.EXACTLY);
1382 }
1383 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1384 }
1385
1386 @Override
1387 public boolean onInterceptTouchEvent(MotionEvent ev) {
1388 // Intercept the touch event while the overflow is animating.
1389 return isOverflowAnimating();
1390 }
1391 };
1392 return mainPanel;
1393 }
1394
1395 private ImageButton createOverflowButton() {
1396 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1397 .inflate(R.layout.floating_popup_overflow_button, null);
1398 overflowButton.setImageDrawable(mOverflow);
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001399 overflowButton.setOnClickListener(v -> {
1400 if (mIsOverflowOpen) {
1401 overflowButton.setImageDrawable(mToOverflow);
1402 mToOverflow.start();
1403 closeOverflow();
1404 } else {
1405 overflowButton.setImageDrawable(mToArrow);
1406 mToArrow.start();
1407 openOverflow();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001408 }
1409 });
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001410 return overflowButton;
1411 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001412
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001413 private OverflowPanel createOverflowPanel() {
1414 final OverflowPanel overflowPanel = new OverflowPanel(this);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001415 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1416 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1417 overflowPanel.setDivider(null);
1418 overflowPanel.setDividerHeight(0);
1419
1420 final ArrayAdapter adapter =
1421 new ArrayAdapter<MenuItem>(mContext, 0) {
1422 @Override
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001423 public View getView(int position, View convertView, ViewGroup parent) {
1424 return mOverflowPanelViewHelper.getView(
1425 getItem(position), mOverflowPanelSize.getWidth(), convertView);
1426 }
1427 };
1428 overflowPanel.setAdapter(adapter);
1429
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001430 overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
1431 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1432 if (mOnMenuItemClickListener != null) {
1433 mOnMenuItemClickListener.onMenuItemClick(menuItem);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001434 }
1435 });
1436
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001437 return overflowPanel;
1438 }
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001439
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001440 private boolean isOverflowAnimating() {
1441 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1442 && !mOpenOverflowAnimation.hasEnded();
1443 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1444 && !mCloseOverflowAnimation.hasEnded();
1445 return overflowOpening || overflowClosing;
1446 }
1447
1448 private Animation.AnimationListener createOverflowAnimationListener() {
1449 Animation.AnimationListener listener = new Animation.AnimationListener() {
1450 @Override
1451 public void onAnimationStart(Animation animation) {
1452 // Disable the overflow button while it's animating.
1453 // It will be re-enabled when the animation stops.
1454 mOverflowButton.setEnabled(false);
Abodunrinwa Toki0cf64892016-03-17 15:29:36 +00001455 // Ensure both panels have visibility turned on when the overflow animation
1456 // starts.
1457 mMainPanel.setVisibility(View.VISIBLE);
1458 mOverflowPanel.setVisibility(View.VISIBLE);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001459 }
1460
1461 @Override
1462 public void onAnimationEnd(Animation animation) {
1463 // Posting this because it seems like this is called before the animation
1464 // actually ends.
Abodunrinwa Toki46850fc2017-05-22 15:20:18 +01001465 mContentContainer.post(() -> {
1466 setPanelsStatesAtRestingPosition();
1467 setContentAreaAsTouchableSurface();
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001468 });
1469 }
1470
1471 @Override
1472 public void onAnimationRepeat(Animation animation) {
1473 }
1474 };
1475 return listener;
1476 }
1477
1478 private static Size measure(View view) {
1479 Preconditions.checkState(view.getParent() == null);
1480 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1481 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1482 }
1483
1484 private static void setSize(View view, int width, int height) {
1485 view.setMinimumWidth(width);
1486 view.setMinimumHeight(height);
1487 ViewGroup.LayoutParams params = view.getLayoutParams();
1488 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1489 params.width = width;
1490 params.height = height;
1491 view.setLayoutParams(params);
1492 }
1493
1494 private static void setSize(View view, Size size) {
1495 setSize(view, size.getWidth(), size.getHeight());
1496 }
1497
1498 private static void setWidth(View view, int width) {
1499 ViewGroup.LayoutParams params = view.getLayoutParams();
1500 setSize(view, width, params.height);
1501 }
1502
1503 private static void setHeight(View view, int height) {
1504 ViewGroup.LayoutParams params = view.getLayoutParams();
1505 setSize(view, params.width, height);
1506 }
1507
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001508 /**
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001509 * A custom ListView for the overflow panel.
1510 */
1511 private static final class OverflowPanel extends ListView {
1512
1513 private final FloatingToolbarPopup mPopup;
1514
1515 OverflowPanel(FloatingToolbarPopup popup) {
1516 super(Preconditions.checkNotNull(popup).mContext);
1517 this.mPopup = popup;
Abodunrinwa Tokif0f4d732016-03-23 15:56:43 +00001518 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
Abodunrinwa Toki8949faf2016-04-06 14:15:26 +01001519 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
Abodunrinwa Tokicc9d6172016-02-03 18:32:42 +00001520 }
1521
1522 @Override
1523 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1524 // Update heightMeasureSpec to make sure that this view is not clipped
1525 // as we offset it's coordinates with respect to it's parent.
1526 int height = mPopup.mOverflowPanelSize.getHeight()
1527 - mPopup.mOverflowButtonSize.getHeight();
1528 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1529 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1530 }
1531
1532 @Override
1533 public boolean dispatchTouchEvent(MotionEvent ev) {
1534 if (mPopup.isOverflowAnimating()) {
1535 // Eat the touch event.
1536 return true;
1537 }
1538 return super.dispatchTouchEvent(ev);
1539 }
1540
1541 @Override
1542 protected boolean awakenScrollBars() {
1543 return super.awakenScrollBars();
1544 }
1545 }
1546
1547 /**
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001548 * A custom interpolator used for various floating toolbar animations.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001549 */
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001550 private static final class LogAccelerateInterpolator implements Interpolator {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001551
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001552 private static final int BASE = 100;
1553 private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001554
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001555 private static float computeLog(float t, int base) {
1556 return (float) (1 - Math.pow(base, -t));
Abodunrinwa Tokiffebf682015-05-12 21:54:08 +01001557 }
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001558
1559 @Override
1560 public float getInterpolation(float t) {
1561 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1562 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001563 }
1564
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001565 /**
1566 * A helper for generating views for the overflow panel.
1567 */
1568 private static final class OverflowPanelViewHelper {
1569
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001570 private final View mCalculator;
1571 private final int mIconTextSpacing;
1572 private final int mSidePadding;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001573
1574 private final Context mContext;
1575
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +01001576 public OverflowPanelViewHelper(Context context, int iconTextSpacing) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001577 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki49ec5422018-04-25 14:07:17 +01001578 mIconTextSpacing = iconTextSpacing;
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001579 mSidePadding = context.getResources()
1580 .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
1581 mCalculator = createMenuButton(null);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001582 }
1583
1584 public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
Abodunrinwa Tokif8e14fd2015-04-14 18:59:40 +01001585 Preconditions.checkNotNull(menuItem);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001586 if (convertView != null) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001587 updateMenuItemButton(
1588 convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001589 } else {
1590 convertView = createMenuButton(menuItem);
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001591 }
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001592 convertView.setMinimumWidth(minimumWidth);
1593 return convertView;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001594 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001595
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001596 public int calculateWidth(MenuItem menuItem) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001597 updateMenuItemButton(
1598 mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001599 mCalculator.measure(
1600 View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1601 return mCalculator.getMeasuredWidth();
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001602 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001603
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001604 private View createMenuButton(MenuItem menuItem) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001605 View button = createMenuItemButton(
1606 mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001607 button.setPadding(mSidePadding, 0, mSidePadding, 0);
1608 return button;
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001609 }
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001610
1611 private boolean shouldShowIcon(MenuItem menuItem) {
1612 if (menuItem != null) {
1613 return menuItem.getGroupId() == android.R.id.textAssist;
1614 }
1615 return false;
1616 }
Abodunrinwa Tokib9044372015-04-19 18:55:42 +01001617 }
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001618 }
1619
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001620 /**
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001621 * Creates and returns a menu button for the specified menu item.
1622 */
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001623 private static View createMenuItemButton(
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001624 Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001625 final View menuItemButton = LayoutInflater.from(context)
1626 .inflate(R.layout.floating_popup_menu_button, null);
1627 if (menuItem != null) {
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001628 updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001629 }
1630 return menuItemButton;
1631 }
1632
1633 /**
1634 * Updates the specified menu item button with the specified menu item data.
1635 */
1636 private static void updateMenuItemButton(
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001637 View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
1638 final TextView buttonText = menuItemButton.findViewById(
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001639 R.id.floating_toolbar_menu_item_text);
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001640 buttonText.setEllipsize(null);
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001641 if (TextUtils.isEmpty(menuItem.getTitle())) {
1642 buttonText.setVisibility(View.GONE);
1643 } else {
1644 buttonText.setVisibility(View.VISIBLE);
1645 buttonText.setText(menuItem.getTitle());
1646 }
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01001647 final ImageView buttonIcon = menuItemButton.findViewById(
1648 R.id.floating_toolbar_menu_item_image);
1649 if (menuItem.getIcon() == null || !showIcon) {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001650 buttonIcon.setVisibility(View.GONE);
1651 if (buttonText != null) {
1652 buttonText.setPaddingRelative(0, 0, 0, 0);
Vladislav Kaznacheev7039cbc2017-01-04 10:15:31 -08001653 }
1654 } else {
Abodunrinwa Toki852e9ac2017-02-22 00:20:15 +00001655 buttonIcon.setVisibility(View.VISIBLE);
1656 buttonIcon.setImageDrawable(menuItem.getIcon());
1657 if (buttonText != null) {
1658 buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
1659 }
Abodunrinwa Tokib21562c2015-05-20 22:25:16 +01001660 }
Vladislav Kaznacheev7039cbc2017-01-04 10:15:31 -08001661 final CharSequence contentDescription = menuItem.getContentDescription();
1662 if (TextUtils.isEmpty(contentDescription)) {
1663 menuItemButton.setContentDescription(menuItem.getTitle());
1664 } else {
1665 menuItemButton.setContentDescription(contentDescription);
1666 }
Abodunrinwa Toki0c7ed282015-03-27 15:02:03 +00001667 }
1668
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001669 private static ViewGroup createContentContainer(Context context) {
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001670 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001671 .inflate(R.layout.floating_popup_container, null);
Abodunrinwa Toki6be228c2016-01-27 22:11:30 +00001672 contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1673 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001674 contentContainer.setTag(FLOATING_TOOLBAR_TAG);
Mihai Popa17f91d52018-04-10 16:10:16 +01001675 contentContainer.setClipToOutline(true);
Abodunrinwa Toki8a5e1ae2015-09-28 21:59:04 +01001676 return contentContainer;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001677 }
1678
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001679 private static PopupWindow createPopupWindow(ViewGroup content) {
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001680 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1681 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
Yohei Yukawabafc9082015-07-14 05:59:05 -07001682 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1683 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1684 popupWindow.setClippingEnabled(false);
Abodunrinwa Toki103d48e2015-04-16 17:27:48 +01001685 popupWindow.setWindowLayoutType(
1686 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001687 popupWindow.setAnimationStyle(0);
1688 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1689 content.setLayoutParams(new ViewGroup.LayoutParams(
1690 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1691 popupContentHolder.addView(content);
1692 return popupWindow;
1693 }
1694
1695 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001696 * Creates an "appear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001697 *
1698 * @param view The view to animate
1699 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001700 private static AnimatorSet createEnterAnimation(View view) {
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001701 AnimatorSet animation = new AnimatorSet();
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001702 animation.playTogether(
Abodunrinwa Toki7e29d1b2015-10-30 16:41:45 +00001703 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001704 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001705 }
1706
1707 /**
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001708 * Creates a "disappear" animation for the specified view.
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001709 *
1710 * @param view The view to animate
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001711 * @param startDelay The start delay of the animation
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001712 * @param listener The animation listener
1713 */
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001714 private static AnimatorSet createExitAnimation(
Abodunrinwa Tokicdffaa42015-05-06 18:35:41 +01001715 View view, int startDelay, Animator.AnimatorListener listener) {
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001716 AnimatorSet animation = new AnimatorSet();
1717 animation.playTogether(
Abodunrinwa Tokib9acbe42015-09-16 19:47:38 +01001718 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
Abodunrinwa Toki7aa6d0a2015-06-03 21:33:15 +01001719 animation.setStartDelay(startDelay);
1720 animation.addListener(listener);
1721 return animation;
Abodunrinwa Toki517adad2015-04-07 22:13:51 +01001722 }
1723
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -07001724 /**
1725 * Returns a re-themed context with controlled look and feel for views.
1726 */
1727 private static Context applyDefaultTheme(Context originalContext) {
1728 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1729 boolean isLightTheme = a.getBoolean(0, true);
Mihai Popafb4b6b82018-03-01 16:08:14 +00001730 int themeId
1731 = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
Abodunrinwa Toki079f33b2015-06-23 20:36:52 -07001732 a.recycle();
1733 return new ContextThemeWrapper(originalContext, themeId);
1734 }
Abodunrinwa Toki30f8ef42016-02-01 13:54:10 +00001735}