• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package com.android.systemui.accessibility.floatingmenu;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.animation.ValueAnimator;
22 import android.graphics.PointF;
23 import android.graphics.Rect;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.util.Log;
27 import android.view.DisplayCutout;
28 import android.view.View;
29 import android.view.animation.Animation;
30 import android.view.animation.OvershootInterpolator;
31 import android.view.animation.TranslateAnimation;
32 
33 import androidx.dynamicanimation.animation.DynamicAnimation;
34 import androidx.dynamicanimation.animation.FlingAnimation;
35 import androidx.dynamicanimation.animation.FloatPropertyCompat;
36 import androidx.dynamicanimation.animation.SpringAnimation;
37 import androidx.dynamicanimation.animation.SpringForce;
38 import androidx.recyclerview.widget.RecyclerView;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 
42 import java.util.HashMap;
43 
44 /**
45  * Controls the interaction animations of the {@link MenuView}. Also, it will use the relative
46  * coordinate based on the {@link MenuViewLayer} to compute the offset of the {@link MenuView}.
47  */
48 class MenuAnimationController {
49     private static final String TAG = "MenuAnimationController";
50     private static final boolean DEBUG = false;
51     private static final float MIN_PERCENT = 0.0f;
52     private static final float MAX_PERCENT = 1.0f;
53     private static final float COMPLETELY_OPAQUE = 1.0f;
54     private static final float COMPLETELY_TRANSPARENT = 0.0f;
55     private static final float SCALE_SHRINK = 0.0f;
56     private static final float SCALE_GROW = 1.0f;
57     private static final float FLING_FRICTION_SCALAR = 1.9f;
58     private static final float DEFAULT_FRICTION = 4.2f;
59     private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
60     private static final float SPRING_STIFFNESS = 700f;
61     private static final float ESCAPE_VELOCITY = 750f;
62     // Make tucked animation by using translation X relative to the view itself.
63     private static final float ANIMATION_TO_X_VALUE = 0.5f;
64 
65     private static final int ANIMATION_START_OFFSET_MS = 600;
66     private static final int ANIMATION_DURATION_MS = 600;
67     private static final int FADE_OUT_DURATION_MS = 1000;
68     private static final int FADE_EFFECT_DURATION_MS = 3000;
69 
70     private final MenuView mMenuView;
71     private final MenuViewAppearance mMenuViewAppearance;
72     private final ValueAnimator mFadeOutAnimator;
73     private final Handler mHandler;
74     private boolean mIsFadeEffectEnabled;
75     private Runnable mSpringAnimationsEndAction;
76     private PointF mAnimationEndPosition = new PointF();
77 
78     // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
79     // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
80     @VisibleForTesting
81     final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
82             new HashMap<>();
83 
84     @VisibleForTesting
85     final RadiiAnimator mRadiiAnimator;
86 
MenuAnimationController(MenuView menuView, MenuViewAppearance menuViewAppearance)87     MenuAnimationController(MenuView menuView, MenuViewAppearance menuViewAppearance) {
88         mMenuView = menuView;
89         mMenuViewAppearance = menuViewAppearance;
90 
91         mHandler = createUiHandler();
92         mFadeOutAnimator = new ValueAnimator();
93         mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
94         mFadeOutAnimator.addUpdateListener(
95                 (animation) -> menuView.setAlpha((float) animation.getAnimatedValue()));
96         mRadiiAnimator = new RadiiAnimator(mMenuViewAppearance.getMenuRadii(),
97                 new IRadiiAnimationListener() {
98                     @Override
99                     public void onRadiiAnimationUpdate(float[] radii) {
100                         mMenuView.setRadii(radii);
101                     }
102 
103                     @Override
104                     public void onRadiiAnimationStart() {}
105 
106                     @Override
107                     public void onRadiiAnimationStop() {}
108                 });
109         mAnimationEndPosition = mMenuView.getMenuPosition();
110     }
111 
moveToPosition(PointF position)112     void moveToPosition(PointF position) {
113         moveToPosition(position, /* animateMovement = */ false);
114         mAnimationEndPosition = position;
115     }
116 
117     /* Moves position without updating underlying percentage position. Can be animated. */
moveToPosition(PointF position, boolean animateMovement)118     void moveToPosition(PointF position, boolean animateMovement) {
119         moveToPositionX(position.x, animateMovement);
120         moveToPositionY(position.y, animateMovement);
121     }
122 
moveToPositionX(float positionX)123     void moveToPositionX(float positionX) {
124         moveToPositionX(positionX, /* animateMovement = */ false);
125     }
126 
moveToPositionX(float positionX, boolean animateMovement)127     void moveToPositionX(float positionX, boolean animateMovement) {
128         if (animateMovement) {
129             springMenuWith(DynamicAnimation.TRANSLATION_X,
130                     createSpringForce(),
131                     /* velocity = */ 0,
132                     positionX, /* writeToPosition = */ false);
133         } else {
134             DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
135         }
136         mAnimationEndPosition.x = positionX;
137     }
138 
moveToPositionY(float positionY)139     void moveToPositionY(float positionY) {
140         moveToPositionY(positionY, /* animateMovement = */ false);
141     }
142 
moveToPositionY(float positionY, boolean animateMovement)143     void moveToPositionY(float positionY, boolean animateMovement) {
144         if (animateMovement) {
145             springMenuWith(DynamicAnimation.TRANSLATION_Y,
146                     createSpringForce(),
147                     /* velocity = */ 0,
148                     positionY, /* writeToPosition = */ false);
149         } else {
150             DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
151         }
152         mAnimationEndPosition.y = positionY;
153     }
154 
moveToPositionYIfNeeded(float positionY)155     void moveToPositionYIfNeeded(float positionY) {
156         // If the list view was out of screen bounds, it would allow users to nest scroll inside
157         // and avoid conflicting with outer scroll.
158         final RecyclerView listView = (RecyclerView) mMenuView.getChildAt(/* index= */ 0);
159         if (listView.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
160             moveToPositionY(positionY);
161         }
162     }
163 
164     /**
165      * Sets the action to be called when the all dynamic animations are completed.
166      */
setSpringAnimationsEndAction(Runnable runnable)167     void setSpringAnimationsEndAction(Runnable runnable) {
168         mSpringAnimationsEndAction = runnable;
169     }
170 
moveToTopLeftPosition()171     void moveToTopLeftPosition() {
172         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
173         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
174         moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top));
175     }
176 
moveToTopRightPosition()177     void moveToTopRightPosition() {
178         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
179         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
180         moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top));
181     }
182 
moveToBottomLeftPosition()183     void moveToBottomLeftPosition() {
184         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
185         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
186         moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom));
187     }
188 
moveToBottomRightPosition()189     void moveToBottomRightPosition() {
190         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
191         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
192         moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom));
193     }
194 
moveAndPersistPosition(PointF position)195     void moveAndPersistPosition(PointF position) {
196         moveToPosition(position);
197         mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
198         constrainPositionAndUpdate(position, /* writeToPosition = */ true);
199     }
200 
flingMenuThenSpringToEdge(PointF position, float velocityX, float velocityY)201     void flingMenuThenSpringToEdge(PointF position, float velocityX, float velocityY) {
202         final boolean shouldMenuFlingLeft = isOnLeftSide()
203                 ? velocityX < ESCAPE_VELOCITY
204                 : velocityX < -ESCAPE_VELOCITY;
205 
206         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
207         final float finalPositionX = shouldMenuFlingLeft
208                 ? draggableBounds.left : draggableBounds.right;
209         final DisplayCutout displayCutout = mMenuViewAppearance.getDisplayCutout();
210         final float finalPositionY =
211                 (displayCutout == null) ? position.y
212                         : mMenuViewAppearance.avoidVerticalDisplayCutout(
213                                 position.y, draggableBounds,
214                                 shouldMenuFlingLeft
215                                         ? displayCutout.getBoundingRectLeft()
216                                         : displayCutout.getBoundingRectRight()
217                         );
218         final float minimumVelocityToReachEdge =
219                 (finalPositionX - position.x) * (FLING_FRICTION_SCALAR * DEFAULT_FRICTION);
220 
221         final float startXVelocity = shouldMenuFlingLeft
222                 ? Math.min(minimumVelocityToReachEdge, velocityX)
223                 : Math.max(minimumVelocityToReachEdge, velocityX);
224 
225         flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
226                 startXVelocity,
227                 FLING_FRICTION_SCALAR,
228                 createSpringForce(),
229                 finalPositionX);
230 
231         if (com.android.systemui.Flags.floatingMenuDisplayCutoutSupport()) {
232             flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
233                     velocityY,
234                     FLING_FRICTION_SCALAR,
235                     createSpringForce(),
236                     (finalPositionY != position.y) ? finalPositionY : null);
237         } else {
238             flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
239                     velocityY,
240                     FLING_FRICTION_SCALAR,
241                     createSpringForce(),
242                     /* finalPosition= */ null);
243         }
244     }
245 
246     private void flingThenSpringMenuWith(DynamicAnimation.ViewProperty property, float velocity,
247             float friction, SpringForce spring, Float finalPosition) {
248 
249         final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
250         final float currentValue = menuPositionProperty.getValue(mMenuView);
251         final Rect bounds = mMenuView.getMenuDraggableBounds();
252         final float min =
253                 property.equals(DynamicAnimation.TRANSLATION_X)
254                         ? bounds.left
255                         : bounds.top;
256         final float max =
257                 property.equals(DynamicAnimation.TRANSLATION_X)
258                         ? bounds.right
259                         : bounds.bottom;
260 
261         final FlingAnimation flingAnimation = createFlingAnimation(mMenuView, menuPositionProperty);
262         flingAnimation.setFriction(friction)
263                 .setStartVelocity(velocity)
264                 .setMinValue(Math.min(currentValue, min))
265                 .setMaxValue(Math.max(currentValue, max))
266                 .addEndListener((animation, canceled, endValue, endVelocity) -> {
267                     if (canceled) {
268                         if (DEBUG) {
269                             Log.d(TAG, "The fling animation was canceled.");
270                         }
271 
272                         return;
273                     }
274 
275                     final float endPosition = finalPosition != null
276                             ? finalPosition
277                             : Math.max(min, Math.min(max, endValue));
278                     springMenuWith(property, spring, endVelocity, endPosition,
279                             /* writeToPosition = */ true);
280                 });
281 
282         cancelAnimation(property);
283         mPositionAnimations.put(property, flingAnimation);
284         if (finalPosition != null) {
285             setAnimationEndPosition(property, finalPosition);
286         }
287         flingAnimation.start();
288     }
289 
290     @VisibleForTesting
291     FlingAnimation createFlingAnimation(MenuView menuView,
292             MenuPositionProperty menuPositionProperty) {
293         return new FlingAnimation(menuView, menuPositionProperty);
294     }
295 
296     @VisibleForTesting
297     void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
298             float velocity, float finalPosition, boolean writeToPosition) {
299         final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
300         final SpringAnimation springAnimation =
301                 new SpringAnimation(mMenuView, menuPositionProperty)
302                         .setSpring(spring)
303                         .addEndListener((animation, canceled, endValue, endVelocity) -> {
304                             if (canceled || endValue != finalPosition) {
305                                 return;
306                             }
307 
308                             final boolean areAnimationsRunning =
309                                     mPositionAnimations.values().stream().anyMatch(
310                                             DynamicAnimation::isRunning);
311                             if (!areAnimationsRunning) {
312                                 onSpringAnimationsEnd(new PointF(mMenuView.getTranslationX(),
313                                         mMenuView.getTranslationY()), writeToPosition);
314                             }
315                         })
316                         .setStartVelocity(velocity);
317 
318         cancelAnimation(property);
319         mPositionAnimations.put(property, springAnimation);
320         setAnimationEndPosition(property, finalPosition);
321         springAnimation.animateToFinalPosition(finalPosition);
322     }
323 
324     /**
325      * Determines whether to hide the menu to the edge of the screen with the given current
326      * translation x of the menu view. It should be used when receiving the action up touch event.
327      *
328      * @param currentXTranslation the current translation x of the menu view.
329      * @return true if the menu would be hidden to the edge, otherwise false.
330      */
maybeMoveToEdgeAndHide(float currentXTranslation)331     boolean maybeMoveToEdgeAndHide(float currentXTranslation) {
332         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
333 
334         // If the translation x is zero, it should be at the left of the bound.
335         if (currentXTranslation < draggableBounds.left
336                 || currentXTranslation > draggableBounds.right) {
337             constrainPositionAndUpdate(
338                     new PointF(mMenuView.getTranslationX(), mMenuView.getTranslationY()),
339                     /* writeToPosition = */ true);
340             mMenuView.onPositionChanged(true);
341             moveToEdgeAndHide();
342             return true;
343         }
344         return false;
345     }
346 
isOnLeftSide()347     boolean isOnLeftSide() {
348         return mMenuView.getTranslationX() < mMenuView.getMenuDraggableBounds().centerX();
349     }
350 
isMoveToTucked()351     boolean isMoveToTucked() {
352         return mMenuView.isMoveToTucked();
353     }
354 
getTuckedMenuPosition()355     PointF getTuckedMenuPosition() {
356         final PointF position = mMenuView.getMenuPosition();
357         final float menuHalfWidth = mMenuView.getMenuWidth() / 2.0f;
358         final float endX = isOnLeftSide()
359                 ? position.x - menuHalfWidth
360                 : position.x + menuHalfWidth;
361         return new PointF(endX, position.y);
362     }
363 
moveToEdgeAndHide()364     void moveToEdgeAndHide() {
365         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
366         final PointF position = mMenuView.getMenuPosition();
367         final PointF tuckedPosition = getTuckedMenuPosition();
368         flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
369                 Math.signum(tuckedPosition.x - position.x) * ESCAPE_VELOCITY,
370                 FLING_FRICTION_SCALAR,
371                 createDefaultSpringForce(),
372                 tuckedPosition.x);
373 
374         // Keep the touch region let users could click extra space to pop up the menu view
375         // from the screen edge
376         mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
377 
378         fadeOutIfEnabled();
379     }
380 
moveOutEdgeAndShow()381     void moveOutEdgeAndShow() {
382         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
383 
384         PointF position = mMenuView.getMenuPosition();
385         springMenuWith(DynamicAnimation.TRANSLATION_X,
386                 createDefaultSpringForce(),
387                 0,
388                 position.x,
389                 true
390         );
391         springMenuWith(DynamicAnimation.TRANSLATION_Y,
392                 createDefaultSpringForce(),
393                 0,
394                 position.y,
395                 true
396         );
397 
398         mMenuView.onEdgeChangedIfNeeded();
399     }
400 
cancelAnimations()401     void cancelAnimations() {
402         cancelAnimation(DynamicAnimation.TRANSLATION_X);
403         cancelAnimation(DynamicAnimation.TRANSLATION_Y);
404     }
405 
cancelAnimation(DynamicAnimation.ViewProperty property)406     private void cancelAnimation(DynamicAnimation.ViewProperty property) {
407         if (!mPositionAnimations.containsKey(property)) {
408             return;
409         }
410 
411         mPositionAnimations.get(property).cancel();
412     }
413 
setAnimationEndPosition( DynamicAnimation.ViewProperty property, Float endPosition)414     private void setAnimationEndPosition(
415             DynamicAnimation.ViewProperty property, Float endPosition) {
416         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
417             mAnimationEndPosition.x = endPosition;
418         }
419         if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
420             mAnimationEndPosition.y = endPosition;
421         }
422     }
423 
skipAnimations()424     void skipAnimations() {
425         cancelAnimations();
426         moveToPosition(mAnimationEndPosition, false);
427     }
428 
429     @VisibleForTesting
getAnimation(DynamicAnimation.ViewProperty property)430     DynamicAnimation getAnimation(DynamicAnimation.ViewProperty property) {
431         return mPositionAnimations.getOrDefault(property, null);
432     }
433 
onDraggingStart()434     void onDraggingStart() {
435         mMenuView.onDraggingStart();
436     }
437 
startShrinkAnimation(Runnable endAction)438     void startShrinkAnimation(Runnable endAction) {
439         mMenuView.animate().cancel();
440 
441         mMenuView.animate()
442                 .scaleX(SCALE_SHRINK)
443                 .scaleY(SCALE_SHRINK)
444                 .alpha(COMPLETELY_TRANSPARENT)
445                 .translationY(mMenuView.getTranslationY())
446                 .withEndAction(endAction).start();
447     }
448 
startGrowAnimation()449     void startGrowAnimation() {
450         mMenuView.animate().cancel();
451 
452         mMenuView.animate()
453                 .scaleX(SCALE_GROW)
454                 .scaleY(SCALE_GROW)
455                 .alpha(COMPLETELY_OPAQUE)
456                 .translationY(mMenuView.getTranslationY())
457                 .start();
458     }
459 
startRadiiAnimation(float[] endRadii)460     void startRadiiAnimation(float[] endRadii) {
461         mRadiiAnimator.startAnimation(endRadii);
462     }
463 
onSpringAnimationsEnd(PointF position, boolean writeToPosition)464     private void onSpringAnimationsEnd(PointF position, boolean writeToPosition) {
465         mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
466         constrainPositionAndUpdate(position, writeToPosition);
467 
468         if (mSpringAnimationsEndAction != null) {
469             mSpringAnimationsEndAction.run();
470         }
471     }
472 
constrainPositionAndUpdate(PointF position, boolean writeToPosition)473     private void constrainPositionAndUpdate(PointF position, boolean writeToPosition) {
474         final Rect draggableBounds = mMenuView.getMenuDraggableBoundsExcludeIme();
475         // Have the space gap margin between the top bound and the menu view, so actually the
476         // position y range needs to cut the margin.
477         position.offset(-draggableBounds.left, -draggableBounds.top);
478 
479         final float percentageX = position.x < draggableBounds.centerX()
480                 ? MIN_PERCENT : MAX_PERCENT;
481 
482         final float percentageY = position.y < 0 || draggableBounds.height() == 0
483                 ? MIN_PERCENT
484                 : Math.min(MAX_PERCENT, position.y / draggableBounds.height());
485 
486         if (!writeToPosition) {
487             mMenuView.onEdgeChangedIfNeeded();
488         } else {
489             mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
490         }
491     }
492 
493     void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
494         mIsFadeEffectEnabled = isFadeEffectEnabled;
495 
496         mHandler.removeCallbacksAndMessages(/* token= */ null);
497         mFadeOutAnimator.cancel();
498         mFadeOutAnimator.setFloatValues(COMPLETELY_OPAQUE, newOpacityValue);
499         mHandler.post(() -> mMenuView.setAlpha(
500                 mIsFadeEffectEnabled ? newOpacityValue : COMPLETELY_OPAQUE));
501     }
502 
503     void fadeInNowIfEnabled() {
504         if (!mIsFadeEffectEnabled) {
505             return;
506         }
507 
508         cancelAndRemoveCallbacksAndMessages();
509         mMenuView.setAlpha(COMPLETELY_OPAQUE);
510     }
511 
512     void fadeOutIfEnabled() {
513         if (!mIsFadeEffectEnabled) {
514             return;
515         }
516 
517         cancelAndRemoveCallbacksAndMessages();
518         mHandler.postDelayed(mFadeOutAnimator::start, FADE_EFFECT_DURATION_MS);
519     }
520 
521     private void cancelAndRemoveCallbacksAndMessages() {
522         mFadeOutAnimator.cancel();
523         mHandler.removeCallbacksAndMessages(/* token= */ null);
524     }
525 
526     void startTuckedAnimationPreview() {
527         fadeInNowIfEnabled();
528 
529         final float toXValue = isOnLeftSide()
530                 ? -ANIMATION_TO_X_VALUE
531                 : ANIMATION_TO_X_VALUE;
532         final TranslateAnimation animation =
533                 new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
534                         Animation.RELATIVE_TO_SELF, toXValue,
535                         Animation.RELATIVE_TO_SELF, 0,
536                         Animation.RELATIVE_TO_SELF, 0);
537         animation.setDuration(ANIMATION_DURATION_MS);
538         animation.setRepeatMode(Animation.REVERSE);
539         animation.setInterpolator(new OvershootInterpolator());
540         animation.setRepeatCount(Animation.INFINITE);
541         animation.setStartOffset(ANIMATION_START_OFFSET_MS);
542 
543         mMenuView.startAnimation(animation);
544     }
545 
546     private Handler createUiHandler() {
547         return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
548     }
549 
550     private static SpringForce createDefaultSpringForce() {
551         return new SpringForce()
552                 .setStiffness(SPRING_STIFFNESS)
553                 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
554     }
555 
556     static class MenuPositionProperty
557             extends FloatPropertyCompat<MenuView> {
558         private final DynamicAnimation.ViewProperty mProperty;
559 
560         MenuPositionProperty(DynamicAnimation.ViewProperty property) {
561             super(property.toString());
562             mProperty = property;
563         }
564 
565         @Override
566         public float getValue(MenuView menuView) {
567             return mProperty.getValue(menuView);
568         }
569 
570         @Override
571         public void setValue(MenuView menuView, float value) {
572             mProperty.setValue(menuView, value);
573         }
574     }
575 
576     @VisibleForTesting
577     static SpringForce createSpringForce() {
578         return new SpringForce()
579                 .setStiffness(SPRING_STIFFNESS)
580                 .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
581     }
582 }
583