1 /*
2  * Copyright (C) 2019 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.quickstep.interaction;
18 
19 import android.animation.ValueAnimator;
20 import android.annotation.SuppressLint;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Point;
28 import android.os.SystemClock;
29 import android.view.MotionEvent;
30 import android.view.VelocityTracker;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewGroup.LayoutParams;
34 import android.view.animation.Interpolator;
35 import android.view.animation.PathInterpolator;
36 
37 import androidx.core.math.MathUtils;
38 import androidx.dynamicanimation.animation.DynamicAnimation;
39 import androidx.dynamicanimation.animation.FloatPropertyCompat;
40 import androidx.dynamicanimation.animation.SpringAnimation;
41 import androidx.dynamicanimation.animation.SpringForce;
42 
43 import com.android.launcher3.R;
44 import com.android.launcher3.ResourceUtils;
45 import com.android.launcher3.anim.Interpolators;
46 import com.android.launcher3.util.VibratorWrapper;
47 
48 /** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */
49 public class EdgeBackGesturePanel extends View {
50 
51     private static final String LOG_TAG = "EdgeBackGesturePanel";
52 
53     private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80;
54     private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
55 
56     /**
57      * The time required since the first vibration effect to automatically trigger a click
58      */
59     private static final int GESTURE_DURATION_FOR_CLICK_MS = 400;
60 
61     /**
62      * The basic translation in dp where the arrow resides
63      */
64     private static final int BASE_TRANSLATION_DP = 32;
65 
66     /**
67      * The length of the arrow leg measured from the center to the end
68      */
69     private static final int ARROW_LENGTH_DP = 18;
70 
71     /**
72      * The angle measured from the xAxis, where the leg is when the arrow rests
73      */
74     private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
75 
76     /**
77      * The angle that is added per 1000 px speed to the angle of the leg
78      */
79     private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 4;
80 
81     /**
82      * The maximum angle offset allowed due to speed
83      */
84     private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
85 
86     /**
87      * The thickness of the arrow. Adjusted to match the home handle (approximately)
88      */
89     private static final float ARROW_THICKNESS_DP = 2.5f;
90 
91     /**
92      * The amount of rubber banding we do for the vertical translation
93      */
94     private static final int RUBBER_BAND_AMOUNT = 15;
95 
96     /**
97      * The interpolator used to rubberband
98      */
99     private static final Interpolator RUBBER_BAND_INTERPOLATOR =
100             new PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f);
101 
102     /**
103      * The amount of rubber banding we do for the translation before base translation
104      */
105     private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
106 
107     /**
108      * The interpolator used to rubberband the appearing of the arrow.
109      */
110     private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR =
111             new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
112 
113     private BackCallback mBackCallback;
114 
115     /**
116      * The paint the arrow is drawn with
117      */
118     private final Paint mPaint = new Paint();
119 
120     private final float mDensity;
121     private final float mBaseTranslation;
122     private final float mArrowLength;
123     private final float mArrowThickness;
124 
125     /**
126      * The minimum delta needed in movement for the arrow to change direction / stop triggering back
127      */
128     private final float mMinDeltaForSwitch;
129     // The closest to y = 0 that the arrow will be displayed.
130     private int mMinArrowPosition;
131     // The amount the arrow is shifted to avoid the finger.
132     private int mFingerOffset;
133 
134     private final float mSwipeThreshold;
135     private final Path mArrowPath = new Path();
136     private final Point mDisplaySize = new Point();
137 
138     private final SpringAnimation mAngleAnimation;
139     private final SpringAnimation mTranslationAnimation;
140     private final SpringAnimation mVerticalTranslationAnimation;
141     private final SpringForce mAngleAppearForce;
142     private final SpringForce mAngleDisappearForce;
143     private final ValueAnimator mArrowDisappearAnimation;
144     private final SpringForce mRegularTranslationSpring;
145     private final SpringForce mTriggerBackSpring;
146 
147     private VelocityTracker mVelocityTracker;
148     private int mArrowPaddingEnd;
149 
150     /**
151      * True if the panel is currently on the left of the screen
152      */
153     private boolean mIsLeftPanel;
154 
155     private float mStartX;
156     private float mStartY;
157     private float mCurrentAngle;
158     /**
159      * The current translation of the arrow
160      */
161     private float mCurrentTranslation;
162     /**
163      * Where the arrow will be in the resting position.
164      */
165     private float mDesiredTranslation;
166 
167     private boolean mDragSlopPassed;
168     private boolean mArrowsPointLeft;
169     private float mMaxTranslation;
170     private boolean mTriggerBack;
171     private float mPreviousTouchTranslation;
172     private float mTotalTouchDelta;
173     private float mVerticalTranslation;
174     private float mDesiredVerticalTranslation;
175     private float mDesiredAngle;
176     private float mAngleOffset;
177     private float mDisappearAmount;
178     private long mVibrationTime;
179     private int mScreenSize;
180 
181     private final DynamicAnimation.OnAnimationEndListener mSetGoneEndListener =
182             new DynamicAnimation.OnAnimationEndListener() {
183                 @Override
184                 public void onAnimationEnd(
185                         DynamicAnimation animation, boolean canceled, float value, float velocity) {
186                     animation.removeEndListener(this);
187                     if (!canceled) {
188                         setVisibility(GONE);
189                     }
190                 }
191             };
192 
193     private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_ANGLE =
194             new FloatPropertyCompat<EdgeBackGesturePanel>("currentAngle") {
195                 @Override
196                 public void setValue(EdgeBackGesturePanel object, float value) {
197                     object.setCurrentAngle(value);
198                 }
199 
200                 @Override
201                 public float getValue(EdgeBackGesturePanel object) {
202                     return object.getCurrentAngle();
203                 }
204             };
205 
206     private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_TRANSLATION =
207             new FloatPropertyCompat<EdgeBackGesturePanel>("currentTranslation") {
208                 @Override
209                 public void setValue(EdgeBackGesturePanel object, float value) {
210                     object.setCurrentTranslation(value);
211                 }
212 
213                 @Override
214                 public float getValue(EdgeBackGesturePanel object) {
215                     return object.getCurrentTranslation();
216                 }
217             };
218 
219     private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_VERTICAL_TRANSLATION =
220             new FloatPropertyCompat<EdgeBackGesturePanel>("verticalTranslation") {
221 
222                 @Override
223                 public void setValue(EdgeBackGesturePanel object, float value) {
224                     object.setVerticalTranslation(value);
225                 }
226 
227                 @Override
228                 public float getValue(EdgeBackGesturePanel object) {
229                     return object.getVerticalTranslation();
230                 }
231             };
232 
EdgeBackGesturePanel(Context context, ViewGroup parent, LayoutParams layoutParams)233     public EdgeBackGesturePanel(Context context, ViewGroup parent, LayoutParams layoutParams) {
234         super(context);
235 
236         mDensity = context.getResources().getDisplayMetrics().density;
237 
238         mBaseTranslation = dp(BASE_TRANSLATION_DP);
239         mArrowLength = dp(ARROW_LENGTH_DP);
240         mArrowThickness = dp(ARROW_THICKNESS_DP);
241         mMinDeltaForSwitch = dp(32);
242 
243         mPaint.setStrokeWidth(mArrowThickness);
244         mPaint.setStrokeCap(Paint.Cap.ROUND);
245         mPaint.setAntiAlias(true);
246         mPaint.setStyle(Paint.Style.STROKE);
247         mPaint.setStrokeJoin(Paint.Join.ROUND);
248 
249         mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
250         mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
251         mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
252         mArrowDisappearAnimation.addUpdateListener(animation -> {
253             mDisappearAmount = (float) animation.getAnimatedValue();
254             invalidate();
255         });
256 
257         mAngleAnimation =
258                 new SpringAnimation(this, CURRENT_ANGLE);
259         mAngleAppearForce = new SpringForce()
260                 .setStiffness(500)
261                 .setDampingRatio(0.5f);
262         mAngleDisappearForce = new SpringForce()
263                 .setStiffness(SpringForce.STIFFNESS_MEDIUM)
264                 .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
265                 .setFinalPosition(90);
266         mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
267 
268         mTranslationAnimation =
269                 new SpringAnimation(this, CURRENT_TRANSLATION);
270         mRegularTranslationSpring = new SpringForce()
271                 .setStiffness(SpringForce.STIFFNESS_MEDIUM)
272                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
273         mTriggerBackSpring = new SpringForce()
274                 .setStiffness(450)
275                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
276         mTranslationAnimation.setSpring(mRegularTranslationSpring);
277         mVerticalTranslationAnimation =
278                 new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
279         mVerticalTranslationAnimation.setSpring(
280                 new SpringForce()
281                         .setStiffness(SpringForce.STIFFNESS_MEDIUM)
282                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
283         int currentNightMode =
284                 context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
285         mPaint.setColor(context.getColor(currentNightMode == Configuration.UI_MODE_NIGHT_YES
286                 ? R.color.back_arrow_color_light
287                 : R.color.back_arrow_color_dark));
288         loadDimens();
289         updateArrowDirection();
290 
291         mSwipeThreshold = ResourceUtils.getDimenByName(
292             "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */);
293         parent.addView(this, layoutParams);
294         setVisibility(GONE);
295     }
296 
onDestroy()297     void onDestroy() {
298         ViewGroup parent = (ViewGroup) getParent();
299         if (parent != null) {
300             parent.removeView(this);
301         }
302     }
303 
304     @Override
hasOverlappingRendering()305     public boolean hasOverlappingRendering() {
306         return false;
307     }
308 
309     @SuppressLint("RtlHardcoded")
setIsLeftPanel(boolean isLeftPanel)310     void setIsLeftPanel(boolean isLeftPanel) {
311         mIsLeftPanel = isLeftPanel;
312     }
313 
getIsLeftPanel()314     boolean getIsLeftPanel() {
315         return mIsLeftPanel;
316     }
317 
setDisplaySize(Point displaySize)318     void setDisplaySize(Point displaySize) {
319         mDisplaySize.set(displaySize.x, displaySize.y);
320         mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
321     }
322 
setBackCallback(BackCallback callback)323     void setBackCallback(BackCallback callback) {
324         mBackCallback = callback;
325     }
326 
getCurrentAngle()327     private float getCurrentAngle() {
328         return mCurrentAngle;
329     }
330 
getCurrentTranslation()331     private float getCurrentTranslation() {
332         return mCurrentTranslation;
333     }
334 
onMotionEvent(MotionEvent event)335     void onMotionEvent(MotionEvent event) {
336         if (mVelocityTracker == null) {
337             mVelocityTracker = VelocityTracker.obtain();
338         }
339         mVelocityTracker.addMovement(event);
340         switch (event.getActionMasked()) {
341             case MotionEvent.ACTION_DOWN:
342                 mDragSlopPassed = false;
343                 resetOnDown();
344                 mStartX = event.getX();
345                 mStartY = event.getY();
346                 setVisibility(VISIBLE);
347                 updatePosition(event.getY());
348                 break;
349             case MotionEvent.ACTION_MOVE:
350                 handleMoveEvent(event);
351                 break;
352             case MotionEvent.ACTION_UP:
353                 if (mTriggerBack) {
354                     triggerBack();
355                 } else {
356                     cancelBack();
357                 }
358                 mVelocityTracker.recycle();
359                 mVelocityTracker = null;
360                 break;
361             case MotionEvent.ACTION_CANCEL:
362                 cancelBack();
363                 mVelocityTracker.recycle();
364                 mVelocityTracker = null;
365                 break;
366         }
367     }
368 
369     @Override
onConfigurationChanged(Configuration newConfig)370     protected void onConfigurationChanged(Configuration newConfig) {
371         super.onConfigurationChanged(newConfig);
372         updateArrowDirection();
373         loadDimens();
374     }
375 
376     @Override
onDraw(Canvas canvas)377     protected void onDraw(Canvas canvas) {
378         float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
379         canvas.save();
380         canvas.translate(
381                 mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
382                 (getHeight() * 0.5f) + mVerticalTranslation);
383 
384         // Let's calculate the position of the end based on the angle
385         float x = (polarToCartX(mCurrentAngle) * mArrowLength);
386         float y = (polarToCartY(mCurrentAngle) * mArrowLength);
387         Path arrowPath = calculatePath(x, y);
388 
389         canvas.drawPath(arrowPath, mPaint);
390         canvas.restore();
391     }
392 
393     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)394     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
395         super.onLayout(changed, left, top, right, bottom);
396         mMaxTranslation = getWidth() - mArrowPaddingEnd;
397     }
398 
loadDimens()399     private void loadDimens() {
400         Resources res = getResources();
401         mArrowPaddingEnd = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8);
402         mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64);
403         mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48);
404     }
405 
updateArrowDirection()406     private void updateArrowDirection() {
407         // Both panels arrow point the same way
408         mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
409         invalidate();
410     }
411 
getStaticArrowWidth()412     private float getStaticArrowWidth() {
413         return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
414     }
415 
polarToCartX(float angleInDegrees)416     private float polarToCartX(float angleInDegrees) {
417         return (float) Math.cos(Math.toRadians(angleInDegrees));
418     }
419 
polarToCartY(float angleInDegrees)420     private float polarToCartY(float angleInDegrees) {
421         return (float) Math.sin(Math.toRadians(angleInDegrees));
422     }
423 
calculatePath(float x, float y)424     private Path calculatePath(float x, float y) {
425         if (!mArrowsPointLeft) {
426             x = -x;
427         }
428         float extent = lerp(1.0f, 0.75f, mDisappearAmount);
429         x = x * extent;
430         y = y * extent;
431         mArrowPath.reset();
432         mArrowPath.moveTo(x, y);
433         mArrowPath.lineTo(0, 0);
434         mArrowPath.lineTo(x, -y);
435         return mArrowPath;
436     }
437 
lerp(float start, float stop, float amount)438     private static float lerp(float start, float stop, float amount) {
439         return start + (stop - start) * amount;
440     }
441 
triggerBack()442     private void triggerBack() {
443         if (mBackCallback != null) {
444             mBackCallback.triggerBack();
445         }
446 
447         if (mVelocityTracker == null) {
448             mVelocityTracker = VelocityTracker.obtain();
449         }
450         mVelocityTracker.computeCurrentVelocity(1000);
451         // Only do the extra translation if we're not already flinging
452         boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500;
453         if (isSlow
454                 || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
455             VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
456         }
457 
458         // Let's also snap the angle a bit
459         if (mAngleOffset > -4) {
460             mAngleOffset = Math.max(-8, mAngleOffset - 8);
461             updateAngle(true /* animated */);
462         }
463 
464         // Finally, after the translation, animate back and disappear the arrow
465         Runnable translationEnd = () -> {
466             // let's snap it back
467             mAngleOffset = Math.max(0, mAngleOffset + 8);
468             updateAngle(true /* animated */);
469 
470             mTranslationAnimation.setSpring(mTriggerBackSpring);
471             // Translate the arrow back a bit to make for a nice transition
472             setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */);
473             animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
474                     .withEndAction(() -> setVisibility(GONE));
475             mArrowDisappearAnimation.start();
476         };
477         if (mTranslationAnimation.isRunning()) {
478             mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
479                 @Override
480                 public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
481                         float value,
482                         float velocity) {
483                     animation.removeEndListener(this);
484                     if (!canceled) {
485                         translationEnd.run();
486                     }
487                 }
488             });
489         } else {
490             translationEnd.run();
491         }
492     }
493 
cancelBack()494     private void cancelBack() {
495         if (mBackCallback != null) {
496             mBackCallback.cancelBack();
497         }
498 
499         if (mTranslationAnimation.isRunning()) {
500             mTranslationAnimation.addEndListener(mSetGoneEndListener);
501         } else {
502             setVisibility(GONE);
503         }
504     }
505 
resetOnDown()506     private void resetOnDown() {
507         animate().cancel();
508         mAngleAnimation.cancel();
509         mTranslationAnimation.cancel();
510         mVerticalTranslationAnimation.cancel();
511         mArrowDisappearAnimation.cancel();
512         mAngleOffset = 0;
513         mTranslationAnimation.setSpring(mRegularTranslationSpring);
514         // Reset the arrow to the side
515         setTriggerBack(false /* triggerBack */, false /* animated */);
516         setDesiredTranslation(0, false /* animated */);
517         setCurrentTranslation(0);
518         updateAngle(false /* animate */);
519         mPreviousTouchTranslation = 0;
520         mTotalTouchDelta = 0;
521         mVibrationTime = 0;
522         setDesiredVerticalTransition(0, false /* animated */);
523     }
524 
handleMoveEvent(MotionEvent event)525     private void handleMoveEvent(MotionEvent event) {
526         float x = event.getX();
527         float y = event.getY();
528         float touchTranslation = Math.abs(x - mStartX);
529         float yOffset = y - mStartY;
530         float delta = touchTranslation - mPreviousTouchTranslation;
531         if (Math.abs(delta) > 0) {
532             if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
533                 mTotalTouchDelta += delta;
534             } else {
535                 mTotalTouchDelta = delta;
536             }
537         }
538         mPreviousTouchTranslation = touchTranslation;
539 
540         // Apply a haptic on drag slop passed
541         if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
542             mDragSlopPassed = true;
543             VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
544             mVibrationTime = SystemClock.uptimeMillis();
545 
546             // Let's show the arrow and animate it in!
547             mDisappearAmount = 0.0f;
548             setAlpha(1f);
549             // And animate it go to back by default!
550             setTriggerBack(true /* triggerBack */, true /* animated */);
551         }
552 
553         // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
554         if (touchTranslation > mBaseTranslation) {
555             float diff = touchTranslation - mBaseTranslation;
556             float progress = MathUtils.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1);
557             progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
558                     * (mMaxTranslation - mBaseTranslation);
559             touchTranslation = mBaseTranslation + progress;
560         } else {
561             float diff = mBaseTranslation - touchTranslation;
562             float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1);
563             progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
564                     * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
565             touchTranslation = mBaseTranslation - progress;
566         }
567         // By default we just assume the current direction is kept
568         boolean triggerBack = mTriggerBack;
569 
570         //  First lets see if we had continuous motion in one direction for a while
571         if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
572             triggerBack = mTotalTouchDelta > 0;
573         }
574 
575         // Then, let's see if our velocity tells us to change direction
576         mVelocityTracker.computeCurrentVelocity(1000);
577         float xVelocity = mVelocityTracker.getXVelocity();
578         float yVelocity = mVelocityTracker.getYVelocity();
579         float velocity = (float) Math.hypot(xVelocity, yVelocity);
580         mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
581                 ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
582         if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
583             mAngleOffset *= -1;
584         }
585 
586         // Last if the direction in Y is bigger than X * 2 we also abort
587         if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
588             triggerBack = false;
589         }
590         setTriggerBack(triggerBack, true /* animated */);
591 
592         if (!mTriggerBack) {
593             touchTranslation = 0;
594         } else if (mIsLeftPanel && mArrowsPointLeft
595                 || (!mIsLeftPanel && !mArrowsPointLeft)) {
596             // If we're on the left we should move less, because the arrow is facing the other
597             // direction
598             touchTranslation -= getStaticArrowWidth();
599         }
600         setDesiredTranslation(touchTranslation, true /* animated */);
601         updateAngle(true /* animated */);
602 
603         float maxYOffset = getHeight() / 2.0f - mArrowLength;
604         float progress =
605                 MathUtils.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1);
606         float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
607                 * maxYOffset * Math.signum(yOffset);
608         setDesiredVerticalTransition(verticalTranslation, true /* animated */);
609     }
610 
updatePosition(float touchY)611     private void updatePosition(float touchY) {
612         float positionY = touchY - mFingerOffset;
613         positionY = Math.max(positionY, mMinArrowPosition);
614         positionY -= getLayoutParams().height / 2.0f;
615         setX(mIsLeftPanel ? 0 : mDisplaySize.x - getLayoutParams().width);
616         setY(MathUtils.clamp((int) positionY, 0, mDisplaySize.y));
617     }
618 
setDesiredVerticalTransition(float verticalTranslation, boolean animated)619     private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
620         if (mDesiredVerticalTranslation != verticalTranslation) {
621             mDesiredVerticalTranslation = verticalTranslation;
622             if (!animated) {
623                 setVerticalTranslation(verticalTranslation);
624             } else {
625                 mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
626             }
627             invalidate();
628         }
629     }
630 
setVerticalTranslation(float verticalTranslation)631     private void setVerticalTranslation(float verticalTranslation) {
632         mVerticalTranslation = verticalTranslation;
633         invalidate();
634     }
635 
getVerticalTranslation()636     private float getVerticalTranslation() {
637         return mVerticalTranslation;
638     }
639 
setDesiredTranslation(float desiredTranslation, boolean animated)640     private void setDesiredTranslation(float desiredTranslation, boolean animated) {
641         if (mDesiredTranslation != desiredTranslation) {
642             mDesiredTranslation = desiredTranslation;
643             if (!animated) {
644                 setCurrentTranslation(desiredTranslation);
645             } else {
646                 mTranslationAnimation.animateToFinalPosition(desiredTranslation);
647             }
648         }
649     }
650 
setCurrentTranslation(float currentTranslation)651     private void setCurrentTranslation(float currentTranslation) {
652         mCurrentTranslation = currentTranslation;
653         invalidate();
654     }
655 
setTriggerBack(boolean triggerBack, boolean animated)656     private void setTriggerBack(boolean triggerBack, boolean animated) {
657         if (mTriggerBack != triggerBack) {
658             mTriggerBack = triggerBack;
659             mAngleAnimation.cancel();
660             updateAngle(animated);
661             // Whenever the trigger back state changes the existing translation animation should be
662             // cancelled
663             mTranslationAnimation.cancel();
664         }
665     }
666 
updateAngle(boolean animated)667     private void updateAngle(boolean animated) {
668         float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
669         if (newAngle != mDesiredAngle) {
670             if (!animated) {
671                 setCurrentAngle(newAngle);
672             } else {
673                 mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
674                 mAngleAnimation.animateToFinalPosition(newAngle);
675             }
676             mDesiredAngle = newAngle;
677         }
678     }
679 
setCurrentAngle(float currentAngle)680     private void setCurrentAngle(float currentAngle) {
681         mCurrentAngle = currentAngle;
682         invalidate();
683     }
684 
dp(float dp)685     private float dp(float dp) {
686         return mDensity * dp;
687     }
688 
689     /** Callback to let the gesture handler react to the detected back gestures. */
690     interface BackCallback {
691         /** Indicates that a Back gesture was recognized. */
triggerBack()692         void triggerBack();
693 
694         /** Indicates that the gesture was cancelled. */
cancelBack()695         void cancelBack();
696     }
697 }
698