Merge "Refactor QuickStepController into Gestures"

This commit is contained in:
TreeHugger Robot
2018-11-12 19:44:42 +00:00
committed by Android (Google) Code Review
9 changed files with 1626 additions and 494 deletions

View File

@@ -36,7 +36,7 @@ public interface NavGesture extends Plugin {
public boolean onInterceptTouchEvent(MotionEvent event);
public void setBarState(boolean vertical, boolean isRtl);
public void setBarState(boolean isRtl, int navBarPosition);
public void onDraw(Canvas canvas);

View File

@@ -263,6 +263,16 @@ public class ButtonDispatcher {
}
}
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 @@ public class ButtonDispatcher {
if (mImageDrawable != null) {
mImageDrawable.setCallback(mCurrentView);
}
if (mCurrentView != null) {
mCurrentView.setTranslationX(0);
mCurrentView.setTranslationY(0);
mCurrentView.setTranslationZ(0);
}
}
public void setVertical(boolean vertical) {

View File

@@ -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);
}
}

View File

@@ -38,9 +38,11 @@ import android.graphics.Rect;
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.View;
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mPanelView = panel;
if (mGestureHelper instanceof QuickStepController) {
((QuickStepController) mGestureHelper).setComponents(this);
((QuickStepController) mGestureHelper).setGestureActions(mQuickStepAction,
null /* swipeDownAction*/, mBackAction, mQuickScrubAction);
}
}
@@ -756,24 +769,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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);

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -18,187 +18,96 @@ package com.android.systemui.statusbar.phone;
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 NavigationGestureAction mCurrentAction;
private NavigationGestureAction[] mGestureActions = new NavigationGestureAction[MAX_GESTURES];
private final Handler mHandler = new Handler();
private final Rect mTrackRect = new Rect();
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) {
@@ -209,6 +118,31 @@ public class QuickStepController implements GestureHelper {
: View.VISIBLE);
}
/**
* 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 @@ public class QuickStepController implements GestureHelper {
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 @@ public class QuickStepController implements GestureHelper {
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 @@ public class QuickStepController implements GestureHelper {
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 @@ public class QuickStepController implements GestureHelper {
@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 @@ public class QuickStepController implements GestureHelper {
// 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);
}
// 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;
}
}
}
mOverviewEventSender.notifyQuickScrubStarted();
if (mHitTarget != null) {
mHitTarget.abortCurrentGesture();
@@ -576,148 +505,13 @@ public class QuickStepController implements GestureHelper {
}
}
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 @@ public class QuickStepController implements GestureHelper {
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);
}
}

View File

@@ -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();
}
}