blob: 19544b170d1ff1c5362cdc024d8a72119b0c40d2 [file] [log] [blame]
/*
* 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManagerGlobal;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.support.annotation.DimenRes;
import com.android.systemui.Dependency;
import com.android.systemui.OverviewProxyService;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.utilities.Utilities;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
import static com.android.systemui.OverviewProxyService.TAG_OPS;
import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
/**
* Class to detect gestures on the navigation bar and implement quick scrub and switch.
*/
public class QuickStepController extends GestureDetector.SimpleOnGestureListener implements
GestureHelper {
private static final String TAG = "QuickStepController";
private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
private static final int ANIM_DURATION_MS = 200;
private static final long LONG_PRESS_DELAY_MS = 225;
/**
* For quick step, set a damping value to allow the button to stick closer its origin position
* when dragging before quick scrub is active.
*/
private static final int SWITCH_STICKINESS = 4;
private NavigationBarView mNavigationBarView;
private GestureDetector mGestureDetector;
private boolean mDraggingActive;
private boolean mQuickScrubActive;
private boolean mAllowQuickSwitch;
private boolean mAllowGestureDetection;
private boolean mQuickStepStarted;
private float mDownOffset;
private float mTranslation;
private int mTouchDownX;
private int mTouchDownY;
private boolean mDragPositive;
private boolean mIsVertical;
private boolean mIsRTL;
private float mTrackAlpha;
private int mLightTrackColor;
private int mDarkTrackColor;
private float mDarkIntensity;
private View mHomeButtonView;
private final Handler mHandler = new Handler();
private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
private final Rect mTrackRect = new Rect();
private final Paint mTrackPaint = new Paint();
private final int mScrollTouchSlop;
private final OverviewProxyService mOverviewEventSender;
private final int mTrackThickness;
private final int mTrackPadding;
private final ValueAnimator mTrackAnimator;
private final ValueAnimator mButtonAnimator;
private final AnimatorSet mQuickScrubEndAnimator;
private final Context mContext;
private final Matrix mTransformGlobalMatrix = new Matrix();
private final Matrix mTransformLocalMatrix = new Matrix();
private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator();
private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
mTrackAlpha = (float) valueAnimator.getAnimatedValue();
mNavigationBarView.invalidate();
};
private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
int pos = (int) animator.getAnimatedValue();
if (!mQuickScrubActive) {
pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
}
if (mIsVertical) {
mHomeButtonView.setTranslationY(pos);
} else {
mHomeButtonView.setTranslationX(pos);
}
};
private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mQuickScrubActive = false;
mTranslation = 0;
mQuickScrubEndAnimator.setCurrentPlayTime(mQuickScrubEndAnimator.getDuration());
mHomeButtonView = null;
}
};
private Runnable mLongPressRunnable = this::startQuickScrub;
private final GestureDetector.SimpleOnGestureListener mGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
if (!mNavigationBarView.isQuickScrubEnabled() || mQuickScrubActive
|| !mAllowQuickSwitch
|| mNavigationBarView.getDownHitTarget() != HIT_TARGET_HOME) {
return false;
}
float velocityX = mIsRTL ? -velX : velX;
float absVelY = Math.abs(velY);
final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
if (isValidFling) {
mDraggingActive = false;
animateEnd();
mHandler.removeCallbacks(mLongPressRunnable);
try {
final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
overviewProxy.onQuickSwitch();
if (DEBUG_OVERVIEW_PROXY) {
Log.d(TAG_OPS, "Quick Switch");
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to send start of quick switch.", e);
}
return true;
}
return false;
}
};
public QuickStepController(Context context) {
mContext = context;
mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mOverviewEventSender = Dependency.get(OverviewProxyService.class);
mGestureDetector = new GestureDetector(mContext, mGestureListener);
mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
mTrackPaint.setAlpha(0);
mTrackAnimator = ObjectAnimator.ofFloat();
mTrackAnimator.addUpdateListener(mTrackAnimatorListener);
mTrackAnimator.setFloatValues(0);
mButtonAnimator = ObjectAnimator.ofInt();
mButtonAnimator.addUpdateListener(mButtonTranslationListener);
mButtonAnimator.setIntValues(0);
mQuickScrubEndAnimator = new AnimatorSet();
mQuickScrubEndAnimator.playTogether(mTrackAnimator, mButtonAnimator);
mQuickScrubEndAnimator.setDuration(ANIM_DURATION_MS);
mQuickScrubEndAnimator.addListener(mQuickScrubEndListener);
mQuickScrubEndAnimator.setInterpolator(mQuickScrubEndInterpolator);
}
public void setComponents(NavigationBarView navigationBarView) {
mNavigationBarView = navigationBarView;
}
/**
* @return true if we want to intercept touch events for quick scrub/switch and prevent proxying
* the event to the overview service.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return handleTouchEvent(event);
}
/**
* @return true if we want to handle touch events for quick scrub/switch or if down event (that
* will get consumed and ignored). No events will be proxied to the overview service.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// The same down event was just sent on intercept and therefore can be ignored here
final boolean ignoreProxyDownEvent = event.getAction() == MotionEvent.ACTION_DOWN
&& mOverviewEventSender.getProxy() != null;
return ignoreProxyDownEvent || handleTouchEvent(event);
}
private boolean handleTouchEvent(MotionEvent event) {
if (!mNavigationBarView.isQuickScrubEnabled()
&& !mNavigationBarView.isQuickStepSwipeUpEnabled()) {
mNavigationBarView.getHomeButton().setDelayTouchFeedback(false /* delay */);
return false;
}
mNavigationBarView.requestUnbufferedDispatch(event);
final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
if (mGestureDetector.onTouchEvent(event)) {
// If the fling has been handled on UP, then skip proxying the UP
return true;
}
final boolean homePressed = mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME;
int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
int x = (int) event.getX();
int y = (int) event.getY();
// End any existing quickscrub animations before starting the new transition
if (mHomeButtonView != null) {
mQuickScrubEndAnimator.end();
}
mHomeButtonView = homeButton.getCurrentView();
homeButton.setDelayTouchFeedback(true /* delay */);
mTouchDownX = x;
mTouchDownY = y;
if (homePressed) {
mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
}
mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX);
mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX);
mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix);
mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix);
mQuickStepStarted = false;
mAllowQuickSwitch = true;
mAllowGestureDetection = true;
break;
}
case MotionEvent.ACTION_MOVE: {
if (mQuickStepStarted || !mAllowGestureDetection){
break;
}
int x = (int) event.getX();
int y = (int) event.getY();
int xDiff = Math.abs(x - mTouchDownX);
int yDiff = Math.abs(y - mTouchDownY);
boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
int pos, touchDown, offset, trackSize;
if (mIsVertical) {
exceededTouchSlop = exceededTouchSlopY;
exceededPerpendicularTouchSlop = exceededTouchSlopX;
pos = y;
touchDown = mTouchDownY;
offset = pos - mTrackRect.top;
trackSize = mTrackRect.height();
} else {
exceededTouchSlop = exceededTouchSlopX;
exceededPerpendicularTouchSlop = exceededTouchSlopY;
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 (!mDraggingActive && !mQuickScrubActive && exceededPerpendicularTouchSlop) {
if (mNavigationBarView.isQuickStepSwipeUpEnabled()) {
startQuickStep(event);
}
break;
}
// Do not handle quick scrub/switch if disabled or hit target is not home button
if (!homePressed || !mNavigationBarView.isQuickScrubEnabled()) {
break;
}
if (!mDragPositive) {
offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
}
// Control the button movement
if (!mDraggingActive && exceededTouchSlop) {
boolean allowDrag = !mDragPositive
? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
if (allowDrag) {
mDownOffset = offset;
homeButton.abortCurrentGesture();
mDraggingActive = true;
}
}
if (mDraggingActive && (mDragPositive && offset >= 0
|| !mDragPositive && offset <= 0)) {
float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
mTranslation = !mDragPositive
? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
: Utilities.clamp(offset - mDownOffset, 0, trackSize);
if (mQuickScrubActive) {
try {
mOverviewEventSender.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);
}
} else {
mTranslation /= SWITCH_STICKINESS;
}
if (mIsVertical) {
mHomeButtonView.setTranslationY(mTranslation);
} else {
mHomeButtonView.setTranslationX(mTranslation);
}
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
endQuickScrub(true /* animate */);
break;
}
// Proxy motion events to launcher if not handled by quick scrub/switch
boolean handled = mDraggingActive || mQuickScrubActive;
if (!handled && mAllowGestureDetection) {
proxyMotionEvents(event);
}
return handled || mQuickStepStarted;
}
@Override
public void onDraw(Canvas canvas) {
if (mNavigationBarView.isQuickScrubEnabled()) {
return;
}
int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor,
mDarkTrackColor);
mTrackPaint.setColor(color);
mTrackPaint.setAlpha((int) (mTrackPaint.getAlpha() * mTrackAlpha));
canvas.drawRect(mTrackRect, mTrackPaint);
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = right - left;
final int height = bottom - top;
final int x1, x2, y1, y2;
if (mIsVertical) {
x1 = (width - mTrackThickness) / 2;
x2 = x1 + mTrackThickness;
y1 = mDragPositive ? height / 2 : mTrackPadding;
y2 = y1 + height / 2 - mTrackPadding;
} else {
y1 = (height - mTrackThickness) / 2;
y2 = y1 + mTrackThickness;
x1 = mDragPositive ? width / 2 : mTrackPadding;
x2 = x1 + width / 2 - mTrackPadding;
}
mTrackRect.set(x1, y1, x2, y2);
}
@Override
public void onDarkIntensityChange(float intensity) {
mDarkIntensity = intensity;
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;
mIsRTL = isRTL;
try {
int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
if (isRTL) {
mDragPositive = !mDragPositive;
}
} catch (RemoteException e) {
Slog.e(TAG, "Failed to get nav bar position.", e);
}
}
@Override
public void onNavigationButtonLongPress(View v) {
mAllowGestureDetection = false;
mHandler.removeCallbacksAndMessages(null);
}
private void startQuickStep(MotionEvent event) {
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();
mNavigationBarView.getHomeButton().abortCurrentGesture();
cancelQuickSwitch();
mHandler.removeCallbacksAndMessages(null);
}
private void startQuickScrub() {
if (!mQuickScrubActive && mDraggingActive) {
mQuickScrubActive = true;
mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
mTrackAnimator.setFloatValues(0, 1);
mTrackAnimator.start();
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);
}
}
}
private void endQuickScrub(boolean animate) {
mHandler.removeCallbacks(mLongPressRunnable);
if (mDraggingActive || 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);
}
}
if (mHomeButtonView != null && !animate) {
mQuickScrubEndAnimator.end();
}
mDraggingActive = false;
}
private boolean proxyMotionEvents(MotionEvent event) {
final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
event.transform(mTransformGlobalMatrix);
try {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
overviewProxy.onPreMotionEvent(mNavigationBarView.getDownHitTarget());
}
overviewProxy.onMotionEvent(event);
if (DEBUG_OVERVIEW_PROXY) {
Log.d(TAG_OPS, "Send MotionEvent: " + event.toString());
}
return true;
} catch (RemoteException e) {
Log.e(TAG, "Callback failed", e);
} finally {
event.transform(mTransformLocalMatrix);
}
return false;
}
public void cancelQuickSwitch() {
mAllowQuickSwitch = false;
mHandler.removeCallbacks(mLongPressRunnable);
}
private void animateEnd() {
mButtonAnimator.setIntValues((int) mTranslation, 0);
mTrackAnimator.setFloatValues(mTrackAlpha, 0);
mQuickScrubEndAnimator.setCurrentPlayTime(0);
mQuickScrubEndAnimator.start();
}
private int getDimensionPixelSize(Context context, @DimenRes int resId) {
return context.getResources().getDimensionPixelSize(resId);
}
}