| /* |
| * 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 com.android.systemui.Interpolators.ALPHA_IN; |
| import static com.android.systemui.Interpolators.ALPHA_OUT; |
| 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_DEAD_ZONE; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| 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.os.Handler; |
| import android.os.RemoteException; |
| import android.util.FloatProperty; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.WindowManagerGlobal; |
| import android.view.animation.DecelerateInterpolator; |
| import android.view.animation.Interpolator; |
| import androidx.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 com.android.systemui.shared.system.NavigationBarCompat; |
| |
| /** |
| * Class to detect gestures on the navigation bar and implement quick scrub. |
| */ |
| 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; |
| |
| private NavigationBarView mNavigationBarView; |
| |
| private boolean mQuickScrubActive; |
| private boolean mAllowGestureDetection; |
| private boolean mQuickStepStarted; |
| private int mTouchDownX; |
| private int mTouchDownY; |
| private boolean mDragPositive; |
| private boolean mIsVertical; |
| private boolean mIsRTL; |
| private float mTrackAlpha; |
| private float mTrackScale = TRACK_SCALE; |
| private float mDarkIntensity; |
| private RadialGradient mHighlight; |
| private float mHighlightCenter; |
| private AnimatorSet mTrackAnimator; |
| private ButtonDispatcher mHitTarget; |
| private View mCurrentNavigationBarView; |
| |
| 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 Context mContext; |
| 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(); |
| } |
| }; |
| |
| public QuickStepController(Context context) { |
| final Resources res = context.getResources(); |
| mContext = context; |
| 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); |
| mTrackPaint.setAntiAlias(true); |
| mTrackPaint.setDither(true); |
| } |
| |
| public void setComponents(NavigationBarView navigationBarView) { |
| mNavigationBarView = navigationBarView; |
| } |
| |
| /** |
| * @return true if we want to intercept touch events for quick scrub 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 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) { |
| final boolean deadZoneConsumed = |
| mNavigationBarView.getDownHitTarget() == HIT_TARGET_DEAD_ZONE; |
| if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled() |
| && !mNavigationBarView.isQuickStepSwipeUpEnabled())) { |
| return false; |
| } |
| mNavigationBarView.requestUnbufferedDispatch(event); |
| |
| int action = event.getActionMasked(); |
| switch (action) { |
| 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 (mTrackAnimator != null) { |
| mTrackAnimator.end(); |
| mTrackAnimator = null; |
| } |
| |
| mCurrentNavigationBarView = mNavigationBarView.getCurrentView(); |
| mHitTarget = mNavigationBarView.getButtonAtPosition(x, y); |
| if (mHitTarget != null) { |
| // Pre-emptively delay the touch feedback for the button that we just touched |
| mHitTarget.setDelayTouchFeedback(true); |
| } |
| mTouchDownX = x; |
| mTouchDownY = y; |
| mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX); |
| mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX); |
| mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix); |
| mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix); |
| mQuickStepStarted = false; |
| 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 exceededScrubTouchSlop, exceededSwipeUpTouchSlop; |
| int pos, touchDown, offset, trackSize; |
| |
| if (mIsVertical) { |
| exceededScrubTouchSlop = |
| yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff; |
| exceededSwipeUpTouchSlop = |
| xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff; |
| pos = y; |
| touchDown = mTouchDownY; |
| offset = pos - mTrackRect.top; |
| trackSize = mTrackRect.height(); |
| } else { |
| exceededScrubTouchSlop = |
| xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff; |
| exceededSwipeUpTouchSlop = |
| 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 (!mQuickScrubActive && exceededSwipeUpTouchSlop) { |
| if (mNavigationBarView.isQuickStepSwipeUpEnabled()) { |
| startQuickStep(event); |
| } |
| break; |
| } |
| |
| // 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 (allowDrag) { |
| // Passing the drag slop then touch slop will start quick step |
| if (!mQuickScrubActive && exceededScrubTouchSlop) { |
| startQuickScrub(); |
| } |
| } |
| |
| 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); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to send progress of quick scrub.", e); |
| } |
| mHighlightCenter = x; |
| mNavigationBarView.invalidate(); |
| } |
| break; |
| } |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| endQuickScrub(true /* animate */); |
| break; |
| } |
| |
| // Proxy motion events to launcher if not handled by quick scrub |
| // Proxy motion events up/cancel that would be sent after long press on any nav button |
| if (!mQuickScrubActive && (mAllowGestureDetection || action == MotionEvent.ACTION_CANCEL |
| || action == MotionEvent.ACTION_UP)) { |
| proxyMotionEvents(event); |
| } |
| return mQuickScrubActive || mQuickStepStarted || deadZoneConsumed; |
| } |
| |
| @Override |
| public void onDraw(Canvas canvas) { |
| if (!mNavigationBarView.isQuickScrubEnabled()) { |
| 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 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; |
| } |
| mTrackRect.set(x1, y1, x2, y2); |
| updateHighlight(); |
| } |
| |
| @Override |
| public void onDarkIntensityChange(float intensity) { |
| final float oldIntensity = mDarkIntensity; |
| mDarkIntensity = intensity; |
| |
| // When in quick scrub, invalidate gradient if changing intensity from black to white and |
| // vice-versa |
| if (mNavigationBarView.isQuickScrubEnabled() |
| && Math.round(intensity) != Math.round(oldIntensity)) { |
| updateHighlight(); |
| } |
| 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(); |
| mHandler.removeCallbacksAndMessages(null); |
| |
| if (mHitTarget != null) { |
| mHitTarget.abortCurrentGesture(); |
| } |
| |
| if (mQuickScrubActive) { |
| animateEnd(); |
| } |
| } |
| |
| private void startQuickScrub() { |
| 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); |
| } |
| mOverviewEventSender.notifyQuickScrubStarted(); |
| |
| if (mHitTarget != null) { |
| mHitTarget.abortCurrentGesture(); |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |
| if (!animate) { |
| if (mTrackAnimator != null) { |
| mTrackAnimator.end(); |
| mTrackAnimator = null; |
| } |
| } |
| } |
| |
| 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; |
| mCurrentNavigationBarView = null; |
| updateHighlight(); |
| } |
| |
| 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 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; |
| } |
| } |