1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.phone; 18 19 import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; 20 import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; 21 import static com.android.systemui.Interpolators.ALPHA_IN; 22 import static com.android.systemui.Interpolators.ALPHA_OUT; 23 import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY; 24 import static com.android.systemui.OverviewProxyService.TAG_OPS; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.animation.AnimatorSet; 29 import android.animation.ArgbEvaluator; 30 import android.animation.ObjectAnimator; 31 import android.animation.PropertyValuesHolder; 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.Matrix; 37 import android.graphics.Rect; 38 import android.graphics.drawable.Drawable; 39 import android.os.Handler; 40 import android.os.RemoteException; 41 import android.util.FloatProperty; 42 import android.util.Log; 43 import android.util.Slog; 44 import android.view.MotionEvent; 45 import android.view.View; 46 import android.view.WindowManagerGlobal; 47 import android.view.animation.DecelerateInterpolator; 48 import android.view.animation.Interpolator; 49 import android.support.annotation.DimenRes; 50 import com.android.systemui.Dependency; 51 import com.android.systemui.OverviewProxyService; 52 import com.android.systemui.R; 53 import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper; 54 import com.android.systemui.shared.recents.IOverviewProxy; 55 import com.android.systemui.shared.recents.utilities.Utilities; 56 import com.android.systemui.shared.system.NavigationBarCompat; 57 import com.android.internal.graphics.ColorUtils; 58 59 /** 60 * Class to detect gestures on the navigation bar and implement quick scrub. 61 */ 62 public class QuickStepController implements GestureHelper { 63 64 private static final String TAG = "QuickStepController"; 65 private static final int ANIM_IN_DURATION_MS = 150; 66 private static final int ANIM_OUT_DURATION_MS = 134; 67 private static final float TRACK_SCALE = 0.95f; 68 69 private NavigationBarView mNavigationBarView; 70 71 private boolean mQuickScrubActive; 72 private boolean mAllowGestureDetection; 73 private boolean mQuickStepStarted; 74 private int mTouchDownX; 75 private int mTouchDownY; 76 private boolean mDragPositive; 77 private boolean mIsVertical; 78 private boolean mIsRTL; 79 private float mTrackAlpha; 80 private float mTrackScale = TRACK_SCALE; 81 private int mLightTrackColor; 82 private int mDarkTrackColor; 83 private float mDarkIntensity; 84 private AnimatorSet mTrackAnimator; 85 private ButtonDispatcher mHitTarget; 86 private View mCurrentNavigationBarView; 87 88 private final Handler mHandler = new Handler(); 89 private final Rect mTrackRect = new Rect(); 90 private final Drawable mTrackDrawable; 91 private final OverviewProxyService mOverviewEventSender; 92 private final int mTrackThickness; 93 private final int mTrackEndPadding; 94 private final Context mContext; 95 private final Matrix mTransformGlobalMatrix = new Matrix(); 96 private final Matrix mTransformLocalMatrix = new Matrix(); 97 private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator(); 98 99 private final FloatProperty<QuickStepController> mTrackAlphaProperty = 100 new FloatProperty<QuickStepController>("TrackAlpha") { 101 @Override 102 public void setValue(QuickStepController controller, float alpha) { 103 mTrackAlpha = alpha; 104 mNavigationBarView.invalidate(); 105 } 106 107 @Override 108 public Float get(QuickStepController controller) { 109 return mTrackAlpha; 110 } 111 }; 112 113 private final FloatProperty<QuickStepController> mTrackScaleProperty = 114 new FloatProperty<QuickStepController>("TrackScale") { 115 @Override 116 public void setValue(QuickStepController controller, float scale) { 117 mTrackScale = scale; 118 mNavigationBarView.invalidate(); 119 } 120 121 @Override 122 public Float get(QuickStepController controller) { 123 return mTrackScale; 124 } 125 }; 126 127 private final FloatProperty<QuickStepController> mNavBarAlphaProperty = 128 new FloatProperty<QuickStepController>("NavBarAlpha") { 129 @Override 130 public void setValue(QuickStepController controller, float alpha) { 131 if (mCurrentNavigationBarView != null) { 132 mCurrentNavigationBarView.setAlpha(alpha); 133 } 134 } 135 136 @Override 137 public Float get(QuickStepController controller) { 138 if (mCurrentNavigationBarView != null) { 139 return mCurrentNavigationBarView.getAlpha(); 140 } 141 return 1f; 142 } 143 }; 144 145 private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() { 146 @Override 147 public void onAnimationEnd(Animator animation) { 148 resetQuickScrub(); 149 } 150 }; 151 152 public QuickStepController(Context context) { 153 final Resources res = context.getResources(); 154 mContext = context; 155 mOverviewEventSender = Dependency.get(OverviewProxyService.class); 156 mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness); 157 mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding); 158 mTrackDrawable = context.getDrawable(R.drawable.qs_scrubber_track).mutate(); 159 } 160 161 public void setComponents(NavigationBarView navigationBarView) { 162 mNavigationBarView = navigationBarView; 163 } 164 165 /** 166 * @return true if we want to intercept touch events for quick scrub and prevent proxying the 167 * event to the overview service. 168 */ 169 @Override 170 public boolean onInterceptTouchEvent(MotionEvent event) { 171 return handleTouchEvent(event); 172 } 173 174 /** 175 * @return true if we want to handle touch events for quick scrub or if down event (that will 176 * get consumed and ignored). No events will be proxied to the overview service. 177 */ 178 @Override 179 public boolean onTouchEvent(MotionEvent event) { 180 // The same down event was just sent on intercept and therefore can be ignored here 181 final boolean ignoreProxyDownEvent = event.getAction() == MotionEvent.ACTION_DOWN 182 && mOverviewEventSender.getProxy() != null; 183 return ignoreProxyDownEvent || handleTouchEvent(event); 184 } 185 186 private boolean handleTouchEvent(MotionEvent event) { 187 if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled() 188 && !mNavigationBarView.isQuickStepSwipeUpEnabled())) { 189 return false; 190 } 191 mNavigationBarView.requestUnbufferedDispatch(event); 192 193 int action = event.getActionMasked(); 194 switch (action) { 195 case MotionEvent.ACTION_DOWN: { 196 int x = (int) event.getX(); 197 int y = (int) event.getY(); 198 199 // End any existing quickscrub animations before starting the new transition 200 if (mTrackAnimator != null) { 201 mTrackAnimator.end(); 202 mTrackAnimator = null; 203 } 204 205 mCurrentNavigationBarView = mNavigationBarView.getCurrentView(); 206 mHitTarget = mNavigationBarView.getButtonAtPosition(x, y); 207 if (mHitTarget != null) { 208 // Pre-emptively delay the touch feedback for the button that we just touched 209 mHitTarget.setDelayTouchFeedback(true); 210 } 211 mTouchDownX = x; 212 mTouchDownY = y; 213 mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX); 214 mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX); 215 mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix); 216 mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix); 217 mQuickStepStarted = false; 218 mAllowGestureDetection = true; 219 break; 220 } 221 case MotionEvent.ACTION_MOVE: { 222 if (mQuickStepStarted || !mAllowGestureDetection){ 223 break; 224 } 225 int x = (int) event.getX(); 226 int y = (int) event.getY(); 227 int xDiff = Math.abs(x - mTouchDownX); 228 int yDiff = Math.abs(y - mTouchDownY); 229 230 boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop; 231 int pos, touchDown, offset, trackSize; 232 233 if (mIsVertical) { 234 exceededScrubTouchSlop = 235 yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff; 236 exceededSwipeUpTouchSlop = 237 xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff; 238 pos = y; 239 touchDown = mTouchDownY; 240 offset = pos - mTrackRect.top; 241 trackSize = mTrackRect.height(); 242 } else { 243 exceededScrubTouchSlop = 244 xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff; 245 exceededSwipeUpTouchSlop = 246 yDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && yDiff > xDiff; 247 pos = x; 248 touchDown = mTouchDownX; 249 offset = pos - mTrackRect.left; 250 trackSize = mTrackRect.width(); 251 } 252 // Decide to start quickstep if dragging away from the navigation bar, otherwise in 253 // the parallel direction, decide to start quickscrub. Only one may run. 254 if (!mQuickScrubActive && exceededSwipeUpTouchSlop) { 255 if (mNavigationBarView.isQuickStepSwipeUpEnabled()) { 256 startQuickStep(event); 257 } 258 break; 259 } 260 261 // Do not handle quick scrub if disabled 262 if (!mNavigationBarView.isQuickScrubEnabled()) { 263 break; 264 } 265 266 if (!mDragPositive) { 267 offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width(); 268 } 269 270 final boolean allowDrag = !mDragPositive 271 ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown; 272 float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1); 273 if (allowDrag) { 274 // Passing the drag slop then touch slop will start quick step 275 if (!mQuickScrubActive && exceededScrubTouchSlop) { 276 startQuickScrub(); 277 } 278 } 279 280 if (mQuickScrubActive && (mDragPositive && offset >= 0 281 || !mDragPositive && offset <= 0)) { 282 try { 283 mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction); 284 if (DEBUG_OVERVIEW_PROXY) { 285 Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction); 286 } 287 } catch (RemoteException e) { 288 Log.e(TAG, "Failed to send progress of quick scrub.", e); 289 } 290 } 291 break; 292 } 293 case MotionEvent.ACTION_CANCEL: 294 case MotionEvent.ACTION_UP: 295 endQuickScrub(true /* animate */); 296 break; 297 } 298 299 // Proxy motion events to launcher if not handled by quick scrub 300 // Proxy motion events up/cancel that would be sent after long press on any nav button 301 if (!mQuickScrubActive && (mAllowGestureDetection || action == MotionEvent.ACTION_CANCEL 302 || action == MotionEvent.ACTION_UP)) { 303 proxyMotionEvents(event); 304 } 305 return mQuickScrubActive || mQuickStepStarted; 306 } 307 308 @Override 309 public void onDraw(Canvas canvas) { 310 if (!mNavigationBarView.isQuickScrubEnabled()) { 311 return; 312 } 313 int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor, 314 mDarkTrackColor); 315 int colorAlpha = ColorUtils.setAlphaComponent(color, 316 (int) (Color.alpha(color) * mTrackAlpha)); 317 mTrackDrawable.setTint(colorAlpha); 318 319 // Scale the track, but apply the inverse scale from the nav bar 320 canvas.save(); 321 canvas.scale(mTrackScale / mNavigationBarView.getScaleX(), 322 1f / mNavigationBarView.getScaleY(), 323 mTrackRect.centerX(), mTrackRect.centerY()); 324 mTrackDrawable.draw(canvas); 325 canvas.restore(); 326 } 327 328 @Override 329 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 330 final int paddingLeft = mNavigationBarView.getPaddingLeft(); 331 final int paddingTop = mNavigationBarView.getPaddingTop(); 332 final int paddingRight = mNavigationBarView.getPaddingRight(); 333 final int paddingBottom = mNavigationBarView.getPaddingBottom(); 334 final int width = (right - left) - paddingRight - paddingLeft; 335 final int height = (bottom - top) - paddingBottom - paddingTop; 336 final int x1, x2, y1, y2; 337 if (mIsVertical) { 338 x1 = (width - mTrackThickness) / 2 + paddingLeft; 339 x2 = x1 + mTrackThickness; 340 y1 = paddingTop + mTrackEndPadding; 341 y2 = y1 + height - 2 * mTrackEndPadding; 342 } else { 343 y1 = (height - mTrackThickness) / 2 + paddingTop; 344 y2 = y1 + mTrackThickness; 345 x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding; 346 x2 = x1 + width - 2 * mTrackEndPadding; 347 } 348 mTrackRect.set(x1, y1, x2, y2); 349 mTrackDrawable.setBounds(mTrackRect); 350 } 351 352 @Override 353 public void onDarkIntensityChange(float intensity) { 354 mDarkIntensity = intensity; 355 mNavigationBarView.invalidate(); 356 } 357 358 @Override 359 public void setBarState(boolean isVertical, boolean isRTL) { 360 final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL); 361 if (changed) { 362 // End quickscrub if the state changes mid-transition 363 endQuickScrub(false /* animate */); 364 } 365 mIsVertical = isVertical; 366 mIsRTL = isRTL; 367 try { 368 int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition(); 369 mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM; 370 if (isRTL) { 371 mDragPositive = !mDragPositive; 372 } 373 } catch (RemoteException e) { 374 Slog.e(TAG, "Failed to get nav bar position.", e); 375 } 376 } 377 378 @Override 379 public void onNavigationButtonLongPress(View v) { 380 mAllowGestureDetection = false; 381 mHandler.removeCallbacksAndMessages(null); 382 } 383 384 private void startQuickStep(MotionEvent event) { 385 mQuickStepStarted = true; 386 event.transform(mTransformGlobalMatrix); 387 try { 388 mOverviewEventSender.getProxy().onQuickStep(event); 389 if (DEBUG_OVERVIEW_PROXY) { 390 Log.d(TAG_OPS, "Quick Step Start"); 391 } 392 } catch (RemoteException e) { 393 Log.e(TAG, "Failed to send quick step started.", e); 394 } finally { 395 event.transform(mTransformLocalMatrix); 396 } 397 mOverviewEventSender.notifyQuickStepStarted(); 398 mHandler.removeCallbacksAndMessages(null); 399 400 if (mHitTarget != null) { 401 mHitTarget.abortCurrentGesture(); 402 } 403 404 if (mQuickScrubActive) { 405 animateEnd(); 406 } 407 } 408 409 private void startQuickScrub() { 410 if (!mQuickScrubActive) { 411 mQuickScrubActive = true; 412 mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light); 413 mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark); 414 415 ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, 416 PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f), 417 PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f)); 418 trackAnimator.setInterpolator(ALPHA_IN); 419 trackAnimator.setDuration(ANIM_IN_DURATION_MS); 420 ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f); 421 navBarAnimator.setInterpolator(ALPHA_OUT); 422 navBarAnimator.setDuration(ANIM_OUT_DURATION_MS); 423 mTrackAnimator = new AnimatorSet(); 424 mTrackAnimator.playTogether(trackAnimator, navBarAnimator); 425 mTrackAnimator.start(); 426 427 try { 428 mOverviewEventSender.getProxy().onQuickScrubStart(); 429 if (DEBUG_OVERVIEW_PROXY) { 430 Log.d(TAG_OPS, "Quick Scrub Start"); 431 } 432 } catch (RemoteException e) { 433 Log.e(TAG, "Failed to send start of quick scrub.", e); 434 } 435 mOverviewEventSender.notifyQuickScrubStarted(); 436 437 if (mHitTarget != null) { 438 mHitTarget.abortCurrentGesture(); 439 } 440 } 441 } 442 443 private void endQuickScrub(boolean animate) { 444 if (mQuickScrubActive) { 445 animateEnd(); 446 try { 447 mOverviewEventSender.getProxy().onQuickScrubEnd(); 448 if (DEBUG_OVERVIEW_PROXY) { 449 Log.d(TAG_OPS, "Quick Scrub End"); 450 } 451 } catch (RemoteException e) { 452 Log.e(TAG, "Failed to send end of quick scrub.", e); 453 } 454 } 455 if (!animate) { 456 if (mTrackAnimator != null) { 457 mTrackAnimator.end(); 458 mTrackAnimator = null; 459 } 460 } 461 } 462 463 private void animateEnd() { 464 if (mTrackAnimator != null) { 465 mTrackAnimator.cancel(); 466 } 467 468 ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this, 469 PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f), 470 PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE)); 471 trackAnimator.setInterpolator(ALPHA_OUT); 472 trackAnimator.setDuration(ANIM_OUT_DURATION_MS); 473 ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f); 474 navBarAnimator.setInterpolator(ALPHA_IN); 475 navBarAnimator.setDuration(ANIM_IN_DURATION_MS); 476 mTrackAnimator = new AnimatorSet(); 477 mTrackAnimator.playTogether(trackAnimator, navBarAnimator); 478 mTrackAnimator.addListener(mQuickScrubEndListener); 479 mTrackAnimator.start(); 480 } 481 482 private void resetQuickScrub() { 483 mQuickScrubActive = false; 484 mAllowGestureDetection = false; 485 mCurrentNavigationBarView = null; 486 } 487 488 private boolean proxyMotionEvents(MotionEvent event) { 489 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy(); 490 event.transform(mTransformGlobalMatrix); 491 try { 492 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 493 overviewProxy.onPreMotionEvent(mNavigationBarView.getDownHitTarget()); 494 } 495 overviewProxy.onMotionEvent(event); 496 if (DEBUG_OVERVIEW_PROXY) { 497 Log.d(TAG_OPS, "Send MotionEvent: " + event.toString()); 498 } 499 return true; 500 } catch (RemoteException e) { 501 Log.e(TAG, "Callback failed", e); 502 } finally { 503 event.transform(mTransformLocalMatrix); 504 } 505 return false; 506 } 507 } 508