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