Merge "Refactor QuickStepController into Gestures"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 814324e..99cc3a3 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -36,7 +36,7 @@
public boolean onInterceptTouchEvent(MotionEvent event);
- public void setBarState(boolean vertical, boolean isRtl);
+ public void setBarState(boolean isRtl, int navBarPosition);
public void onDraw(Canvas canvas);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index 4eca6bb..119f01a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -263,6 +263,16 @@
}
}
+ public void setTranslation(int x, int y, int z) {
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ final View view = mViews.get(i);
+ view.setTranslationX(x);
+ view.setTranslationY(y);
+ view.setTranslationZ(z);
+ }
+ }
+
public ArrayList<View> getViews() {
return mViews;
}
@@ -276,6 +286,11 @@
if (mImageDrawable != null) {
mImageDrawable.setCallback(mCurrentView);
}
+ if (mCurrentView != null) {
+ mCurrentView.setTranslationX(0);
+ mCurrentView.setTranslationY(0);
+ mCurrentView.setTranslationZ(0);
+ }
}
public void setVertical(boolean vertical) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java
new file mode 100644
index 0000000..1002f9e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 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 static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+
+import android.annotation.NonNull;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * A back action when triggered will execute a back command
+ */
+public class NavigationBackAction extends NavigationGestureAction {
+
+ private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback";
+ private static final String BACK_AFTER_END_PROP =
+ "quickstepcontroller_homegoesbackwhenend";
+ private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled";
+ private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60;
+ private static final long BACK_GESTURE_POLL_TIMEOUT = 1000;
+
+ private final Handler mHandler = new Handler();
+
+ private final Runnable mExecuteBackRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (isEnabled() && canPerformAction()) {
+ performBack();
+ mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT);
+ }
+ }
+ };
+
+ public NavigationBackAction(@NonNull NavigationBarView navigationBarView,
+ @NonNull OverviewProxyService service) {
+ super(navigationBarView, service);
+ }
+
+ @Override
+ public int requiresTouchDownHitTarget() {
+ return HIT_TARGET_HOME;
+ }
+
+ @Override
+ public boolean requiresDragWithHitTarget() {
+ return true;
+ }
+
+ @Override
+ public boolean canPerformAction() {
+ return mProxySender.getBackButtonAlpha() > 0;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return swipeHomeGoBackGestureEnabled();
+ }
+
+ @Override
+ protected void onGestureStart(MotionEvent event) {
+ if (!QuickStepController.shouldhideBackButton(getContext())) {
+ mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */,
+ BACK_BUTTON_FADE_OUT_ALPHA);
+ }
+ mHandler.removeCallbacks(mExecuteBackRunnable);
+ if (!shouldExecuteBackOnUp()) {
+ performBack();
+ mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT);
+ }
+ }
+
+ @Override
+ protected void onGestureEnd() {
+ mHandler.removeCallbacks(mExecuteBackRunnable);
+ if (!QuickStepController.shouldhideBackButton(getContext())) {
+ mNavigationBarView.getBackButton().setAlpha(
+ mProxySender.getBackButtonAlpha(), true /* animate */);
+ }
+ if (shouldExecuteBackOnUp()) {
+ performBack();
+ }
+ }
+
+ private void performBack() {
+ sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
+ sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
+ mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+
+ private boolean swipeHomeGoBackGestureEnabled() {
+ return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED)
+ && getGlobalBoolean(PULL_HOME_GO_BACK_PROP);
+ }
+
+ private boolean shouldExecuteBackOnUp() {
+ return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED)
+ && getGlobalBoolean(BACK_AFTER_END_PROP);
+ }
+
+ private void sendEvent(int action, int code) {
+ long when = SystemClock.uptimeMillis();
+ final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
+ 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
+ KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+ InputDevice.SOURCE_KEYBOARD);
+ InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 6728f08..2c3c27f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -38,9 +38,11 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
+import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.MotionEvent;
@@ -49,6 +51,7 @@
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.inputmethod.InputMethodManager;
@@ -143,6 +146,10 @@
private RecentsOnboarding mRecentsOnboarding;
private NotificationPanelView mPanelView;
+ private QuickScrubAction mQuickScrubAction;
+ private QuickStepAction mQuickStepAction;
+ private NavigationBackAction mBackAction;
+
/**
* Helper that is responsible for showing the right toast when a disallowed activity operation
* occurred. In pinned mode, we show instructions on how to break out of this mode, whilst in
@@ -299,6 +306,10 @@
mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton);
mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup);
mDeadZone = new DeadZone(this);
+
+ mQuickScrubAction = new QuickScrubAction(this, mOverviewProxyService);
+ mQuickStepAction = new QuickStepAction(this, mOverviewProxyService);
+ mBackAction = new NavigationBackAction(this, mOverviewProxyService);
}
public BarTransitions getBarTransitions() {
@@ -313,6 +324,8 @@
mPanelView = panel;
if (mGestureHelper instanceof QuickStepController) {
((QuickStepController) mGestureHelper).setComponents(this);
+ ((QuickStepController) mGestureHelper).setGestureActions(mQuickStepAction,
+ null /* swipeDownAction*/, mBackAction, mQuickScrubAction);
}
}
@@ -756,24 +769,6 @@
mRecentsOnboarding.hide(true);
}
- /**
- * @return the button at the given {@param x} and {@param y}.
- */
- ButtonDispatcher getButtonAtPosition(int x, int y) {
- for (int i = 0; i < mButtonDispatchers.size(); i++) {
- ButtonDispatcher button = mButtonDispatchers.valueAt(i);
- View buttonView = button.getCurrentView();
- if (buttonView != null) {
- buttonView.getHitRect(mTmpRect);
- offsetDescendantRectToMyCoords(buttonView, mTmpRect);
- if (mTmpRect.contains(x, y)) {
- return button;
- }
- }
- }
- return null;
- }
-
@Override
public void onFinishInflate() {
mNavigationInflaterView = findViewById(R.id.navigation_inflater);
@@ -908,7 +903,13 @@
private void updateTaskSwitchHelper() {
if (mGestureHelper == null) return;
boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
- mGestureHelper.setBarState(mVertical, isRtl);
+ int navBarPos = 0;
+ try {
+ navBarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get nav bar position.", e);
+ }
+ mGestureHelper.setBarState(isRtl, navBarPos);
}
@Override
@@ -1112,6 +1113,14 @@
mContextualButtonGroup.dump(pw);
if (mGestureHelper != null) {
+ pw.println("Navigation Gesture Actions {");
+ pw.print(" "); pw.println("QuickScrub Enabled=" + mQuickScrubAction.isEnabled());
+ pw.print(" "); pw.println("QuickScrub Active=" + mQuickScrubAction.isActive());
+ pw.print(" "); pw.println("QuickStep Enabled=" + mQuickStepAction.isEnabled());
+ pw.print(" "); pw.println("QuickStep Active=" + mQuickStepAction.isActive());
+ pw.print(" "); pw.println("Back Gesture Enabled=" + mBackAction.isEnabled());
+ pw.print(" "); pw.println("Back Gesture Active=" + mBackAction.isActive());
+ pw.println("}");
mGestureHelper.dump(pw);
}
mRecentsOnboarding.dump(pw);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java
new file mode 100644
index 0000000..593bfae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2018 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 static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import android.view.WindowManagerPolicyConstants;
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * A gesture action that would be triggered and reassigned by {@link QuickStepController}
+ */
+public abstract class NavigationGestureAction {
+
+ protected final NavigationBarView mNavigationBarView;
+ protected final OverviewProxyService mProxySender;
+
+ protected int mNavigationBarPosition;
+ protected boolean mDragHorizontalPositive;
+ protected boolean mDragVerticalPositive;
+ private boolean mIsActive;
+
+ public NavigationGestureAction(@NonNull NavigationBarView navigationBarView,
+ @NonNull OverviewProxyService service) {
+ mNavigationBarView = navigationBarView;
+ mProxySender = service;
+ }
+
+ /**
+ * Pass event that the state of the bar (such as rotation) has changed
+ * @param changed if rotation or drag positive direction (such as ltr) has changed
+ * @param navBarPos position of navigation bar
+ * @param dragHorPositive direction of positive horizontal drag, could change with ltr changes
+ * @param dragVerPositive direction of positive vertical drag, could change with ltr changes
+ */
+ public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive,
+ boolean dragVerPositive) {
+ mNavigationBarPosition = navBarPos;
+ mDragHorizontalPositive = dragHorPositive;
+ mDragVerticalPositive = dragVerPositive;
+ }
+
+ /**
+ * Resets the state of the action. Called when touch down occurs over the Navigation Bar.
+ */
+ public void reset() {
+ mIsActive = false;
+ }
+
+ /**
+ * Start the gesture and the action will be active
+ * @param event the event that caused the gesture
+ */
+ public void startGesture(MotionEvent event) {
+ mIsActive = true;
+ onGestureStart(event);
+ }
+
+ /**
+ * Gesture has ended with action cancel or up and this action will not be active
+ */
+ public void endGesture() {
+ mIsActive = false;
+ onGestureEnd();
+ }
+
+ /**
+ * If the action is currently active based on the gesture that triggered it. Only one action
+ * can occur at a time
+ * @return whether or not if this action has been triggered
+ */
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ /**
+ * @return whether or not this action can run if notification shade is shown
+ */
+ public boolean canRunWhenNotificationsShowing() {
+ return true;
+ }
+
+ /**
+ * @return whether or not this action triggers when starting a gesture from a certain hit target
+ * If {@link HIT_TARGET_NONE} is specified then action does not need to be triggered by button
+ */
+ public int requiresTouchDownHitTarget() {
+ return HIT_TARGET_NONE;
+ }
+
+ /**
+ * @return whether or not to move the button that started gesture over with user input drag
+ */
+ public boolean requiresDragWithHitTarget() {
+ return false;
+ }
+
+ /**
+ * Tell if the action is able to execute. Note that {@link #isEnabled()} must be true for this
+ * to be checked. The difference between this and {@link #isEnabled()} is that this dependent
+ * on the state of the navigation bar
+ * @return true if action can execute after gesture activates based on current states
+ */
+ public boolean canPerformAction() {
+ return true;
+ }
+
+ /**
+ * Tell if action is enabled. Compared to {@link #canPerformAction()} this is based on settings
+ * if the action is disabled for a particular gesture. For example a back action can be enabled
+ * however if there is nothing to back to then {@link #canPerformAction()} should return false.
+ * In this way if the action requires {@link #requiresDragWithHitTarget()} then if enabled, the
+ * button can be dragged with a large dampening factor during the gesture but will not activate
+ * the action.
+ * @return true if this action is enabled and can run
+ */
+ public abstract boolean isEnabled();
+
+ protected void onDarkIntensityChange(float intensity) {
+ }
+
+ protected void onDraw(Canvas canvas) {
+ }
+
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * When gesture starts, this will run to execute the action
+ * @param event the event that triggered the gesture
+ */
+ protected abstract void onGestureStart(MotionEvent event);
+
+ /**
+ * Channels motion move events to the action to track the user inputs
+ * @param x the x position
+ * @param y the y position
+ */
+ public void onGestureMove(int x, int y) {
+ }
+
+ /**
+ * When gesture ends, this will run from action up or cancel
+ */
+ protected void onGestureEnd() {
+ }
+
+ protected Context getContext() {
+ return mNavigationBarView.getContext();
+ }
+
+ protected boolean isNavBarVertical() {
+ return mNavigationBarPosition == NAV_BAR_LEFT || mNavigationBarPosition == NAV_BAR_RIGHT;
+ }
+
+ protected boolean getGlobalBoolean(@NonNull String key) {
+ return QuickStepController.getBoolGlobalSetting(getContext(), key);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java
new file mode 100644
index 0000000..c64e124
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2018 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 static com.android.systemui.Interpolators.ALPHA_IN;
+import static com.android.systemui.Interpolators.ALPHA_OUT;
+import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.os.RemoteException;
+
+import android.util.FloatProperty;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.recents.utilities.Utilities;
+
+/**
+ * QuickScrub action to send to launcher to start quickscrub gesture
+ */
+public class QuickScrubAction extends NavigationGestureAction {
+ private static final String TAG = "QuickScrubAction";
+
+ private static final float TRACK_SCALE = 0.95f;
+ private static final float GRADIENT_WIDTH = .75f;
+ private static final int ANIM_IN_DURATION_MS = 150;
+ private static final int ANIM_OUT_DURATION_MS = 134;
+
+ private AnimatorSet mTrackAnimator;
+ private View mCurrentNavigationBarView;
+
+ private float mTrackScale = TRACK_SCALE;
+ private float mTrackAlpha;
+ private float mHighlightCenter;
+ private float mDarkIntensity;
+
+ private final int mTrackThickness;
+ private final int mTrackEndPadding;
+ private final Paint mTrackPaint = new Paint();
+ private final Rect mTrackRect = new Rect();
+
+ private final FloatProperty<QuickScrubAction> mTrackAlphaProperty =
+ new FloatProperty<QuickScrubAction>("TrackAlpha") {
+ @Override
+ public void setValue(QuickScrubAction action, float alpha) {
+ mTrackAlpha = alpha;
+ mNavigationBarView.invalidate();
+ }
+
+ @Override
+ public Float get(QuickScrubAction action) {
+ return mTrackAlpha;
+ }
+ };
+
+ private final FloatProperty<QuickScrubAction> mTrackScaleProperty =
+ new FloatProperty<QuickScrubAction>("TrackScale") {
+ @Override
+ public void setValue(QuickScrubAction action, float scale) {
+ mTrackScale = scale;
+ mNavigationBarView.invalidate();
+ }
+
+ @Override
+ public Float get(QuickScrubAction action) {
+ return mTrackScale;
+ }
+ };
+
+ private final FloatProperty<QuickScrubAction> mNavBarAlphaProperty =
+ new FloatProperty<QuickScrubAction>("NavBarAlpha") {
+ @Override
+ public void setValue(QuickScrubAction action, float alpha) {
+ if (mCurrentNavigationBarView != null) {
+ mCurrentNavigationBarView.setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public Float get(QuickScrubAction action) {
+ if (mCurrentNavigationBarView != null) {
+ return mCurrentNavigationBarView.getAlpha();
+ }
+ return 1f;
+ }
+ };
+
+ private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCurrentNavigationBarView != null) {
+ mCurrentNavigationBarView.setAlpha(1f);
+ }
+ mCurrentNavigationBarView = null;
+ updateHighlight();
+ }
+ };
+
+ public QuickScrubAction(@NonNull NavigationBarView navigationBarView,
+ @NonNull OverviewProxyService service) {
+ super(navigationBarView, service);
+ mTrackPaint.setAntiAlias(true);
+ mTrackPaint.setDither(true);
+
+ final Resources res = navigationBarView.getResources();
+ mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness);
+ mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
+ }
+
+ @Override
+ public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive,
+ boolean dragVerPositive) {
+ super.setBarState(changed, navBarPos, dragHorPositive, dragVerPositive);
+ if (changed && isActive()) {
+ // End quickscrub if the state changes mid-transition
+ endQuickScrub(false /* animate */);
+ }
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+
+ // End any existing quickscrub animations before starting the new transition
+ if (mTrackAnimator != null) {
+ mTrackAnimator.end();
+ mTrackAnimator = null;
+ }
+ mCurrentNavigationBarView = mNavigationBarView.getCurrentView();
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int paddingLeft = mNavigationBarView.getPaddingLeft();
+ final int paddingTop = mNavigationBarView.getPaddingTop();
+ final int paddingRight = mNavigationBarView.getPaddingRight();
+ final int paddingBottom = mNavigationBarView.getPaddingBottom();
+ final int width = (right - left) - paddingRight - paddingLeft;
+ final int height = (bottom - top) - paddingBottom - paddingTop;
+ final int x1, x2, y1, y2;
+ if (isNavBarVertical()) {
+ x1 = (width - mTrackThickness) / 2 + paddingLeft;
+ x2 = x1 + mTrackThickness;
+ y1 = paddingTop + mTrackEndPadding;
+ y2 = y1 + height - 2 * mTrackEndPadding;
+ } else {
+ y1 = (height - mTrackThickness) / 2 + paddingTop;
+ y2 = y1 + mTrackThickness;
+ x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding;
+ x2 = x1 + width - 2 * mTrackEndPadding;
+ }
+ mTrackRect.set(x1, y1, x2, y2);
+ }
+
+ @Override
+ public void onDarkIntensityChange(float intensity) {
+ mDarkIntensity = intensity;
+ updateHighlight();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (!isEnabled()) {
+ return;
+ }
+ mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha));
+
+ // Scale the track, but apply the inverse scale from the nav bar
+ final float radius = mTrackRect.height() / 2;
+ canvas.save();
+ float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right);
+ canvas.translate(translate, 0);
+ canvas.scale(mTrackScale / mNavigationBarView.getScaleX(),
+ 1f / mNavigationBarView.getScaleY(),
+ mTrackRect.centerX(), mTrackRect.centerY());
+ canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top,
+ mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint);
+ canvas.restore();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mNavigationBarView.isQuickScrubEnabled();
+ }
+
+ @Override
+ protected void onGestureStart(MotionEvent event) {
+ updateHighlight();
+ ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f),
+ PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f));
+ trackAnimator.setInterpolator(ALPHA_IN);
+ trackAnimator.setDuration(ANIM_IN_DURATION_MS);
+ ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f);
+ navBarAnimator.setInterpolator(ALPHA_OUT);
+ navBarAnimator.setDuration(ANIM_OUT_DURATION_MS);
+ mTrackAnimator = new AnimatorSet();
+ mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
+ mTrackAnimator.start();
+
+ // Disable slippery for quick scrub to not cancel outside the nav bar
+ mNavigationBarView.updateSlippery();
+
+ try {
+ mProxySender.getProxy().onQuickScrubStart();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub Start");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send start of quick scrub.", e);
+ }
+ mProxySender.notifyQuickScrubStarted();
+ }
+
+ @Override
+ public void onGestureMove(int x, int y) {
+ int trackSize, offset;
+ if (isNavBarVertical()) {
+ trackSize = mTrackRect.height();
+ offset = y - mTrackRect.top;
+ } else {
+ offset = x - mTrackRect.left;
+ trackSize = mTrackRect.width();
+ }
+ if (!mDragHorizontalPositive || !mDragVerticalPositive) {
+ offset -= isNavBarVertical() ? mTrackRect.height() : mTrackRect.width();
+ }
+ float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
+ try {
+ mProxySender.getProxy().onQuickScrubProgress(scrubFraction);
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send progress of quick scrub.", e);
+ }
+ mHighlightCenter = x;
+ mNavigationBarView.invalidate();
+ }
+
+ @Override
+ protected void onGestureEnd() {
+ endQuickScrub(true /* animate */);
+ }
+
+ private void endQuickScrub(boolean animate) {
+ animateEnd();
+ try {
+ mProxySender.getProxy().onQuickScrubEnd();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub End");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send end of quick scrub.", e);
+ }
+ if (!animate) {
+ if (mTrackAnimator != null) {
+ mTrackAnimator.end();
+ mTrackAnimator = null;
+ }
+ }
+ }
+
+ private void updateHighlight() {
+ if (mTrackRect.isEmpty()) {
+ return;
+ }
+ int colorBase, colorGrad;
+ if (mDarkIntensity > 0.5f) {
+ colorBase = getContext().getColor(R.color.quick_step_track_background_background_dark);
+ colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_dark);
+ } else {
+ colorBase = getContext().getColor(R.color.quick_step_track_background_background_light);
+ colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_light);
+ }
+ final RadialGradient mHighlight = new RadialGradient(0, mTrackRect.height() / 2,
+ mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase,
+ Shader.TileMode.CLAMP);
+ mTrackPaint.setShader(mHighlight);
+ }
+
+ private void animateEnd() {
+ if (mTrackAnimator != null) {
+ mTrackAnimator.cancel();
+ }
+
+ ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
+ PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f),
+ PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE));
+ trackAnimator.setInterpolator(ALPHA_OUT);
+ trackAnimator.setDuration(ANIM_OUT_DURATION_MS);
+ ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f);
+ navBarAnimator.setInterpolator(ALPHA_IN);
+ navBarAnimator.setDuration(ANIM_IN_DURATION_MS);
+ mTrackAnimator = new AnimatorSet();
+ mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
+ mTrackAnimator.addListener(mQuickScrubEndListener);
+ mTrackAnimator.start();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java
new file mode 100644
index 0000000..b18b79e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 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 static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * QuickStep action to send to launcher to start overview
+ */
+public class QuickStepAction extends NavigationGestureAction {
+ private static final String TAG = "QuickStepAction";
+
+ public QuickStepAction(@NonNull NavigationBarView navigationBarView,
+ @NonNull OverviewProxyService service) {
+ super(navigationBarView, service);
+ }
+
+ @Override
+ public boolean canRunWhenNotificationsShowing() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mNavigationBarView.isQuickStepSwipeUpEnabled();
+ }
+
+ @Override
+ public void onGestureStart(MotionEvent event) {
+ try {
+ mProxySender.getProxy().onQuickStep(event);
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Step Start");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send quick step started.", e);
+ }
+ mProxySender.notifyQuickStepStarted();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
index 3980126..c03800e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
@@ -18,187 +18,96 @@
import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
-import static com.android.systemui.Interpolators.ALPHA_IN;
-import static com.android.systemui.Interpolators.ALPHA_OUT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE;
import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.RadialGradient;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.hardware.input.InputManager;
-import android.os.Handler;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.provider.Settings;
-import android.util.FloatProperty;
import android.util.Log;
-import android.util.Slog;
-import android.view.HapticFeedbackConstants;
-import android.view.InputDevice;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewPropertyAnimator;
-import android.view.WindowManagerGlobal;
+
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
-import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.R;
-import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.shared.recents.IOverviewProxy;
-import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.system.NavigationBarCompat;
+
import java.io.PrintWriter;
/**
* Class to detect gestures on the navigation bar and implement quick scrub.
+ * Note that the variables in this class horizontal and vertical represents horizontal always
+ * aligned with along the navigation bar).
*/
public class QuickStepController implements GestureHelper {
private static final String TAG = "QuickStepController";
- private static final int ANIM_IN_DURATION_MS = 150;
- private static final int ANIM_OUT_DURATION_MS = 134;
- private static final float TRACK_SCALE = 0.95f;
- private static final float GRADIENT_WIDTH = .75f;
/** Experiment to swipe home button left to execute a back key press */
- private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback";
private static final String HIDE_BACK_BUTTON_PROP = "quickstepcontroller_hideback";
- private static final String BACK_AFTER_END_PROP
- = "quickstepcontroller_homegoesbackwhenend";
- private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled";
- private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60;
private static final long BACK_BUTTON_FADE_IN_ALPHA = 150;
- private static final long BACK_GESTURE_POLL_TIMEOUT = 1000;
/** When the home-swipe-back gesture is disallowed, make it harder to pull */
private static final float DISALLOW_GESTURE_DAMPING_FACTOR = 0.16f;
+ private static final int ACTION_SWIPE_UP_INDEX = 0;
+ private static final int ACTION_SWIPE_DOWN_INDEX = 1;
+ private static final int ACTION_SWIPE_LEFT_INDEX = 2;
+ private static final int ACTION_SWIPE_RIGHT_INDEX = 3;
+ private static final int MAX_GESTURES = 4;
+
private NavigationBarView mNavigationBarView;
- private boolean mQuickScrubActive;
private boolean mAllowGestureDetection;
- private boolean mBackGestureActive;
- private boolean mCanPerformBack;
- private boolean mQuickStepStarted;
private boolean mNotificationsVisibleOnDown;
private int mTouchDownX;
private int mTouchDownY;
- private boolean mDragPositive;
- private boolean mIsVertical;
+ private boolean mDragHPositive;
+ private boolean mDragVPositive;
private boolean mIsRTL;
- private float mTrackAlpha;
- private float mTrackScale = TRACK_SCALE;
+ private int mNavBarPosition;
private float mDarkIntensity;
- private RadialGradient mHighlight;
- private float mHighlightCenter;
- private AnimatorSet mTrackAnimator;
- private ViewPropertyAnimator mHomeAnimator;
+ private ViewPropertyAnimator mDragBtnAnimator;
private ButtonDispatcher mHitTarget;
- private View mCurrentNavigationBarView;
private boolean mIsInScreenPinning;
+ private boolean mGestureHorizontalDragsButton;
+ private boolean mGestureVerticalDragsButton;
+ private boolean mGestureTrackPositive;
- private final Handler mHandler = new Handler();
- private final Rect mTrackRect = new Rect();
+ private NavigationGestureAction mCurrentAction;
+ private NavigationGestureAction[] mGestureActions = new NavigationGestureAction[MAX_GESTURES];
+
private final OverviewProxyService mOverviewEventSender;
- private final int mTrackThickness;
- private final int mTrackEndPadding;
private final int mHomeBackGestureDragLimit;
private final Context mContext;
private final StatusBar mStatusBar;
private final Matrix mTransformGlobalMatrix = new Matrix();
private final Matrix mTransformLocalMatrix = new Matrix();
- private final Paint mTrackPaint = new Paint();
-
- private final FloatProperty<QuickStepController> mTrackAlphaProperty =
- new FloatProperty<QuickStepController>("TrackAlpha") {
- @Override
- public void setValue(QuickStepController controller, float alpha) {
- mTrackAlpha = alpha;
- mNavigationBarView.invalidate();
- }
-
- @Override
- public Float get(QuickStepController controller) {
- return mTrackAlpha;
- }
- };
-
- private final FloatProperty<QuickStepController> mTrackScaleProperty =
- new FloatProperty<QuickStepController>("TrackScale") {
- @Override
- public void setValue(QuickStepController controller, float scale) {
- mTrackScale = scale;
- mNavigationBarView.invalidate();
- }
-
- @Override
- public Float get(QuickStepController controller) {
- return mTrackScale;
- }
- };
-
- private final FloatProperty<QuickStepController> mNavBarAlphaProperty =
- new FloatProperty<QuickStepController>("NavBarAlpha") {
- @Override
- public void setValue(QuickStepController controller, float alpha) {
- if (mCurrentNavigationBarView != null) {
- mCurrentNavigationBarView.setAlpha(alpha);
- }
- }
-
- @Override
- public Float get(QuickStepController controller) {
- if (mCurrentNavigationBarView != null) {
- return mCurrentNavigationBarView.getAlpha();
- }
- return 1f;
- }
- };
-
- private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- resetQuickScrub();
- }
- };
-
- private final Runnable mExecuteBackRunnable = new Runnable() {
- @Override
- public void run() {
- if (canPerformHomeBackGesture()) {
- performBack();
- mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT);
- }
- }
- };
public QuickStepController(Context context) {
final Resources res = context.getResources();
mContext = context;
mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
mOverviewEventSender = Dependency.get(OverviewProxyService.class);
- mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness);
- mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
mHomeBackGestureDragLimit =
res.getDimensionPixelSize(R.dimen.nav_home_back_gesture_drag_limit);
- mTrackPaint.setAntiAlias(true);
- mTrackPaint.setDither(true);
}
public void setComponents(NavigationBarView navigationBarView) {
@@ -210,6 +119,31 @@
}
/**
+ * Set each gesture an action. After set the gestures triggered will run the actions attached.
+ * @param swipeUpAction action after swiping up
+ * @param swipeDownAction action after swiping down
+ * @param swipeLeftAction action after swiping left
+ * @param swipeRightAction action after swiping right
+ */
+ public void setGestureActions(@Nullable NavigationGestureAction swipeUpAction,
+ @Nullable NavigationGestureAction swipeDownAction,
+ @Nullable NavigationGestureAction swipeLeftAction,
+ @Nullable NavigationGestureAction swipeRightAction) {
+ mGestureActions[ACTION_SWIPE_UP_INDEX] = swipeUpAction;
+ mGestureActions[ACTION_SWIPE_DOWN_INDEX] = swipeDownAction;
+ mGestureActions[ACTION_SWIPE_LEFT_INDEX] = swipeLeftAction;
+ mGestureActions[ACTION_SWIPE_RIGHT_INDEX] = swipeRightAction;
+
+ // Set the current state to all actions
+ for (NavigationGestureAction action: mGestureActions) {
+ if (action != null) {
+ action.setBarState(true, mNavBarPosition, mDragHPositive, mDragVPositive);
+ action.onDarkIntensityChange(mDarkIntensity);
+ }
+ }
+ }
+
+ /**
* @return true if we want to intercept touch events for quick scrub and prevent proxying the
* event to the overview service.
*/
@@ -242,8 +176,10 @@
private boolean handleTouchEvent(MotionEvent event) {
final boolean deadZoneConsumed =
mNavigationBarView.getDownHitTarget() == HIT_TARGET_DEAD_ZONE;
- if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled()
- && !mNavigationBarView.isQuickStepSwipeUpEnabled())) {
+
+ // Requires proxy and an active gesture or able to perform any gesture to continue
+ if (mOverviewEventSender.getProxy() == null
+ || (mCurrentAction == null && !canPerformAnyAction())) {
return deadZoneConsumed;
}
mNavigationBarView.requestUnbufferedDispatch(event);
@@ -255,33 +191,45 @@
int y = (int) event.getY();
mIsInScreenPinning = mNavigationBarView.inScreenPinning();
- // End any existing quickscrub animations before starting the new transition
- if (mTrackAnimator != null) {
- mTrackAnimator.end();
- mTrackAnimator = null;
+ for (NavigationGestureAction gestureAction: mGestureActions) {
+ if (gestureAction != null) {
+ gestureAction.reset();
+ }
}
- mCurrentNavigationBarView = mNavigationBarView.getCurrentView();
- mHitTarget = mNavigationBarView.getButtonAtPosition(x, y);
+ // Valid buttons to drag over
+ switch (mNavigationBarView.getDownHitTarget()) {
+ case HIT_TARGET_BACK:
+ mHitTarget = mNavigationBarView.getBackButton();
+ break;
+ case HIT_TARGET_HOME:
+ mHitTarget = mNavigationBarView.getHomeButton();
+ break;
+ case HIT_TARGET_OVERVIEW:
+ mHitTarget = mNavigationBarView.getRecentsButton();
+ break;
+ default:
+ mHitTarget = null;
+ break;
+ }
if (mHitTarget != null) {
// Pre-emptively delay the touch feedback for the button that we just touched
mHitTarget.setDelayTouchFeedback(true);
}
mTouchDownX = x;
mTouchDownY = y;
+ mGestureHorizontalDragsButton = false;
+ mGestureVerticalDragsButton = false;
mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX);
mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX);
mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix);
mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix);
- mQuickStepStarted = false;
- mBackGestureActive = false;
mAllowGestureDetection = true;
mNotificationsVisibleOnDown = !mNavigationBarView.isNotificationsFullyCollapsed();
- mCanPerformBack = canPerformHomeBackGesture();
break;
}
case MotionEvent.ACTION_MOVE: {
- if (mQuickStepStarted || !mAllowGestureDetection){
+ if (!mAllowGestureDetection) {
break;
}
int x = (int) event.getX();
@@ -289,108 +237,132 @@
int xDiff = Math.abs(x - mTouchDownX);
int yDiff = Math.abs(y - mTouchDownY);
- boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop;
- int pos, touchDown, offset, trackSize;
+ boolean exceededSwipeHorizontalTouchSlop, exceededSwipeVerticalTouchSlop;
+ int posH, touchDownH, posV, touchDownV;
- if (mIsVertical) {
- exceededScrubTouchSlop =
+ if (isNavBarVertical()) {
+ exceededSwipeHorizontalTouchSlop =
yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff;
- exceededSwipeUpTouchSlop =
+ exceededSwipeVerticalTouchSlop =
xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff;
- pos = y;
- touchDown = mTouchDownY;
- offset = pos - mTrackRect.top;
- trackSize = mTrackRect.height();
+ posH = y;
+ touchDownH = mTouchDownY;
+ posV = x;
+ touchDownV = mTouchDownX;
} else {
- exceededScrubTouchSlop =
+ exceededSwipeHorizontalTouchSlop =
xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff;
- exceededSwipeUpTouchSlop =
+ exceededSwipeVerticalTouchSlop =
yDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && yDiff > xDiff;
- pos = x;
- touchDown = mTouchDownX;
- offset = pos - mTrackRect.left;
- trackSize = mTrackRect.width();
- }
- // Decide to start quickstep if dragging away from the navigation bar, otherwise in
- // the parallel direction, decide to start quickscrub. Only one may run.
- if (!mBackGestureActive && !mQuickScrubActive && exceededSwipeUpTouchSlop) {
- if (mNavigationBarView.isQuickStepSwipeUpEnabled()
- && !mNotificationsVisibleOnDown) {
- startQuickStep(event);
- }
- break;
+ posH = x;
+ touchDownH = mTouchDownX;
+ posV = y;
+ touchDownV = mTouchDownY;
}
- // Do not handle quick scrub if disabled
- if (!mNavigationBarView.isQuickScrubEnabled()) {
- break;
- }
-
- if (!mDragPositive) {
- offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
- }
-
- final boolean allowDrag = !mDragPositive
- ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
- float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
- if (!mQuickScrubActive && !mBackGestureActive && exceededScrubTouchSlop) {
- // Passing the drag slop then touch slop will start quick step
- if (allowDrag) {
- startQuickScrub();
- } else if (swipeHomeGoBackGestureEnabled(mContext)
- && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME
- && mDragPositive ? pos < touchDown : pos > touchDown) {
- startBackGesture();
- }
- }
-
- if (mQuickScrubActive && (mDragPositive && offset >= 0
- || !mDragPositive && offset <= 0)) {
- try {
- mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+ if (mCurrentAction != null) {
+ // Gesture started, provide positions to the current action
+ mCurrentAction.onGestureMove(x, y);
+ } else {
+ // Detect gesture and try to execute an action, only one can run at a time
+ if (exceededSwipeVerticalTouchSlop) {
+ if (mDragVPositive ? (posV < touchDownV) : (posV > touchDownV)) {
+ // Swiping up gesture
+ tryToStartGesture(mGestureActions[ACTION_SWIPE_UP_INDEX],
+ false /* alignedWithNavBar */, false /* positiveDirection */,
+ event);
+ } else {
+ // Swiping down gesture
+ tryToStartGesture(mGestureActions[ACTION_SWIPE_DOWN_INDEX],
+ false /* alignedWithNavBar */, true /* positiveDirection */,
+ event);
}
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send progress of quick scrub.", e);
- }
- mHighlightCenter = x;
- mNavigationBarView.invalidate();
- } else if (mBackGestureActive) {
- int diff = pos - touchDown;
- // If dragging the incorrect direction after starting back gesture or unable
- // to execute back functionality, then move home but dampen its distance
- if (!mCanPerformBack || (mDragPositive ? diff > 0 : diff < 0)) {
- diff *= DISALLOW_GESTURE_DAMPING_FACTOR;
- } if (Math.abs(diff) > mHomeBackGestureDragLimit) {
- // Once the user drags the home button past a certain limit, the distance
- // will lessen as the home button dampens showing that it was pulled too far
- float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit)
- * DISALLOW_GESTURE_DAMPING_FACTOR;
- diff = (int)(distanceAfterDragLimit + mHomeBackGestureDragLimit);
- if (mDragPositive) {
- diff *= -1;
+ } else if (exceededSwipeHorizontalTouchSlop) {
+ if (mDragHPositive ? (posH < touchDownH) : (posH > touchDownH)) {
+ // Swiping left (ltr) gesture
+ tryToStartGesture(mGestureActions[ACTION_SWIPE_LEFT_INDEX],
+ true /* alignedWithNavBar */, false /* positiveDirection */,
+ event);
+ } else {
+ // Swiping right (ltr) gesture
+ tryToStartGesture(mGestureActions[ACTION_SWIPE_RIGHT_INDEX],
+ true /* alignedWithNavBar */, true /* positiveDirection */,
+ event);
}
}
- moveHomeButton(diff);
}
+
+ handleDragHitTarget(mGestureHorizontalDragsButton ? posH : posV,
+ mGestureHorizontalDragsButton ? touchDownH : touchDownV);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
- endQuickScrub(true /* animate */);
- endBackGesture();
+ if (mCurrentAction != null) {
+ mCurrentAction.endGesture();
+ mCurrentAction = null;
+ }
+
+ // Return the hit target back to its original position
+ if (mHitTarget != null) {
+ final View button = mHitTarget.getCurrentView();
+ if (mGestureHorizontalDragsButton || mGestureVerticalDragsButton) {
+ mDragBtnAnimator = button.animate().setDuration(BACK_BUTTON_FADE_IN_ALPHA)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ if (mGestureVerticalDragsButton ^ isNavBarVertical()) {
+ mDragBtnAnimator.translationY(0);
+ } else {
+ mDragBtnAnimator.translationX(0);
+ }
+ mDragBtnAnimator.start();
+ }
+ }
break;
}
if (shouldProxyEvents(action)) {
proxyMotionEvents(event);
}
- return mBackGestureActive || mQuickScrubActive || mQuickStepStarted || deadZoneConsumed;
+ return mCurrentAction != null || deadZoneConsumed;
+ }
+
+ private void handleDragHitTarget(int position, int touchDown) {
+ // Drag the hit target if gesture action requires it
+ if (mHitTarget != null && (mGestureVerticalDragsButton || mGestureHorizontalDragsButton)) {
+ final View button = mHitTarget.getCurrentView();
+ if (mDragBtnAnimator != null) {
+ mDragBtnAnimator.cancel();
+ mDragBtnAnimator = null;
+ }
+
+ int diff = position - touchDown;
+ // If dragging the incorrect direction after starting gesture or unable to
+ // execute tried action, then move the button but dampen its distance
+ if (mCurrentAction == null || (mGestureTrackPositive ? diff < 0 : diff > 0)) {
+ diff *= DISALLOW_GESTURE_DAMPING_FACTOR;
+ } else if (Math.abs(diff) > mHomeBackGestureDragLimit) {
+ // Once the user drags the button past a certain limit, the distance will
+ // lessen as the button dampens that it was pulled too far
+ float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit)
+ * DISALLOW_GESTURE_DAMPING_FACTOR;
+ diff = (int) (distanceAfterDragLimit + mHomeBackGestureDragLimit);
+ if (!mGestureTrackPositive) {
+ diff *= -1;
+ }
+ }
+ if (mGestureVerticalDragsButton ^ isNavBarVertical()) {
+ button.setTranslationY(diff);
+ } else {
+ button.setTranslationX(diff);
+ }
+ }
}
private boolean shouldProxyEvents(int action) {
- if (!mBackGestureActive && !mQuickScrubActive && !mIsInScreenPinning) {
+ final boolean actionValid = (mCurrentAction == null
+ || (mGestureActions[ACTION_SWIPE_UP_INDEX] != null
+ && mGestureActions[ACTION_SWIPE_UP_INDEX].isActive()));
+ if (actionValid && !mIsInScreenPinning) {
// Allow down, cancel and up events, move and other events are passed if notifications
// are not showing and disabled gestures (such as long press) are not executed
switch (action) {
@@ -407,46 +379,18 @@
@Override
public void onDraw(Canvas canvas) {
- if (!mNavigationBarView.isQuickScrubEnabled()) {
- return;
+ if (mCurrentAction != null) {
+ mCurrentAction.onDraw(canvas);
}
- mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha));
-
- // Scale the track, but apply the inverse scale from the nav bar
- final float radius = mTrackRect.height() / 2;
- canvas.save();
- float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right);
- canvas.translate(translate, 0);
- canvas.scale(mTrackScale / mNavigationBarView.getScaleX(),
- 1f / mNavigationBarView.getScaleY(),
- mTrackRect.centerX(), mTrackRect.centerY());
- canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top,
- mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint);
- canvas.restore();
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int paddingLeft = mNavigationBarView.getPaddingLeft();
- final int paddingTop = mNavigationBarView.getPaddingTop();
- final int paddingRight = mNavigationBarView.getPaddingRight();
- final int paddingBottom = mNavigationBarView.getPaddingBottom();
- final int width = (right - left) - paddingRight - paddingLeft;
- final int height = (bottom - top) - paddingBottom - paddingTop;
- final int x1, x2, y1, y2;
- if (mIsVertical) {
- x1 = (width - mTrackThickness) / 2 + paddingLeft;
- x2 = x1 + mTrackThickness;
- y1 = paddingTop + mTrackEndPadding;
- y2 = y1 + height - 2 * mTrackEndPadding;
- } else {
- y1 = (height - mTrackThickness) / 2 + paddingTop;
- y2 = y1 + mTrackThickness;
- x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding;
- x2 = x1 + width - 2 * mTrackEndPadding;
+ for (NavigationGestureAction action: mGestureActions) {
+ if (action != null) {
+ action.onLayout(changed, left, top, right, bottom);
+ }
}
- mTrackRect.set(x1, y1, x2, y2);
- updateHighlight();
}
@Override
@@ -456,119 +400,104 @@
// When in quick scrub, invalidate gradient if changing intensity from black to white and
// vice-versa
- if (mNavigationBarView.isQuickScrubEnabled()
+ if (mCurrentAction != null && mNavigationBarView.isQuickScrubEnabled()
&& Math.round(intensity) != Math.round(oldIntensity)) {
- updateHighlight();
+ mCurrentAction.onDarkIntensityChange(mDarkIntensity);
}
mNavigationBarView.invalidate();
}
@Override
- public void setBarState(boolean isVertical, boolean isRTL) {
- final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL);
- if (changed) {
- // End quickscrub if the state changes mid-transition
- endQuickScrub(false /* animate */);
- }
- mIsVertical = isVertical;
+ public void setBarState(boolean isRTL, int navBarPosition) {
+ final boolean changed = (mIsRTL != isRTL) || (mNavBarPosition != navBarPosition);
mIsRTL = isRTL;
- try {
- int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
- mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
- if (isRTL) {
- mDragPositive = !mDragPositive;
+ mNavBarPosition = navBarPosition;
+
+ // Determine the drag directions depending on location of nav bar
+ switch (navBarPosition) {
+ case NAV_BAR_LEFT:
+ mDragHPositive = !isRTL;
+ mDragVPositive = false;
+ break;
+ case NAV_BAR_RIGHT:
+ mDragHPositive = isRTL;
+ mDragVPositive = true;
+ break;
+ case NAV_BAR_BOTTOM:
+ mDragHPositive = !isRTL;
+ mDragVPositive = true;
+ break;
+ }
+
+ for (NavigationGestureAction action: mGestureActions) {
+ if (action != null) {
+ action.setBarState(changed, mNavBarPosition, mDragHPositive, mDragVPositive);
}
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to get nav bar position.", e);
}
}
@Override
public void onNavigationButtonLongPress(View v) {
mAllowGestureDetection = false;
- mHandler.removeCallbacksAndMessages(null);
}
@Override
public void dump(PrintWriter pw) {
pw.println("QuickStepController {");
- pw.print(" "); pw.println("mQuickScrubActive=" + mQuickScrubActive);
- pw.print(" "); pw.println("mQuickStepStarted=" + mQuickStepStarted);
pw.print(" "); pw.println("mAllowGestureDetection=" + mAllowGestureDetection);
- pw.print(" "); pw.println("mBackGestureActive=" + mBackGestureActive);
- pw.print(" "); pw.println("mCanPerformBack=" + mCanPerformBack);
pw.print(" "); pw.println("mNotificationsVisibleOnDown=" + mNotificationsVisibleOnDown);
- pw.print(" "); pw.println("mIsVertical=" + mIsVertical);
+ pw.print(" "); pw.println("mNavBarPosition=" + mNavBarPosition);
pw.print(" "); pw.println("mIsRTL=" + mIsRTL);
pw.print(" "); pw.println("mIsInScreenPinning=" + mIsInScreenPinning);
pw.println("}");
}
- private void startQuickStep(MotionEvent event) {
- if (mIsInScreenPinning) {
- mNavigationBarView.showPinningEscapeToast();
- mAllowGestureDetection = false;
- return;
- }
-
- mQuickStepStarted = true;
- event.transform(mTransformGlobalMatrix);
- try {
- mOverviewEventSender.getProxy().onQuickStep(event);
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Step Start");
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send quick step started.", e);
- } finally {
- event.transform(mTransformLocalMatrix);
- }
- mOverviewEventSender.notifyQuickStepStarted();
- mHandler.removeCallbacksAndMessages(null);
-
- if (mHitTarget != null) {
- mHitTarget.abortCurrentGesture();
- }
-
- if (mQuickScrubActive) {
- animateEnd();
- }
+ public NavigationGestureAction getCurrentAction() {
+ return mCurrentAction;
}
- private void startQuickScrub() {
+ private void tryToStartGesture(NavigationGestureAction action, boolean alignedWithNavBar,
+ boolean positiveDirection, MotionEvent event) {
+ if (action == null) {
+ return;
+ }
if (mIsInScreenPinning) {
mNavigationBarView.showPinningEscapeToast();
mAllowGestureDetection = false;
return;
}
- if (!mQuickScrubActive) {
- updateHighlight();
- mQuickScrubActive = true;
- ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f),
- PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f));
- trackAnimator.setInterpolator(ALPHA_IN);
- trackAnimator.setDuration(ANIM_IN_DURATION_MS);
- ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f);
- navBarAnimator.setInterpolator(ALPHA_OUT);
- navBarAnimator.setDuration(ANIM_OUT_DURATION_MS);
- mTrackAnimator = new AnimatorSet();
- mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
- mTrackAnimator.start();
-
- // Disable slippery for quick scrub to not cancel outside the nav bar
- mNavigationBarView.updateSlippery();
-
- try {
- mOverviewEventSender.getProxy().onQuickScrubStart();
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Scrub Start");
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send start of quick scrub.", e);
+ // Start new action from gesture if is able to start and depending on notifications
+ // visibility and starting touch down target. If the action is enabled, then also check if
+ // can perform the action so that if action requires the button to be dragged, then the
+ // gesture will have a large dampening factor and prevent action from running.
+ final boolean validHitTarget = action.requiresTouchDownHitTarget() == HIT_TARGET_NONE
+ || action.requiresTouchDownHitTarget() == mNavigationBarView.getDownHitTarget();
+ if (mCurrentAction == null && validHitTarget && action.isEnabled()
+ && (!mNotificationsVisibleOnDown || action.canRunWhenNotificationsShowing())) {
+ if (action.canPerformAction()) {
+ mCurrentAction = action;
+ event.transform(mTransformGlobalMatrix);
+ action.startGesture(event);
+ event.transform(mTransformLocalMatrix);
}
- mOverviewEventSender.notifyQuickScrubStarted();
+
+ // Handle direction of the hit target drag from the axis that started the gesture
+ if (action.requiresDragWithHitTarget()) {
+ if (alignedWithNavBar) {
+ mGestureHorizontalDragsButton = true;
+ mGestureVerticalDragsButton = false;
+ if (positiveDirection) {
+ mGestureTrackPositive = mDragHPositive;
+ }
+ } else {
+ mGestureVerticalDragsButton = true;
+ mGestureHorizontalDragsButton = false;
+ if (positiveDirection) {
+ mGestureTrackPositive = mDragVPositive;
+ }
+ }
+ }
if (mHitTarget != null) {
mHitTarget.abortCurrentGesture();
@@ -576,148 +505,13 @@
}
}
- private void endQuickScrub(boolean animate) {
- if (mQuickScrubActive) {
- animateEnd();
- try {
- mOverviewEventSender.getProxy().onQuickScrubEnd();
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Scrub End");
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send end of quick scrub.", e);
+ private boolean canPerformAnyAction() {
+ for (NavigationGestureAction action: mGestureActions) {
+ if (action != null && action.isEnabled()) {
+ return true;
}
}
- if (!animate) {
- if (mTrackAnimator != null) {
- mTrackAnimator.end();
- mTrackAnimator = null;
- }
- }
- }
-
- private void startBackGesture() {
- if (!mBackGestureActive) {
- mBackGestureActive = true;
- mNavigationBarView.getHomeButton().abortCurrentGesture();
- final boolean runBackMidGesture = !shouldExecuteBackOnUp(mContext);
- if (mCanPerformBack) {
- if (!shouldhideBackButton(mContext)) {
- mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */,
- BACK_BUTTON_FADE_OUT_ALPHA);
- }
- if (runBackMidGesture) {
- performBack();
- }
- }
- mHandler.removeCallbacks(mExecuteBackRunnable);
- if (runBackMidGesture) {
- mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT);
- }
- }
- }
-
- private void endBackGesture() {
- if (mBackGestureActive) {
- mHandler.removeCallbacks(mExecuteBackRunnable);
- mHomeAnimator = mNavigationBarView.getHomeButton().getCurrentView()
- .animate()
- .setDuration(BACK_BUTTON_FADE_IN_ALPHA)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- if (mIsVertical) {
- mHomeAnimator.translationY(0);
- } else {
- mHomeAnimator.translationX(0);
- }
- mHomeAnimator.start();
- if (!shouldhideBackButton(mContext)) {
- mNavigationBarView.getBackButton().setAlpha(
- mOverviewEventSender.getBackButtonAlpha(), true /* animate */);
- }
- if (shouldExecuteBackOnUp(mContext)) {
- performBack();
- }
- }
- }
-
- private void animateEnd() {
- if (mTrackAnimator != null) {
- mTrackAnimator.cancel();
- }
-
- ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
- PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f),
- PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE));
- trackAnimator.setInterpolator(ALPHA_OUT);
- trackAnimator.setDuration(ANIM_OUT_DURATION_MS);
- ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f);
- navBarAnimator.setInterpolator(ALPHA_IN);
- navBarAnimator.setDuration(ANIM_IN_DURATION_MS);
- mTrackAnimator = new AnimatorSet();
- mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
- mTrackAnimator.addListener(mQuickScrubEndListener);
- mTrackAnimator.start();
- }
-
- private void resetQuickScrub() {
- mQuickScrubActive = false;
- mAllowGestureDetection = false;
- if (mCurrentNavigationBarView != null) {
- mCurrentNavigationBarView.setAlpha(1f);
- }
- mCurrentNavigationBarView = null;
- updateHighlight();
- }
-
- private void moveHomeButton(float pos) {
- if (mHomeAnimator != null) {
- mHomeAnimator.cancel();
- mHomeAnimator = null;
- }
- final View homeButton = mNavigationBarView.getHomeButton().getCurrentView();
- if (mIsVertical) {
- homeButton.setTranslationY(pos);
- } else {
- homeButton.setTranslationX(pos);
- }
- }
-
- private void updateHighlight() {
- if (mTrackRect.isEmpty()) {
- return;
- }
- int colorBase, colorGrad;
- if (mDarkIntensity > 0.5f) {
- colorBase = mContext.getColor(R.color.quick_step_track_background_background_dark);
- colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_dark);
- } else {
- colorBase = mContext.getColor(R.color.quick_step_track_background_background_light);
- colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_light);
- }
- mHighlight = new RadialGradient(0, mTrackRect.height() / 2,
- mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase,
- Shader.TileMode.CLAMP);
- mTrackPaint.setShader(mHighlight);
- }
-
- private boolean canPerformHomeBackGesture() {
- return swipeHomeGoBackGestureEnabled(mContext)
- && mOverviewEventSender.getBackButtonAlpha() > 0;
- }
-
- private void performBack() {
- sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
- sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
- mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
- }
-
- private void sendEvent(int action, int code) {
- long when = SystemClock.uptimeMillis();
- final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
- 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
- KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
- InputDevice.SOURCE_KEYBOARD);
- InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+ return false;
}
private boolean proxyMotionEvents(MotionEvent event) {
@@ -740,22 +534,15 @@
return false;
}
- private static boolean getBoolGlobalSetting(Context context, String key) {
+ protected boolean isNavBarVertical() {
+ return mNavBarPosition == NAV_BAR_LEFT || mNavBarPosition == NAV_BAR_RIGHT;
+ }
+
+ static boolean getBoolGlobalSetting(Context context, String key) {
return Settings.Global.getInt(context.getContentResolver(), key, 0) != 0;
}
- public static boolean swipeHomeGoBackGestureEnabled(Context context) {
- return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED)
- && getBoolGlobalSetting(context, PULL_HOME_GO_BACK_PROP);
- }
-
public static boolean shouldhideBackButton(Context context) {
- return swipeHomeGoBackGestureEnabled(context)
- && getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP);
- }
-
- public static boolean shouldExecuteBackOnUp(Context context) {
- return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED)
- && getBoolGlobalSetting(context, BACK_AFTER_END_PROP);
+ return getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
new file mode 100644
index 0000000..0781602
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
@@ -0,0 +1,618 @@
+/*
+ * Copyright (C) 2018 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 static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.SysuiTestCase;
+
+import android.content.res.Resources;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockitoAnnotations;
+
+/** atest QuickStepControllerTest */
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class QuickStepControllerTest extends SysuiTestCase {
+ private QuickStepController mController;
+ private NavigationBarView mNavigationBarView;
+ private StatusBar mStatusBar;
+ private OverviewProxyService mProxyService;
+ private IOverviewProxy mProxy;
+ private Resources mResources;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ final ButtonDispatcher backButton = mock(ButtonDispatcher.class);
+ mResources = mock(Resources.class);
+
+ mProxyService = mock(OverviewProxyService.class);
+ mProxy = mock(IOverviewProxy.Stub.class);
+ doReturn(mProxy).when(mProxyService).getProxy();
+ mDependency.injectTestDependency(OverviewProxyService.class, mProxyService);
+
+ mStatusBar = mock(StatusBar.class);
+ doReturn(false).when(mStatusBar).isKeyguardShowing();
+ mContext.putComponent(StatusBar.class, mStatusBar);
+
+ mNavigationBarView = mock(NavigationBarView.class);
+ doReturn(false).when(mNavigationBarView).inScreenPinning();
+ doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+ doReturn(true).when(mNavigationBarView).isQuickScrubEnabled();
+ doReturn(HIT_TARGET_NONE).when(mNavigationBarView).getDownHitTarget();
+ doReturn(backButton).when(mNavigationBarView).getBackButton();
+ doReturn(mResources).when(mNavigationBarView).getResources();
+
+ mController = new QuickStepController(mContext);
+ mController.setComponents(mNavigationBarView);
+ mController.setBarState(false /* isRTL */, NAV_BAR_BOTTOM);
+ }
+
+ @Test
+ public void testNoActionsNoGestures() throws Exception {
+ MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+ assertFalse(mController.onInterceptTouchEvent(ev));
+ verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+ assertNull(mController.getCurrentAction());
+ }
+
+ @Test
+ public void testHasActionDetectGesturesTouchdown() throws Exception {
+ MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+ // Add enabled gesture action
+ NavigationGestureAction action = mockAction(true);
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ assertFalse(mController.onInterceptTouchEvent(ev));
+ verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+ verify(action, times(1)).reset();
+ verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+ verify(mProxy, times(1)).onMotionEvent(ev);
+ assertNull(mController.getCurrentAction());
+ }
+
+ @Test
+ public void testProxyDisconnectedNoGestures() throws Exception {
+ MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+ // Add enabled gesture action
+ mController.setGestureActions(mockAction(true), null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Set the gesture on deadzone
+ doReturn(null).when(mProxyService).getProxy();
+
+ assertFalse(mController.onInterceptTouchEvent(ev));
+ verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+ assertNull(mController.getCurrentAction());
+ }
+
+ @Test
+ public void testNoActionsNoGesturesOverDeadzone() throws Exception {
+ MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+ // Touched over deadzone
+ doReturn(HIT_TARGET_DEAD_ZONE).when(mNavigationBarView).getDownHitTarget();
+
+ assertTrue(mController.onInterceptTouchEvent(ev));
+ verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+ assertNull(mController.getCurrentAction());
+ }
+
+ @Test
+ public void testOnTouchIgnoredDownEventAfterOnIntercept() {
+ mController.setGestureActions(mockAction(true), null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+ assertFalse(touch(ev));
+ verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+
+ // OnTouch event for down is ignored, so requestUnbufferedDispatch ran once from before
+ assertFalse(mNavigationBarView.onTouchEvent(ev));
+ verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+ }
+
+ @Test
+ public void testGesturesCallCorrectAction() throws Exception {
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // Swipe Up
+ assertGestureTriggersAction(swipeUp, 1, 100, 5, 1);
+ // Swipe Down
+ assertGestureTriggersAction(swipeDown, 1, 1, 5, 100);
+ // Swipe Left
+ assertGestureTriggersAction(swipeLeft, 100, 1, 5, 1);
+ // Swipe Right
+ assertGestureTriggersAction(swipeRight, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testGesturesCallCorrectActionLandscape() throws Exception {
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // In landscape
+ mController.setBarState(false /* isRTL */, NAV_BAR_RIGHT);
+
+ // Swipe Up
+ assertGestureTriggersAction(swipeRight, 1, 100, 5, 1);
+ // Swipe Down
+ assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100);
+ // Swipe Left
+ assertGestureTriggersAction(swipeUp, 100, 1, 5, 1);
+ // Swipe Right
+ assertGestureTriggersAction(swipeDown, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testGesturesCallCorrectActionSeascape() throws Exception {
+ mController.setBarState(false /* isRTL */, NAV_BAR_LEFT);
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // Swipe Up
+ assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1);
+ // Swipe Down
+ assertGestureTriggersAction(swipeRight, 1, 1, 5, 100);
+ // Swipe Left
+ assertGestureTriggersAction(swipeDown, 100, 1, 5, 1);
+ // Swipe Right
+ assertGestureTriggersAction(swipeUp, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testGesturesCallCorrectActionRTL() throws Exception {
+ mController.setBarState(true /* isRTL */, NAV_BAR_BOTTOM);
+
+ // The swipe gestures below are for LTR, so RTL in portrait will be swapped
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // Swipe Up in RTL
+ assertGestureTriggersAction(swipeUp, 1, 100, 5, 1);
+ // Swipe Down in RTL
+ assertGestureTriggersAction(swipeDown, 1, 1, 5, 100);
+ // Swipe Left in RTL
+ assertGestureTriggersAction(swipeRight, 100, 1, 5, 1);
+ // Swipe Right in RTL
+ assertGestureTriggersAction(swipeLeft, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testGesturesCallCorrectActionLandscapeRTL() throws Exception {
+ mController.setBarState(true /* isRTL */, NAV_BAR_RIGHT);
+
+ // The swipe gestures below are for LTR, so RTL in landscape will be swapped
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // Swipe Up
+ assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1);
+ // Swipe Down
+ assertGestureTriggersAction(swipeRight, 1, 1, 5, 100);
+ // Swipe Left
+ assertGestureTriggersAction(swipeUp, 100, 1, 5, 1);
+ // Swipe Right
+ assertGestureTriggersAction(swipeDown, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testGesturesCallCorrectActionSeascapeRTL() throws Exception {
+ mController.setBarState(true /* isRTL */, NAV_BAR_LEFT);
+
+ // The swipe gestures below are for LTR, so RTL in seascape will be swapped
+ NavigationGestureAction swipeUp = mockAction(true);
+ NavigationGestureAction swipeDown = mockAction(true);
+ NavigationGestureAction swipeLeft = mockAction(true);
+ NavigationGestureAction swipeRight = mockAction(true);
+ mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+ // Swipe Up
+ assertGestureTriggersAction(swipeRight, 1, 100, 5, 1);
+ // Swipe Down
+ assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100);
+ // Swipe Left
+ assertGestureTriggersAction(swipeDown, 100, 1, 5, 1);
+ // Swipe Right
+ assertGestureTriggersAction(swipeUp, 1, 1, 100, 5);
+ }
+
+ @Test
+ public void testActionPreventByPinnedState() throws Exception {
+ // Screen is pinned
+ doReturn(true).when(mNavigationBarView).inScreenPinning();
+
+ // Add enabled gesture action
+ NavigationGestureAction action = mockAction(true);
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Touch down to begin swipe
+ MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100);
+ assertFalse(touch(downEvent));
+ verify(mProxy, never()).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+ verify(mProxy, never()).onMotionEvent(downEvent);
+
+ // Move to start gesture, but pinned so it should not trigger action
+ MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+ assertFalse(touch(moveEvent));
+ assertNull(mController.getCurrentAction());
+ verify(mNavigationBarView, times(1)).showPinningEscapeToast();
+ verify(action, never()).onGestureStart(moveEvent);
+ }
+
+ @Test
+ public void testActionPreventedNotificationsShown() throws Exception {
+ NavigationGestureAction action = mockAction(true);
+ doReturn(false).when(action).canRunWhenNotificationsShowing();
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Show the notifications
+ doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+ // Swipe up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+ assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+ assertNull(mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+
+ // Hide the notifications
+ doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+ // Swipe up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+ assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1));
+ assertEquals(action, mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+ }
+
+ @Test
+ public void testActionCannotPerform() throws Exception {
+ NavigationGestureAction action = mockAction(true);
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Cannot perform action
+ doReturn(false).when(action).canPerformAction();
+
+ // Swipe up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+ assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+ assertNull(mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+
+ // Cannot perform action
+ doReturn(true).when(action).canPerformAction();
+
+ // Swipe up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+ assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1));
+ assertEquals(action, mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+ }
+
+ @Test
+ public void testQuickScrub() throws Exception {
+ QuickScrubAction action = spy(new QuickScrubAction(mNavigationBarView, mProxyService));
+ mController.setGestureActions(null /* swipeUpAction */, null /* swipeDownAction */,
+ null /* swipeLeftAction */, action);
+ int y = 20;
+
+ // Set the layout and other padding to make sure the scrub fraction is calculated correctly
+ action.onLayout(true, 0, 0, 400, 100);
+ doReturn(0).when(mNavigationBarView).getPaddingLeft();
+ doReturn(0).when(mNavigationBarView).getPaddingRight();
+ doReturn(0).when(mNavigationBarView).getPaddingStart();
+ doReturn(0).when(mResources)
+ .getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
+
+ // Quickscrub disabled, so the action should be disabled
+ doReturn(false).when(mNavigationBarView).isQuickScrubEnabled();
+ assertFalse(action.isEnabled());
+ doReturn(true).when(mNavigationBarView).isQuickScrubEnabled();
+
+ // Touch down
+ MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 0, y);
+ assertFalse(touch(downEvent));
+ assertNull(mController.getCurrentAction());
+ verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+ verify(mProxy, times(1)).onMotionEvent(downEvent);
+
+ // Move to start trigger action from gesture
+ MotionEvent moveEvent1 = event(MotionEvent.ACTION_MOVE, 100, y);
+ assertTrue(touch(moveEvent1));
+ assertEquals(action, mController.getCurrentAction());
+ verify(action, times(1)).onGestureStart(moveEvent1);
+ verify(mProxy, times(1)).onQuickScrubStart();
+ verify(mProxyService, times(1)).notifyQuickScrubStarted();
+ verify(mNavigationBarView, times(1)).updateSlippery();
+
+ // Move again for scrub
+ MotionEvent moveEvent2 = event(MotionEvent.ACTION_MOVE, 200, y);
+ assertTrue(touch(moveEvent2));
+ assertEquals(action, mController.getCurrentAction());
+ verify(action, times(1)).onGestureMove(200, y);
+ verify(mProxy, times(1)).onQuickScrubProgress(1f / 2);
+
+ // Action up
+ MotionEvent upEvent = event(MotionEvent.ACTION_UP, 1, y);
+ assertFalse(touch(upEvent));
+ assertNull(mController.getCurrentAction());
+ verify(action, times(1)).onGestureEnd();
+ verify(mProxy, times(1)).onQuickScrubEnd();
+ }
+
+ @Test
+ public void testQuickStep() throws Exception {
+ QuickStepAction action = new QuickStepAction(mNavigationBarView, mProxyService);
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Notifications are up, should prevent quickstep
+ doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+ // Swipe up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+ assertNull(mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+ assertNull(mController.getCurrentAction());
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+ doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+ // Quickstep disabled, so the action should be disabled
+ doReturn(false).when(mNavigationBarView).isQuickStepSwipeUpEnabled();
+ assertFalse(action.isEnabled());
+ doReturn(true).when(mNavigationBarView).isQuickStepSwipeUpEnabled();
+
+ // Swipe up should call proxy events
+ MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100);
+ assertFalse(touch(downEvent));
+ assertNull(mController.getCurrentAction());
+ verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+ verify(mProxy, times(1)).onMotionEvent(downEvent);
+
+ MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+ assertTrue(touch(moveEvent));
+ assertEquals(action, mController.getCurrentAction());
+ verify(mProxy, times(1)).onQuickStep(moveEvent);
+ verify(mProxyService, times(1)).notifyQuickStepStarted();
+ }
+
+ @Test
+ public void testLongPressPreventDetection() throws Exception {
+ NavigationGestureAction action = mockAction(true);
+ mController.setGestureActions(action, null /* swipeDownAction */,
+ null /* swipeLeftAction */, null /* swipeRightAction */);
+
+ // Start the drag up
+ assertFalse(touch(MotionEvent.ACTION_DOWN, 100, 1));
+ assertNull(mController.getCurrentAction());
+
+ // Long press something on the navigation bar such as Home button
+ mNavigationBarView.onNavigationButtonLongPress(mock(View.class));
+
+ // Swipe right will not start any gestures
+ MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+ assertFalse(touch(motionMoveEvent));
+ assertNull(mController.getCurrentAction());
+ verify(action, never()).startGesture(motionMoveEvent);
+
+ // Touch up
+ assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+ verify(action, never()).endGesture();
+ }
+
+ @Test
+ public void testHitTargetDragged() throws Exception {
+ ButtonDispatcher button = mock(ButtonDispatcher.class);
+ View buttonView = spy(new View(mContext));
+ doReturn(buttonView).when(button).getCurrentView();
+
+ NavigationGestureAction action = mockAction(true);
+ mController.setGestureActions(action, action, action, action);
+
+ // Setup getting the hit target
+ doReturn(HIT_TARGET_HOME).when(action).requiresTouchDownHitTarget();
+ doReturn(true).when(action).requiresDragWithHitTarget();
+ doReturn(HIT_TARGET_HOME).when(mNavigationBarView).getDownHitTarget();
+ doReturn(button).when(mNavigationBarView).getHomeButton();
+
+ // Portrait
+ assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_BOTTOM);
+
+ // Portrait RTL
+ assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_BOTTOM);
+
+ // Landscape
+ assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_RIGHT);
+
+ // Landscape RTL
+ assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_RIGHT);
+
+ // Seascape
+ assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_LEFT);
+
+ // Seascape RTL
+ assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_LEFT);
+ }
+
+ private void assertGestureDragsHitTargetAllDirections(View buttonView, boolean isRTL,
+ int navPos) {
+ mController.setBarState(isRTL, navPos);
+
+ // Swipe up
+ assertGestureDragsHitTarget(buttonView, 10 /* x1 */, 200 /* y1 */, 0 /* x2 */, 0 /* y2 */,
+ 0 /* dx */, -1 /* dy */);
+ // Swipe left
+ assertGestureDragsHitTarget(buttonView, 200 /* x1 */, 10 /* y1 */, 0 /* x2 */, 0 /* y2 */,
+ -1 /* dx */, 0 /* dy */);
+ // Swipe right
+ assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 200 /* x2 */, 10 /* y2 */,
+ 1 /* dx */, 0 /* dy */);
+ // Swipe down
+ assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 10 /* x2 */, 200 /* y2 */,
+ 0 /* dx */, 1 /* dy */);
+ }
+
+ /**
+ * Asserts the gesture actually moves the hit target
+ * @param buttonView button to check if moved, use Mockito.spy on a real object
+ * @param x1 start x
+ * @param x2 start y
+ * @param y1 end x
+ * @param y2 end y
+ * @param dx diff in x, if not 0, its sign determines direction, value does not matter
+ * @param dy diff in y, if not 0, its sign determines direction, value does not matter
+ */
+ private void assertGestureDragsHitTarget(View buttonView, int x1, int y1, int x2, int y2,
+ int dx, int dy) {
+ ArgumentCaptor<Float> captor = ArgumentCaptor.forClass(Float.class);
+ assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1));
+ assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2));
+
+ // Verify positions of the button drag
+ if (dx == 0) {
+ verify(buttonView, never()).setTranslationX(anyFloat());
+ } else {
+ verify(buttonView).setTranslationX(captor.capture());
+ if (dx < 0) {
+ assertTrue("Button should have moved left", (float) captor.getValue() < 0);
+ } else {
+ assertTrue("Button should have moved right", (float) captor.getValue() > 0);
+ }
+ }
+ if (dy == 0) {
+ verify(buttonView, never()).setTranslationY(anyFloat());
+ } else {
+ verify(buttonView).setTranslationY(captor.capture());
+ if (dy < 0) {
+ assertTrue("Button should have moved up", (float) captor.getValue() < 0);
+ } else {
+ assertTrue("Button should have moved down", (float) captor.getValue() > 0);
+ }
+ }
+
+ // Touch up
+ assertFalse(touch(MotionEvent.ACTION_UP, x2, y2));
+ verify(buttonView, times(1)).animate();
+
+ // Reset button state
+ reset(buttonView);
+ }
+
+
+ private MotionEvent event(int action, float x, float y) {
+ final MotionEvent event = mock(MotionEvent.class);
+ doReturn(x).when(event).getX();
+ doReturn(y).when(event).getY();
+ doReturn(action & MotionEvent.ACTION_MASK).when(event).getActionMasked();
+ doReturn(action).when(event).getAction();
+ return event;
+ }
+
+ private boolean touch(int action, float x, float y) {
+ return touch(event(action, x, y));
+ }
+
+ private boolean touch(MotionEvent event) {
+ return mController.onInterceptTouchEvent(event);
+ }
+
+ private NavigationGestureAction mockAction(boolean enabled) {
+ final NavigationGestureAction action = mock(NavigationGestureAction.class);
+ doReturn(enabled).when(action).isEnabled();
+ doReturn(HIT_TARGET_NONE).when(action).requiresTouchDownHitTarget();
+ doReturn(true).when(action).canPerformAction();
+ return action;
+ }
+
+ private void assertGestureTriggersAction(NavigationGestureAction action, int x1, int y1,
+ int x2, int y2) {
+ // Start the drag
+ assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1));
+ assertNull(mController.getCurrentAction());
+
+ // Swipe
+ MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, x2, y2);
+ assertTrue(touch(motionMoveEvent));
+ assertEquals(action, mController.getCurrentAction());
+ verify(action, times(1)).startGesture(motionMoveEvent);
+
+ // Move again
+ assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2));
+ verify(action, times(1)).onGestureMove(x2, y2);
+
+ // Touch up
+ assertFalse(touch(MotionEvent.ACTION_UP, x2, y2));
+ assertNull(mController.getCurrentAction());
+ verify(action, times(1)).endGesture();
+ }
+}