blob: 23573095e037e81999bd54c800581471aac2146f [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.phone;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.util.MathUtils;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
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;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.plugins.NavigationEdgeBackPlugin;
import com.android.systemui.statusbar.VibratorHelper;
public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin {
private static final String TAG = "NavigationBarEdgePanel";
private static final long COLOR_ANIMATION_DURATION_MS = 120;
private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80;
private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
/**
* The time required since the first vibration effect to automatically trigger a click
*/
private static final int GESTURE_DURATION_FOR_CLICK_MS = 400;
/**
* 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 = 4;
/**
* 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 vertical translation
*/
private static final int RUBBER_BAND_AMOUNT = 15;
/**
* The interpolator used to rubberband
*/
private static final Interpolator RUBBER_BAND_INTERPOLATOR
= new PathInterpolator(1.0f / 5.0f, 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 WindowManager mWindowManager;
private final VibratorHelper mVibratorHelper;
/**
* The paint the arrow is drawn with
*/
private final Paint mPaint = new Paint();
/**
* The paint the arrow protection is drawn with
*/
private final Paint mProtectionPaint;
private final float mDensity;
private final float mBaseTranslation;
private final float mArrowLength;
private final float mArrowThickness;
/**
* The minimum delta needed in movement for the arrow to change direction / stop triggering back
*/
private final float mMinDeltaForSwitch;
// The closest to y = 0 that the arrow will be displayed.
private int mMinArrowPosition;
// The amount the arrow is shifted to avoid the finger.
private int mFingerOffset;
private final float mSwipeThreshold;
private final Path mArrowPath = new Path();
private final Point mDisplaySize = new Point();
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;
private RegionSamplingHelper mRegionSamplingHelper;
private final Rect mSamplingRect = new Rect();
private WindowManager.LayoutParams mLayoutParams;
private int mLeftInset;
private int mRightInset;
/**
* 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 boolean mArrowsPointLeft;
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 long mVibrationTime;
private int mScreenSize;
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.setCurrentAngle(value);
}
@Override
public float getValue(NavigationBarEdgePanel object) {
return object.getCurrentAngle();
}
};
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.setVerticalTranslation(value);
}
@Override
public float getValue(NavigationBarEdgePanel object) {
return object.getVerticalTranslation();
}
};
private BackCallback mBackCallback;
public NavigationBarEdgePanel(Context context) {
super(context);
mWindowManager = context.getSystemService(WindowManager.class);
mVibratorHelper = Dependency.get(VibratorHelper.class);
mDensity = context.getResources().getDisplayMetrics().density;
mBaseTranslation = dp(BASE_TRANSLATION_DP);
mArrowLength = dp(ARROW_LENGTH_DP);
mArrowThickness = dp(ARROW_THICKNESS_DP);
mMinDeltaForSwitch = dp(32);
mPaint.setStrokeWidth(mArrowThickness);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.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(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(500)
.setDampingRatio(0.5f);
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);
updateArrowDirection();
mSwipeThreshold = context.getResources()
.getDimension(R.dimen.navigation_edge_action_drag_threshold);
setVisibility(GONE);
mRegionSamplingHelper = new RegionSamplingHelper(this,
new RegionSamplingHelper.SamplingCallback() {
@Override
public void onRegionDarknessChanged(boolean isRegionDark) {
setIsDark(!isRegionDark, true /* animate */);
}
@Override
public Rect getSampledRegion(View sampledView) {
return mSamplingRect;
}
});
mRegionSamplingHelper.setWindowVisible(true);
}
@Override
public void onDestroy() {
mWindowManager.removeView(this);
mRegionSamplingHelper.stop();
mRegionSamplingHelper = null;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
private void setIsDark(boolean isDark, boolean animate) {
mIsDark = isDark;
updateIsDark(animate);
}
private void setShowProtection(boolean showProtection) {
mShowProtection = showProtection;
invalidate();
}
@Override
public void setIsLeftPanel(boolean isLeftPanel) {
mIsLeftPanel = isLeftPanel;
mLayoutParams.gravity = mIsLeftPanel
? (Gravity.LEFT | Gravity.TOP)
: (Gravity.RIGHT | Gravity.TOP);
}
@Override
public void setInsets(int leftInset, int rightInset) {
mLeftInset = leftInset;
mRightInset = rightInset;
}
@Override
public void setDisplaySize(Point displaySize) {
mDisplaySize.set(displaySize.x, displaySize.y);
mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
}
@Override
public void setBackCallback(BackCallback callback) {
mBackCallback = callback;
}
@Override
public void setLayoutParams(WindowManager.LayoutParams layoutParams) {
mLayoutParams = layoutParams;
mWindowManager.addView(this, mLayoutParams);
}
/**
* Adjusts the sampling rect to conform to the actual visible bounding box of the arrow.
*/
private void adjustSamplingRectToBoundingBox() {
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 : mSamplingRect.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;
mSamplingRect.offset((int) left, (int) top);
mSamplingRect.set(mSamplingRect.left, mSamplingRect.top,
(int) (mSamplingRect.left + width),
(int) (mSamplingRect.top + height));
mRegionSamplingHelper.updateSamplingRect();
}
@Override
public void onMotionEvent(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);
updatePosition(event.getY());
mRegionSamplingHelper.start(mSamplingRect);
mWindowManager.updateViewLayout(this, mLayoutParams);
break;
case MotionEvent.ACTION_MOVE:
handleMoveEvent(event);
break;
case MotionEvent.ACTION_UP:
if (mTriggerBack) {
triggerBack();
} else {
cancelBack();
}
mRegionSamplingHelper.stop();
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
case MotionEvent.ACTION_CANCEL:
cancelBack();
mRegionSamplingHelper.stop();
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
}
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateArrowDirection();
loadDimens();
}
@Override
protected void onDraw(Canvas canvas) {
float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
canvas.save();
canvas.translate(
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.drawPath(arrowPath, mProtectionPaint);
}
canvas.drawPath(arrowPath, mPaint);
canvas.restore();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mMaxTranslation = getWidth() - mArrowPaddingEnd;
}
private void loadDimens() {
Resources res = getResources();
mArrowPaddingEnd = res.getDimensionPixelSize(R.dimen.navigation_edge_panel_padding);
mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y);
mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
}
private void updateArrowDirection() {
// Both panels arrow point the same way
mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
invalidate();
}
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 setCurrentArrowColor(int color) {
mCurrentArrowColor = color;
mPaint.setColor(color);
invalidate();
}
private float getStaticArrowWidth() {
return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
}
private float polarToCartX(float angleInDegrees) {
return (float) Math.cos(Math.toRadians(angleInDegrees));
}
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 = MathUtils.lerp(1.0f, 0.75f, 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 triggerBack() {
mBackCallback.triggerBack();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.computeCurrentVelocity(1000);
// Only do the extra translation if we're not already flinging
boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500;
if (isSlow
|| SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
mVibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK);
}
// Let's also snap the angle a bit
if (mAngleOffset > -4) {
mAngleOffset = Math.max(-8, mAngleOffset - 8);
updateAngle(true /* animated */);
}
// Finally, after the translation, animate back and disappear the arrow
Runnable translationEnd = () -> {
// let's snap it back
mAngleOffset = Math.max(0, mAngleOffset + 8);
updateAngle(true /* animated */);
mTranslationAnimation.setSpring(mTriggerBackSpring);
// Translate the arrow back a bit to make for a nice transition
setDesiredTranslation(mDesiredTranslation - dp(32), 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 cancelBack() {
mBackCallback.cancelBack();
if (mTranslationAnimation.isRunning()) {
mTranslationAnimation.addEndListener(mSetGoneEndListener);
} else {
setVisibility(GONE);
}
}
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);
updateAngle(false /* animate */);
mPreviousTouchTranslation = 0;
mTotalTouchDelta = 0;
mVibrationTime = 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 && touchTranslation > mSwipeThreshold) {
mDragSlopPassed = true;
mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
mVibrationTime = 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 */);
}
// 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 / (mScreenSize - mBaseTranslation));
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;
// First lets see if we had continuous motion in one direction for a while
if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
triggerBack = mTotalTouchDelta > 0;
}
// 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;
}
// 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 */);
updateSamplingRect();
}
private void updatePosition(float touchY) {
float position = touchY - mFingerOffset;
position = Math.max(position, mMinArrowPosition);
position -= mLayoutParams.height / 2.0f;
mLayoutParams.y = MathUtils.constrain((int) position, 0, mDisplaySize.y);
updateSamplingRect();
}
private void updateSamplingRect() {
int top = mLayoutParams.y;
int left = mIsLeftPanel ? mLeftInset : mDisplaySize.x - mRightInset - mLayoutParams.width;
int right = left + mLayoutParams.width;
int bottom = top + mLayoutParams.height;
mSamplingRect.set(left, top, right, bottom);
adjustSamplingRectToBoundingBox();
}
private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
if (mDesiredVerticalTranslation != verticalTranslation) {
mDesiredVerticalTranslation = verticalTranslation;
if (!animated) {
setVerticalTranslation(verticalTranslation);
} else {
mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
}
invalidate();
}
}
private void setVerticalTranslation(float verticalTranslation) {
mVerticalTranslation = verticalTranslation;
invalidate();
}
private float getVerticalTranslation() {
return mVerticalTranslation;
}
private void setDesiredTranslation(float desiredTranslation, boolean animated) {
if (mDesiredTranslation != desiredTranslation) {
mDesiredTranslation = desiredTranslation;
if (!animated) {
setCurrentTranslation(desiredTranslation);
} else {
mTranslationAnimation.animateToFinalPosition(desiredTranslation);
}
}
}
private void setCurrentTranslation(float currentTranslation) {
mCurrentTranslation = currentTranslation;
invalidate();
}
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;
}
}