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