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