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