Merge "Adds back gesture recognition to Sandbox." into ub-launcher3-master
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
index 294e46e..d8c25bd 100644
--- a/quickstep/res/layout/back_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
@@ -16,9 +16,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layerType="software"
android:background="@color/back_gesture_tutorial_background_color">
- <!--The layout is rendered on the software layer to avoid b/136158117-->
<ImageView
android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
new file mode 100644
index 0000000..3583676
--- /dev/null
+++ b/quickstep/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<resources>
+ <color name="back_arrow_color_dark">#99000000</color>
+</resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 3fe91a3..5c2e992 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -25,6 +25,7 @@
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import java.util.Optional;
@@ -79,21 +80,33 @@
mHandCoachingAnimation.stop();
}
- void onGestureDetected() {
- hideHandCoachingAnimation();
-
- if (mTutorialStep == TutorialStep.CONFIRM) {
+ void onGestureAttempted(BackGestureResult result) {
+ if (mTutorialStep == TutorialStep.CONFIRM
+ && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+ || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
mFragment.closeTutorial();
return;
}
- if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
- mFragment.changeController(TutorialStep.ENGAGED,
- TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+ if (!mTutorialTypeInfo.isPresent()) {
return;
}
- mFragment.changeController(TutorialStep.CONFIRM);
+ switch (mTutorialTypeInfo.get().getTutorialType()) {
+ case RIGHT_EDGE_BACK_NAVIGATION:
+ if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+ hideHandCoachingAnimation();
+ mFragment.changeController(
+ TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+ }
+ break;
+ case LEFT_EDGE_BACK_NAVIGATION:
+ if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
+ hideHandCoachingAnimation();
+ mFragment.changeController(TutorialStep.CONFIRM);
+ }
+ break;
+ }
}
abstract Optional<Integer> getTitleStringId();
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index 54408ce..593b695 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -17,21 +17,26 @@
import android.content.ActivityNotFoundException;
import android.content.Intent;
+import android.graphics.Insets;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowInsets;
+import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import java.net.URISyntaxException;
import java.util.Optional;
/** Shows the Back gesture interactive tutorial. */
-public class BackGestureTutorialFragment extends Fragment {
+public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
private static final String LOG_TAG = "TutorialFragment";
private static final String KEY_TUTORIAL_STEP = "tutorialStep";
@@ -47,6 +52,7 @@
private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
private View mRootView;
private BackGestureTutorialHandAnimation mHandCoachingAnimation;
+ private EdgeBackGestureHandler mEdgeBackGestureHandler;
public static BackGestureTutorialFragment newInstance(
TutorialStep tutorialStep, TutorialType tutorialType) {
@@ -64,17 +70,25 @@
Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+ mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
+ mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
}
@Override
public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
container, /* attachToRoot= */ false);
mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
.setOnClickListener(this::onCloseButtonClicked);
+ mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
+ Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
+ mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
+ return insets;
+ });
+ mRootView.setOnTouchListener(mEdgeBackGestureHandler);
mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
return mRootView;
@@ -92,6 +106,14 @@
mHandCoachingAnimation.stop();
}
+ void onAttachedToWindow() {
+ mEdgeBackGestureHandler.setIsEnabled(true);
+ }
+
+ void onDetachedFromWindow() {
+ mEdgeBackGestureHandler.setIsEnabled(false);
+ }
+
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
@@ -125,10 +147,9 @@
this.mTutorialType = tutorialType;
}
- void onBackPressed() {
- if (mTutorialController.isPresent()) {
- mTutorialController.get().onGestureDetected();
- }
+ @Override
+ public void onBackGestureAttempted(BackGestureResult result) {
+ mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
}
void closeTutorial() {
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
new file mode 100644
index 0000000..04cd2f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2020 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.quickstep.interaction;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.launcher3.ResourceUtils;
+
+/**
+ * Utility class to handle edge swipes for back gestures.
+ *
+ * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
+ */
+public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener {
+
+ private static final String TAG = "EdgeBackGestureHandler";
+ private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
+ "gestures.back_timeout", 250);
+
+ private final Context mContext;
+
+ private final Point mDisplaySize = new Point();
+ private final int mDisplayId;
+
+ // The edge width where touch down is allowed
+ private int mEdgeWidth;
+ // The bottom gesture area height
+ private int mBottomGestureHeight;
+ // The slop to distinguish between horizontal and vertical motion
+ private final float mTouchSlop;
+ // Duration after which we consider the event as longpress.
+ private final int mLongPressTimeout;
+
+ private final PointF mDownPoint = new PointF();
+ private boolean mThresholdCrossed = false;
+ private boolean mAllowGesture = false;
+ private boolean mIsEnabled;
+ private int mLeftInset;
+ private int mRightInset;
+
+ private EdgeBackGesturePanel mEdgeBackPanel;
+ private BackGestureAttemptCallback mGestureCallback;
+
+ private final EdgeBackGesturePanel.BackCallback mBackCallback =
+ new EdgeBackGesturePanel.BackCallback() {
+ @Override
+ public void triggerBack() {
+ if (mGestureCallback != null) {
+ mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+ ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
+ : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
+ }
+ }
+
+ @Override
+ public void cancelBack() {
+ if (mGestureCallback != null) {
+ mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+ ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
+ : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
+ }
+ }
+ };
+
+ EdgeBackGestureHandler(Context context) {
+ final Resources res = context.getResources();
+ mContext = context;
+ mDisplayId = context.getDisplay() == null
+ ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();
+
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
+ ViewConfiguration.getLongPressTimeout());
+
+ mBottomGestureHeight =
+ ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
+ mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+ }
+
+ void setIsEnabled(boolean isEnabled) {
+ if (isEnabled == mIsEnabled) {
+ return;
+ }
+ mIsEnabled = isEnabled;
+
+ if (mEdgeBackPanel != null) {
+ mEdgeBackPanel.onDestroy();
+ mEdgeBackPanel = null;
+ }
+
+ if (!mIsEnabled) {
+ mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ } else {
+ updateDisplaySize();
+ mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
+ new Handler(Looper.getMainLooper()));
+
+ // Add a nav bar panel window.
+ mEdgeBackPanel = new EdgeBackGesturePanel(mContext);
+ mEdgeBackPanel.setBackCallback(mBackCallback);
+ mEdgeBackPanel.setLayoutParams(createLayoutParams());
+ updateDisplaySize();
+ }
+ }
+
+ void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
+ mGestureCallback = callback;
+ }
+
+ private WindowManager.LayoutParams createLayoutParams() {
+ Resources resources = mContext.getResources();
+ WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
+ ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
+ ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources),
+ LayoutParams.TYPE_APPLICATION_PANEL,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+ PixelFormat.TRANSLUCENT);
+ layoutParams.setTitle(TAG + mDisplayId);
+ layoutParams.windowAnimations = 0;
+ layoutParams.setFitInsetsTypes(0 /* types */);
+ return layoutParams;
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ if (mIsEnabled) {
+ onMotionEvent(motionEvent);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isWithinTouchRegion(int x, int y) {
+ // Disallow if too far from the edge
+ if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
+ return false;
+ }
+
+ // Disallow if we are in the bottom gesture area
+ if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void cancelGesture(MotionEvent ev) {
+ // Send action cancel to reset all the touch events
+ mAllowGesture = false;
+ MotionEvent cancelEv = MotionEvent.obtain(ev);
+ cancelEv.setAction(MotionEvent.ACTION_CANCEL);
+ mEdgeBackPanel.onMotionEvent(cancelEv);
+ cancelEv.recycle();
+ }
+
+ private void onMotionEvent(MotionEvent ev) {
+ int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
+ mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+ if (mAllowGesture) {
+ mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
+ mEdgeBackPanel.onMotionEvent(ev);
+
+ mDownPoint.set(ev.getX(), ev.getY());
+ mThresholdCrossed = false;
+ }
+ } else if (mAllowGesture) {
+ if (!mThresholdCrossed) {
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ // We do not support multi touch for back gesture
+ cancelGesture(ev);
+ return;
+ } else if (action == MotionEvent.ACTION_MOVE) {
+ if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
+ cancelGesture(ev);
+ return;
+ }
+ float dx = Math.abs(ev.getX() - mDownPoint.x);
+ float dy = Math.abs(ev.getY() - mDownPoint.y);
+ if (dy > dx && dy > mTouchSlop) {
+ cancelGesture(ev);
+ return;
+
+ } else if (dx > dy && dx > mTouchSlop) {
+ mThresholdCrossed = true;
+ }
+ }
+
+ }
+
+ // forward touch
+ mEdgeBackPanel.onMotionEvent(ev);
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ if (!mAllowGesture && mGestureCallback != null) {
+ mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) { }
+
+ @Override
+ public void onDisplayRemoved(int displayId) { }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == mDisplayId) {
+ updateDisplaySize();
+ }
+ }
+
+ private void updateDisplaySize() {
+ mContext.getDisplay().getRealSize(mDisplaySize);
+ if (mEdgeBackPanel != null) {
+ mEdgeBackPanel.setDisplaySize(mDisplaySize);
+ }
+ }
+
+ void setInsets(int leftInset, int rightInset) {
+ mLeftInset = leftInset;
+ mRightInset = rightInset;
+ }
+
+ enum BackGestureResult {
+ UNKNOWN,
+ BACK_COMPLETED_FROM_LEFT,
+ BACK_COMPLETED_FROM_RIGHT,
+ BACK_CANCELLED_FROM_LEFT,
+ BACK_CANCELLED_FROM_RIGHT,
+ BACK_NOT_STARTED,
+ }
+
+ /** Callback to let the UI react to attempted back gestures. */
+ interface BackGestureAttemptCallback {
+ /** Called whenever any touch is completed. */
+ void onBackGestureAttempted(BackGestureResult result);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
new file mode 100644
index 0000000..34eeafc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -0,0 +1,701 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+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.os.SystemClock;
+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.math.MathUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
+
+/** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */
+public class EdgeBackGesturePanel extends View {
+
+ private static final String LOG_TAG = "EdgeBackGesturePanel";
+
+ 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 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 BackCallback mBackCallback;
+
+ /**
+ * The paint the arrow is drawn with
+ */
+ private final Paint mPaint = new Paint();
+
+ 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 mArrowDisappearAnimation;
+ private final SpringForce mRegularTranslationSpring;
+ private final SpringForce mTriggerBackSpring;
+
+ private VelocityTracker mVelocityTracker;
+ private int mArrowPaddingEnd;
+ private WindowManager.LayoutParams mLayoutParams;
+
+ /**
+ * 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 float mDisappearAmount;
+ private long mVibrationTime;
+ private int mScreenSize;
+
+ private final 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<EdgeBackGesturePanel> CURRENT_ANGLE =
+ new FloatPropertyCompat<EdgeBackGesturePanel>("currentAngle") {
+ @Override
+ public void setValue(EdgeBackGesturePanel object, float value) {
+ object.setCurrentAngle(value);
+ }
+
+ @Override
+ public float getValue(EdgeBackGesturePanel object) {
+ return object.getCurrentAngle();
+ }
+ };
+
+ private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_TRANSLATION =
+ new FloatPropertyCompat<EdgeBackGesturePanel>("currentTranslation") {
+ @Override
+ public void setValue(EdgeBackGesturePanel object, float value) {
+ object.setCurrentTranslation(value);
+ }
+
+ @Override
+ public float getValue(EdgeBackGesturePanel object) {
+ return object.getCurrentTranslation();
+ }
+ };
+
+ private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_VERTICAL_TRANSLATION =
+ new FloatPropertyCompat<EdgeBackGesturePanel>("verticalTranslation") {
+
+ @Override
+ public void setValue(EdgeBackGesturePanel object, float value) {
+ object.setVerticalTranslation(value);
+ }
+
+ @Override
+ public float getValue(EdgeBackGesturePanel object) {
+ return object.getVerticalTranslation();
+ }
+ };
+
+ public EdgeBackGesturePanel(Context context) {
+ super(context);
+
+ mWindowManager = context.getSystemService(WindowManager.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);
+
+ 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));
+ mPaint.setColor(context.getColor(R.color.back_arrow_color_dark));
+ loadDimens();
+ updateArrowDirection();
+
+ mSwipeThreshold = ResourceUtils.getDimenByName(
+ "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */);
+ setVisibility(GONE);
+ }
+
+ void onDestroy() {
+ mWindowManager.removeView(this);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ @SuppressLint("RtlHardcoded")
+ void setIsLeftPanel(boolean isLeftPanel) {
+ mIsLeftPanel = isLeftPanel;
+ mLayoutParams.gravity = mIsLeftPanel
+ ? (Gravity.LEFT | Gravity.TOP)
+ : (Gravity.RIGHT | Gravity.TOP);
+ }
+
+ boolean getIsLeftPanel() {
+ return mIsLeftPanel;
+ }
+
+ void setDisplaySize(Point displaySize) {
+ mDisplaySize.set(displaySize.x, displaySize.y);
+ mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
+ }
+
+ void setBackCallback(BackCallback callback) {
+ mBackCallback = callback;
+ }
+
+ void setLayoutParams(WindowManager.LayoutParams layoutParams) {
+ mLayoutParams = layoutParams;
+ mWindowManager.addView(this, mLayoutParams);
+ }
+
+ private float getCurrentAngle() {
+ return mCurrentAngle;
+ }
+
+ private float getCurrentTranslation() {
+ return mCurrentTranslation;
+ }
+
+ 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());
+ mWindowManager.updateViewLayout(this, mLayoutParams);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ handleMoveEvent(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mTriggerBack) {
+ triggerBack();
+ } else {
+ cancelBack();
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ cancelBack();
+ 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);
+
+ 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 = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8);
+ mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64);
+ mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48);
+ }
+
+ private void updateArrowDirection() {
+ // Both panels arrow point the same way
+ mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+ 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 = 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 static float lerp(float start, float stop, float amount) {
+ return start + (stop - start) * amount;
+ }
+
+ private void triggerBack() {
+ if (mBackCallback != null) {
+ 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) {
+ VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.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() {
+ if (mBackCallback != null) {
+ 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 = Math.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;
+ VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+ 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.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1);
+ progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ * (mMaxTranslation - mBaseTranslation);
+ touchTranslation = mBaseTranslation + progress;
+ } else {
+ float diff = mBaseTranslation - touchTranslation;
+ float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1);
+ 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 = (float) Math.hypot(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.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1);
+ float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+ * maxYOffset * Math.signum(yOffset);
+ setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+ }
+
+ private void updatePosition(float touchY) {
+ float position = touchY - mFingerOffset;
+ position = Math.max(position, mMinArrowPosition);
+ position -= mLayoutParams.height / 2.0f;
+ mLayoutParams.y = MathUtils.clamp((int) position, 0, mDisplaySize.y);
+ }
+
+ 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;
+ }
+
+ /** Callback to let the gesture handler react to the detected back gestures. */
+ interface BackCallback {
+ /** Indicates that a Back gesture was recognized. */
+ void triggerBack();
+
+ /** Indicates that the gesture was cancelled. */
+ void cancelBack();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 8081ad7..4815366 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -30,12 +30,11 @@
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
import java.util.List;
-import java.util.Optional;
/** Shows the gesture interactive sandbox in full screen mode. */
public class GestureSandboxActivity extends FragmentActivity {
- Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+ private BackGestureTutorialFragment mFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -43,10 +42,10 @@
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.back_gesture_tutorial_activity);
- mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
- TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+ mFragment = BackGestureTutorialFragment.newInstance(
+ TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
getSupportFragmentManager().beginTransaction()
- .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+ .add(R.id.back_gesture_tutorial_fragment_container, mFragment)
.commit();
}
@@ -54,6 +53,13 @@
public void onAttachedToWindow() {
super.onAttachedToWindow();
disableSystemGestures();
+ mFragment.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mFragment.onDetachedFromWindow();
}
@Override
@@ -64,13 +70,6 @@
}
}
- @Override
- public void onBackPressed() {
- if (mFragment.isPresent()) {
- mFragment.get().onBackPressed();
- }
- }
-
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 04741a1..b0defd4 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -39,7 +39,7 @@
public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
new MainThreadInitializedObject<>(VibratorWrapper::new);
- private static final VibrationEffect EFFECT_CLICK =
+ public static final VibrationEffect EFFECT_CLICK =
createPredefined(VibrationEffect.EFFECT_CLICK);
/**