summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/values/dimens.xml6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java739
3 files changed, 580 insertions, 173 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a1ae95250565..a0c021a3f132 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -41,8 +41,10 @@
<!-- Size of the nav bar edge panels, should be greater to the
edge sensitivity + the drag threshold -->
- <dimen name="navigation_edge_panel_width">52dp</dimen>
- <dimen name="navigation_edge_panel_height">52dp</dimen>
+ <dimen name="navigation_edge_panel_width">76dp</dimen>
+ <!-- Padding at the end of the navigation panel to allow the arrow not to be clipped off -->
+ <dimen name="navigation_edge_panel_padding">24dp</dimen>
+ <dimen name="navigation_edge_panel_height">84dp</dimen>
<!-- The threshold to drag to trigger the edge action -->
<dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
<!-- The minimum display position of the arrow on the screen -->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index ca78e7c777fa..4638c40c415d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -327,13 +327,13 @@ public class EdgeBackGestureHandler implements DisplayListener {
? (Gravity.LEFT | Gravity.TOP)
: (Gravity.RIGHT | Gravity.TOP);
mEdgePanel.setIsLeftPanel(mIsOnLeftEdge);
+ mEdgePanel.handleTouch(ev);
updateEdgePanelPosition(ev.getY());
mWm.updateViewLayout(mEdgePanel, mEdgePanelLp);
mRegionSamplingHelper.start(mSamplingRect);
mDownPoint.set(ev.getX(), ev.getY());
mThresholdCrossed = false;
- mEdgePanel.handleTouch(ev);
}
} else if (mAllowGesture) {
if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) {
@@ -360,11 +360,7 @@ public class EdgeBackGestureHandler implements DisplayListener {
boolean isUp = ev.getAction() == MotionEvent.ACTION_UP;
if (isUp) {
- float xDiff = ev.getX() - mDownPoint.x;
- boolean exceedsThreshold = mIsOnLeftEdge
- ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold);
- boolean performAction = exceedsThreshold
- && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y);
+ boolean performAction = mEdgePanel.shouldTriggerBack();
if (performAction) {
// Perform back
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
index 5e25162be071..79c7ab1d2b8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java
@@ -16,102 +16,221 @@
package com.android.systemui.statusbar.phone;
-import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.content.Context;
-import android.graphics.Canvas;
+import android.content.res.Configuration;
+import android.graphics.Canvas;;
import android.graphics.Paint;
+import android.graphics.Path;
import android.graphics.Rect;
-import android.os.SystemClock;
import android.os.VibrationEffect;
-import android.util.FloatProperty;
import android.util.MathUtils;
import android.view.ContextThemeWrapper;
-import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
+import android.view.VelocityTracker;
import android.view.View;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.VibratorHelper;
+import androidx.core.graphics.ColorUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
public class NavigationBarEdgePanel extends View {
- // TODO: read from resources once drawing is finalized.
- private static final int PROTECTION_WIDTH_PX = 4;
- private static final int BASE_EXTENT = 32;
- private static final int ARROW_HEIGHT_DP = 32;
- private static final int POINT_EXTENT_DP = 8;
- private static final int ARROW_THICKNESS_DP = 4;
- private static final float TRACK_LENGTH_MULTIPLIER = 1.5f;
- private static final float START_POINTING_RATIO = 0.3f;
- private static final float POINTEDNESS_BEFORE_SNAP_RATIO = 0.4f;
- private static final int ANIM_DURATION_MS = 150;
- private static final long HAPTIC_TIMEOUT_MS = 200;
+ private static final long COLOR_ANIMATION_DURATION_MS = 100;
+ private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 140;
+ private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
+
+ /**
+ * The size of the protection of the arrow in px. Only used if this is not background protected
+ */
+ private static final int PROTECTION_WIDTH_PX = 2;
+
+ /**
+ * The basic translation in dp where the arrow resides
+ */
+ private static final int BASE_TRANSLATION_DP = 32;
+
+ /**
+ * The length of the arrow leg measured from the center to the end
+ */
+ private static final int ARROW_LENGTH_DP = 18;
+
+ /**
+ * The angle measured from the xAxis, where the leg is when the arrow rests
+ */
+ private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
+
+ /**
+ * The angle that is added per 1000 px speed to the angle of the leg
+ */
+ private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 8;
+
+ /**
+ * The maximum angle offset allowed due to speed
+ */
+ private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
+
+ /**
+ * The thickness of the arrow. Adjusted to match the home handle (approximately)
+ */
+ private static final float ARROW_THICKNESS_DP = 2.5f;
+
+ /**
+ * The amount of rubber banding we do for the horizontal translation beyond the base translation
+ */
+ private static final int RUBBER_BAND_AMOUNT = 10;
+
+ /**
+ * The interpolator used to rubberband
+ */
+ private static final Interpolator RUBBER_BAND_INTERPOLATOR
+ = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT, 1.0f, 1.0f, 1.0f);
+
+ /**
+ * The amount of rubber banding we do for the translation before base translation
+ */
+ private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
+
+ /**
+ * The interpolator used to rubberband the appearing of the arrow.
+ */
+ private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR
+ = new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
private final VibratorHelper mVibratorHelper;
+ /**
+ * The paint the arrow is drawn with
+ */
private final Paint mPaint = new Paint();
- private final Paint mProtectionPaint = new Paint();
-
- private final ObjectAnimator mEndAnimator;
- private final ObjectAnimator mLegAnimator;
+ /**
+ * The paint the arrow protection is drawn with
+ */
+ private final Paint mProtectionPaint;
private final float mDensity;
- private final float mBaseExtent;
- private final float mPointExtent;
- private final float mHeight;
- private final float mStrokeThickness;
+ private final float mBaseTranslation;
+ private final float mArrowLength;
+ private final float mArrowThickness;
- private final float mSwipeThreshold;
+ /**
+ * The minimum delta needed in movement for the arrow to change direction / stop triggering back
+ */
+ private final float mMinDeltaForSwitch;
+ private final float mSwipeThreshold;
+ private final Path mArrowPath = new Path();
+
+ private final SpringAnimation mAngleAnimation;
+ private final SpringAnimation mTranslationAnimation;
+ private final SpringAnimation mVerticalTranslationAnimation;
+ private final SpringForce mAngleAppearForce;
+ private final SpringForce mAngleDisappearForce;
+ private final ValueAnimator mArrowColorAnimator;
+ private final ValueAnimator mArrowDisappearAnimation;
+ private final SpringForce mRegularTranslationSpring;
+ private final SpringForce mTriggerBackSpring;
+
+ private VelocityTracker mVelocityTracker;
private boolean mIsDark = false;
private boolean mShowProtection = false;
private int mProtectionColorLight;
+ private int mArrowPaddingEnd;
private int mArrowColorLight;
private int mProtectionColorDark;
private int mArrowColorDark;
private int mProtectionColor;
private int mArrowColor;
+ /**
+ * True if the panel is currently on the left of the screen
+ */
private boolean mIsLeftPanel;
private float mStartX;
+ private float mStartY;
+ private float mCurrentAngle;
+ /**
+ * The current translation of the arrow
+ */
+ private float mCurrentTranslation;
+ /**
+ * Where the arrow will be in the resting position.
+ */
+ private float mDesiredTranslation;
private boolean mDragSlopPassed;
- private long mLastSlopHapticTime;
- private boolean mGestureDetected;
private boolean mArrowsPointLeft;
- private float mGestureLength;
- private float mLegProgress;
- private float mDragProgress;
-
- // How much the "legs" of the back arrow have proceeded from being a line to an arrow.
- private static final FloatProperty<NavigationBarEdgePanel> LEG_PROGRESS =
- new FloatProperty<NavigationBarEdgePanel>("legProgress") {
+ private float mMaxTranslation;
+ private boolean mTriggerBack;
+ private float mPreviousTouchTranslation;
+ private float mTotalTouchDelta;
+ private float mVerticalTranslation;
+ private float mDesiredVerticalTranslation;
+ private float mDesiredAngle;
+ private float mAngleOffset;
+ private int mArrowStartColor;
+ private int mCurrentArrowColor;
+ private float mDisappearAmount;
+
+ private DynamicAnimation.OnAnimationEndListener mSetGoneEndListener
+ = new DynamicAnimation.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
+ float velocity) {
+ animation.removeEndListener(this);
+ if (!canceled) {
+ setVisibility(GONE);
+ }
+ }
+ };
+ private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_ANGLE =
+ new FloatPropertyCompat<NavigationBarEdgePanel>("currentAngle") {
@Override
public void setValue(NavigationBarEdgePanel object, float value) {
- object.setLegProgress(value);
+ object.setCurrentAngle(value);
}
@Override
- public Float get(NavigationBarEdgePanel object) {
- return object.getLegProgress();
+ public float getValue(NavigationBarEdgePanel object) {
+ return object.getCurrentAngle();
}
};
- // How far across the view the arrow should be drawn.
- private static final FloatProperty<NavigationBarEdgePanel> DRAG_PROGRESS =
- new FloatProperty<NavigationBarEdgePanel>("dragProgress") {
+ private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_TRANSLATION =
+ new FloatPropertyCompat<NavigationBarEdgePanel>("currentTranslation") {
+
+ @Override
+ public void setValue(NavigationBarEdgePanel object, float value) {
+ object.setCurrentTranslation(value);
+ }
+
+ @Override
+ public float getValue(NavigationBarEdgePanel object) {
+ return object.getCurrentTranslation();
+ }
+ };
+ private static final FloatPropertyCompat<NavigationBarEdgePanel> CURRENT_VERTICAL_TRANSLATION =
+ new FloatPropertyCompat<NavigationBarEdgePanel>("verticalTranslation") {
@Override
public void setValue(NavigationBarEdgePanel object, float value) {
- object.setDragProgress(value);
+ object.setVerticalTranslation(value);
}
@Override
- public Float get(NavigationBarEdgePanel object) {
- return object.getDragProgress();
+ public float getValue(NavigationBarEdgePanel object) {
+ return object.getVerticalTranslation();
}
};
@@ -120,56 +239,85 @@ public class NavigationBarEdgePanel extends View {
mVibratorHelper = Dependency.get(VibratorHelper.class);
- mEndAnimator = ObjectAnimator.ofFloat(this, DRAG_PROGRESS, 1f);
- mEndAnimator.setAutoCancel(true);
- mEndAnimator.setDuration(ANIM_DURATION_MS);
-
- mLegAnimator = ObjectAnimator.ofFloat(this, LEG_PROGRESS, 1f);
- mLegAnimator.setAutoCancel(true);
- mLegAnimator.setDuration(ANIM_DURATION_MS);
-
mDensity = context.getResources().getDisplayMetrics().density;
- mBaseExtent = dp(BASE_EXTENT);
- mHeight = dp(ARROW_HEIGHT_DP);
- mPointExtent = dp(POINT_EXTENT_DP);
- mStrokeThickness = dp(ARROW_THICKNESS_DP);
+ mBaseTranslation = dp(BASE_TRANSLATION_DP);
+ mArrowLength = dp(ARROW_LENGTH_DP);
+ mArrowThickness = dp(ARROW_THICKNESS_DP);
+ mMinDeltaForSwitch = dp(32);
- mPaint.setStrokeWidth(mStrokeThickness);
+ mPaint.setStrokeWidth(mArrowThickness);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
-
- mProtectionPaint.setStrokeWidth(mStrokeThickness + PROTECTION_WIDTH_PX);
- mProtectionPaint.setStrokeCap(Paint.Cap.ROUND);
- mProtectionPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeJoin(Paint.Join.ROUND);
+
+ mArrowColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mArrowColorAnimator.setDuration(COLOR_ANIMATION_DURATION_MS);
+ mArrowColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int newColor = ColorUtils.blendARGB(mArrowStartColor, mArrowColor,
+ animation.getAnimatedFraction());
+ setCurrentArrowColor(newColor);
+ }
+ });
+
+ mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+ mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
+ mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mArrowDisappearAnimation.addUpdateListener(animation -> {
+ mDisappearAmount = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+
+ mAngleAnimation =
+ new SpringAnimation(this, CURRENT_ANGLE);
+ mAngleAppearForce = new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .setDampingRatio(0.4f)
+ .setFinalPosition(ARROW_ANGLE_WHEN_EXTENDED_DEGREES);
+ mAngleDisappearForce = new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+ .setFinalPosition(90);
+ mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
+
+ mTranslationAnimation =
+ new SpringAnimation(this, CURRENT_TRANSLATION);
+ mRegularTranslationSpring = new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+ mTriggerBackSpring = new SpringForce()
+ .setStiffness(450)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+ mTranslationAnimation.setSpring(mRegularTranslationSpring);
+ mVerticalTranslationAnimation =
+ new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
+ mVerticalTranslationAnimation.setSpring(
+ new SpringForce()
+ .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+
+ mProtectionPaint = new Paint(mPaint);
+ mProtectionPaint.setStrokeWidth(mArrowThickness + PROTECTION_WIDTH_PX);
+ loadDimens();
loadColors(context);
- // Both panels arrow point the same way
- mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+ updateArrowDirection();
mSwipeThreshold = context.getResources()
.getDimension(R.dimen.navigation_edge_action_drag_threshold);
setVisibility(GONE);
}
- private void loadColors(Context context) {
- final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme);
- final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme);
- Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme);
- Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme);
- mArrowColorLight = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor);
- mArrowColorDark = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor);
- mProtectionColorDark = mArrowColorLight;
- mProtectionColorLight = mArrowColorDark;
- updateIsDark(false /* animate */);
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
}
- private void updateIsDark(boolean animate) {
- mArrowColor = mIsDark ? mArrowColorDark : mArrowColorLight;
- mProtectionColor = mIsDark ? mProtectionColorDark : mProtectionColorLight;
- mProtectionPaint.setColor(mProtectionColor);
- mPaint.setColor(mArrowColor);
- // TODO: add animation
+ public boolean shouldTriggerBack() {
+ return mTriggerBack;
}
public void setIsDark(boolean isDark, boolean animate) {
@@ -186,25 +334,105 @@ public class NavigationBarEdgePanel extends View {
mIsLeftPanel = isLeftPanel;
}
+ /**
+ * Adjust the rect to conform the the actual visible bounding box of the arrow.
+ *
+ * @param samplingRect the existing bounding box in screen coordinates, to be modified
+ */
+ public void adjustRectToBoundingBox(Rect samplingRect) {
+ float translation = mDesiredTranslation;
+ if (!mTriggerBack) {
+ // Let's take the resting position and bounds as the sampling rect, since we are not
+ // visible right now
+ translation = mBaseTranslation;
+ if (mIsLeftPanel && mArrowsPointLeft
+ || (!mIsLeftPanel && !mArrowsPointLeft)) {
+ // If we're on the left we should move less, because the arrow is facing the other
+ // direction
+ translation -= getStaticArrowWidth();
+ }
+ }
+ float left = translation - mArrowThickness / 2.0f;
+ left = mIsLeftPanel ? left : samplingRect.width() - left;
+
+ // Let's calculate the position of the end based on the angle
+ float width = getStaticArrowWidth();
+ float height = polarToCartY(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength * 2.0f;
+ if (!mArrowsPointLeft) {
+ left -= width;
+ }
+
+ float top = (getHeight() * 0.5f) + mDesiredVerticalTranslation - height / 2.0f;
+ samplingRect.offset((int) left, (int) top);
+ samplingRect.set(samplingRect.left, samplingRect.top,
+ (int) (samplingRect.left + width),
+ (int) (samplingRect.top + height));
+ }
+
+ /**
+ * Updates the UI based on the motion events passed in device co-ordinates
+ */
+ public void handleTouch(MotionEvent event) {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(event);
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN : {
+ mDragSlopPassed = false;
+ resetOnDown();
+ mStartX = event.getX();
+ mStartY = event.getY();
+ setVisibility(VISIBLE);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ handleMoveEvent(event);
+ break;
+ }
+ // Fall through
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ if (mTriggerBack) {
+ triggerBackAnimation();
+ } else {
+ if (mTranslationAnimation.isRunning()) {
+ mTranslationAnimation.addEndListener(mSetGoneEndListener);
+ } else {
+ setVisibility(GONE);
+ }
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateArrowDirection();
+ loadDimens();
+ }
+
@Override
protected void onDraw(Canvas canvas) {
- float edgeOffset = mBaseExtent * mDragProgress - mStrokeThickness;
- float animatedOffset = mPointExtent * mLegProgress;
+ float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
canvas.save();
canvas.translate(
- mIsLeftPanel ? edgeOffset : getWidth() - edgeOffset,
- (getHeight() - mHeight) * 0.5f);
-
- float outsideX = mArrowsPointLeft ? animatedOffset : 0;
- float middleX = mArrowsPointLeft ? 0 : animatedOffset;
+ mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
+ (getHeight() * 0.5f) + mVerticalTranslation);
+ // Let's calculate the position of the end based on the angle
+ float x = (polarToCartX(mCurrentAngle) * mArrowLength);
+ float y = (polarToCartY(mCurrentAngle) * mArrowLength);
+ Path arrowPath = calculatePath(x,y);
if (mShowProtection) {
- canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mProtectionPaint);
- canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mProtectionPaint);
+ canvas.drawPath(arrowPath, mProtectionPaint);
}
- canvas.drawLine(outsideX, 0, middleX, mHeight * 0.5f, mPaint);
- canvas.drawLine(middleX, mHeight * 0.5f, outsideX, mHeight, mPaint);
+ canvas.drawPath(arrowPath, mPaint);
canvas.restore();
}
@@ -213,117 +441,298 @@ public class NavigationBarEdgePanel extends View {
super.onLayout(changed, left, top, right, bottom);
// TODO: read the gesture length from the nav controller.
- mGestureLength = getWidth();
+ mMaxTranslation = getWidth() - mArrowPaddingEnd;
}
- private void setLegProgress(float progress) {
- mLegProgress = progress;
+ private void loadDimens() {
+ mArrowPaddingEnd = getContext().getResources().getDimensionPixelSize(
+ R.dimen.navigation_edge_panel_padding);
+ }
+
+ private void updateArrowDirection() {
+ // Both panels arrow point the same way
+ mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
invalidate();
}
- private float getLegProgress() {
- return mLegProgress;
+ private void loadColors(Context context) {
+ final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme);
+ final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme);
+ Context lightContext = new ContextThemeWrapper(context, dualToneLightTheme);
+ Context darkContext = new ContextThemeWrapper(context, dualToneDarkTheme);
+ mArrowColorLight = Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor);
+ mArrowColorDark = Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor);
+ mProtectionColorDark = mArrowColorLight;
+ mProtectionColorLight = mArrowColorDark;
+ updateIsDark(false /* animate */);
+ }
+
+ private void updateIsDark(boolean animate) {
+ // TODO: Maybe animate protection as well
+ mProtectionColor = mIsDark ? mProtectionColorDark : mProtectionColorLight;
+ mProtectionPaint.setColor(mProtectionColor);
+ mArrowColor = mIsDark ? mArrowColorDark : mArrowColorLight;
+ mArrowColorAnimator.cancel();
+ if (!animate) {
+ setCurrentArrowColor(mArrowColor);
+ } else {
+ mArrowStartColor = mCurrentArrowColor;
+ mArrowColorAnimator.start();
+ }
}
- private void setDragProgress(float dragProgress) {
- mDragProgress = dragProgress;
+ private void setCurrentArrowColor(int color) {
+ mCurrentArrowColor = color;
+ mPaint.setColor(color);
invalidate();
}
- private float getDragProgress() {
- return mDragProgress;
+ private float getStaticArrowWidth() {
+ return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
}
- private void hide() {
- animate().alpha(0f).setDuration(ANIM_DURATION_MS)
- .withEndAction(() -> setVisibility(GONE));
+ private float polarToCartX(float angleInDegrees) {
+ return (float) Math.cos(Math.toRadians(angleInDegrees));
}
- /**
- * Updates the UI based on the motion events passed in device co-ordinates
- */
- public void handleTouch(MotionEvent event) {
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN : {
- mDragSlopPassed = false;
- mEndAnimator.cancel();
- mLegAnimator.cancel();
- animate().cancel();
- setLegProgress(0f);
- setDragProgress(0f);
- mStartX = event.getX();
- setVisibility(VISIBLE);
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- handleNewSwipePoint(event.getX());
- break;
- }
- // Fall through
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL: {
- hide();
- break;
- }
+ private float polarToCartY(float angleInDegrees) {
+ return (float) Math.sin(Math.toRadians(angleInDegrees));
+ }
+
+ private Path calculatePath(float x, float y) {
+ if (!mArrowsPointLeft) {
+ x = -x;
+ }
+ float extent = 1.0f - mDisappearAmount;
+ x = x * extent;
+ y = y * extent;
+ mArrowPath.reset();
+ mArrowPath.moveTo(x, y);
+ mArrowPath.lineTo(0, 0);
+ mArrowPath.lineTo(x, -y);
+ return mArrowPath;
+ }
+
+ private float getCurrentAngle() {
+ return mCurrentAngle;
+ }
+
+ private float getCurrentTranslation() {
+ return mCurrentTranslation;
+ }
+
+ private void triggerBackAnimation() {
+
+ mVelocityTracker.computeCurrentVelocity(1000);
+ // Only do the extra translation if we're not already flinging
+ boolean doExtraTranslation = Math.abs(mVelocityTracker.getXVelocity()) < 1000;
+ if (doExtraTranslation) {
+ setDesiredTranslation(mDesiredTranslation + dp(16), true /* animate */);
+ }
+
+ // Let's also snap the angle a bit
+ if (mAngleOffset < -4) {
+ mAngleOffset = Math.max(-16, mAngleOffset - 16);
+ updateAngle(true /* animated */);
}
+
+ // Finally, after the translation, animate back and disappear the arrow
+ Runnable translationEnd = () -> {
+ setTriggerBack(false /* false */, true /* animate */);
+ mTranslationAnimation.setSpring(mTriggerBackSpring);
+ setDesiredTranslation(0, true /* animated */);
+ animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
+ .withEndAction(() -> setVisibility(GONE));
+ mArrowDisappearAnimation.start();
+ };
+ if (mTranslationAnimation.isRunning()) {
+ mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
+ float value,
+ float velocity) {
+ animation.removeEndListener(this);
+ if (!canceled) {
+ translationEnd.run();
+ }
+ }
+ });
+ } else {
+ translationEnd.run();
+ }
+
}
- private void handleNewSwipePoint(float x) {
- float dist = MathUtils.abs(x - mStartX);
+ private void resetOnDown() {
+ animate().cancel();
+ mAngleAnimation.cancel();
+ mTranslationAnimation.cancel();
+ mVerticalTranslationAnimation.cancel();
+ mArrowDisappearAnimation.cancel();
+ mAngleOffset = 0;
+ mTranslationAnimation.setSpring(mRegularTranslationSpring);
+ // Reset the arrow to the side
+ setTriggerBack(false /* triggerBack */, false /* animated */);
+ setDesiredTranslation(0, false /* animated */);
+ setCurrentTranslation(0);
+ mPreviousTouchTranslation = 0;
+ mTotalTouchDelta = 0;
+ setDesiredVerticalTransition(0, false /* animated */);
+ }
+
+ private void handleMoveEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ float touchTranslation = MathUtils.abs(x - mStartX);
+ float yOffset = y - mStartY;
+ float delta = touchTranslation - mPreviousTouchTranslation;
+ if (Math.abs(delta) > 0) {
+ if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
+ mTotalTouchDelta += delta;
+ } else {
+ mTotalTouchDelta = delta;
+ }
+ }
+ mPreviousTouchTranslation = touchTranslation;
// Apply a haptic on drag slop passed
- if (!mDragSlopPassed && dist > mSwipeThreshold) {
+ if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
mDragSlopPassed = true;
mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
- mLastSlopHapticTime = SystemClock.uptimeMillis();
+
+ // Let's show the arrow and animate it in!
+ mDisappearAmount = 0.0f;
setAlpha(1f);
+ // And animate it go to back by default!
+ setTriggerBack(true /* triggerBack */, true /* animated */);
}
- setDragProgress(MathUtils.constrainedMap(
- 0, 1.0f,
- 0, mGestureLength * TRACK_LENGTH_MULTIPLIER,
- dist));
+ // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
+ if (touchTranslation > mBaseTranslation) {
+ float diff = touchTranslation - mBaseTranslation;
+ float progress = MathUtils.saturate(diff / (mBaseTranslation * RUBBER_BAND_AMOUNT));
+ progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ * (mMaxTranslation - mBaseTranslation);
+ touchTranslation = mBaseTranslation + progress;
+ } else {
+ float diff = mBaseTranslation - touchTranslation;
+ float progress = MathUtils.saturate(diff / mBaseTranslation);
+ progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
+ * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
+ touchTranslation = mBaseTranslation - progress;
+ }
+ // By default we just assume the current direction is kept
+ boolean triggerBack = mTriggerBack;
- if (dist < mGestureLength) {
- float calculatedLegProgress = MathUtils.constrainedMap(
- 0f, POINTEDNESS_BEFORE_SNAP_RATIO,
- mGestureLength * START_POINTING_RATIO, mGestureLength,
- dist);
+ // First lets see if we had continuous motion in one direction for a while
+ if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
+ triggerBack = mTotalTouchDelta > 0;
+ }
- // Blend animated value with drag calculated value, allow the gesture to continue
- // while the animation is playing with jump cuts in the animation.
- setLegProgress(MathUtils.lerp(calculatedLegProgress, mLegProgress, mDragProgress));
+ // Then, let's see if our velocity tells us to change direction
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float xVelocity = mVelocityTracker.getXVelocity();
+ float yVelocity = mVelocityTracker.getYVelocity();
+ float velocity = MathUtils.mag(xVelocity, yVelocity);
+ mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
+ ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
+ if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
+ mAngleOffset *= -1;
+ }
- if (mGestureDetected) {
- mGestureDetected = false;
+ // Last if the direction in Y is bigger than X * 2 we also abort
+ if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+ triggerBack = false;
+ }
+ setTriggerBack(triggerBack, true /* animated */);
+
+ if (!mTriggerBack) {
+ touchTranslation = 0;
+ } else if (mIsLeftPanel && mArrowsPointLeft
+ || (!mIsLeftPanel && !mArrowsPointLeft)) {
+ // If we're on the left we should move less, because the arrow is facing the other
+ // direction
+ touchTranslation -= getStaticArrowWidth();
+ }
+ setDesiredTranslation(touchTranslation, true /* animated */);
+ updateAngle(true /* animated */);
+
+ float maxYOffset = getHeight() / 2.0f - mArrowLength;
+ float progress = MathUtils.constrain(
+ Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT),
+ 0, 1);
+ float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ * maxYOffset * Math.signum(yOffset);
+ setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+ }
- mLegAnimator.setFloatValues(POINTEDNESS_BEFORE_SNAP_RATIO);
- mLegAnimator.start();
+ private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
+ if (mDesiredVerticalTranslation != verticalTranslation) {
+ mDesiredVerticalTranslation = verticalTranslation;
+ if (!animated) {
+ setVerticalTranslation(verticalTranslation);
+ } else {
+ mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
}
- } else {
- if (!mGestureDetected) {
- // Prevent another haptic if it was just used
- if (SystemClock.uptimeMillis() - mLastSlopHapticTime > HAPTIC_TIMEOUT_MS) {
- performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
- }
- mGestureDetected = true;
+ invalidate();
+ }
+ }
+
+ private void setVerticalTranslation(float verticalTranslation) {
+ mVerticalTranslation = verticalTranslation;
+ invalidate();
+ }
+
+ private float getVerticalTranslation() {
+ return mVerticalTranslation;
+ }
- mLegAnimator.setFloatValues(1f);
- mLegAnimator.start();
+ private void setDesiredTranslation(float desiredTranslation, boolean animated) {
+ if (mDesiredTranslation != desiredTranslation) {
+ mDesiredTranslation = desiredTranslation;
+ if (!animated) {
+ setCurrentTranslation(desiredTranslation);
+ } else {
+ mTranslationAnimation.animateToFinalPosition(desiredTranslation);
}
}
}
- private float dp(float dp) {
- return mDensity * dp;
+ private void setCurrentTranslation(float currentTranslation) {
+ mCurrentTranslation = currentTranslation;
+ invalidate();
}
- /**
- * Adjust the rect to conform the the actual visible bounding box of the arrow.
- *
- * @param samplingRect the existing bounding box in screen coordinates, to be modified
- */
- public void adjustRectToBoundingBox(Rect samplingRect) {
- // TODO: adjust this. For now we take the complete rect
+ private void setTriggerBack(boolean triggerBack, boolean animated) {
+ if (mTriggerBack != triggerBack) {
+ mTriggerBack = triggerBack;
+ mAngleAnimation.cancel();
+ updateAngle(animated);
+ // Whenever the trigger back state changes the existing translation animation should be
+ // cancelled
+ mTranslationAnimation.cancel();
+ }
+ }
+
+ private void updateAngle(boolean animated) {
+ float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
+ if (newAngle != mDesiredAngle) {
+ if (!animated) {
+ setCurrentAngle(newAngle);
+ } else {
+ mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
+ mAngleAnimation.animateToFinalPosition(newAngle);
+ }
+ mDesiredAngle = newAngle;
+ }
+ }
+
+ private void setCurrentAngle(float currentAngle) {
+ mCurrentAngle = currentAngle;
+ invalidate();
+ }
+
+ private float dp(float dp) {
+ return mDensity * dp;
}
}