blob: 2eea4bd50f791ad324a7e7cc5f0ec34c65a72c03 [file] [log] [blame]
Matthew Nga8f24262017-12-19 11:54:24 -08001/*
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
17package com.android.systemui.statusbar.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
Matthew Ng7090a802018-01-19 13:36:22 -080022import android.animation.ArgbEvaluator;
Matthew Nga8f24262017-12-19 11:54:24 -080023import android.animation.ObjectAnimator;
24import android.animation.ValueAnimator;
25import android.animation.ValueAnimator.AnimatorUpdateListener;
26import android.content.Context;
27import android.graphics.Canvas;
28import android.graphics.Paint;
29import android.graphics.Rect;
30import android.os.Handler;
31import android.os.RemoteException;
Matthew Ngf41f7c32018-01-22 17:57:05 -080032import android.os.SystemProperties;
Matthew Nga8f24262017-12-19 11:54:24 -080033import android.util.Log;
34import android.util.Slog;
35import android.view.Display;
36import android.view.GestureDetector;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.ViewConfiguration;
40import android.view.WindowManager;
41import android.view.WindowManagerGlobal;
42import android.view.animation.DecelerateInterpolator;
43import android.view.animation.Interpolator;
44import android.support.annotation.DimenRes;
45import com.android.systemui.Dependency;
46import com.android.systemui.OverviewProxyService;
47import com.android.systemui.R;
48import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
49import com.android.systemui.shared.recents.IOverviewProxy;
50import com.android.systemui.shared.recents.utilities.Utilities;
51
52import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
53import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
Matthew Ngbd824572018-01-17 16:25:56 -080054import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
55import static com.android.systemui.OverviewProxyService.TAG_OPS;
Winson Chungc4e06202018-02-13 10:37:35 -080056import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
Matthew Nga8f24262017-12-19 11:54:24 -080057
58/**
59 * Class to detect gestures on the navigation bar and implement quick scrub and switch.
60 */
61public class QuickScrubController extends GestureDetector.SimpleOnGestureListener implements
62 GestureHelper {
63
64 private static final String TAG = "QuickScrubController";
65 private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
66 private static final int ANIM_DURATION_MS = 200;
Winson Chungde2a1242018-02-07 15:59:43 -080067 private static final long LONG_PRESS_DELAY_MS = 225;
Matthew Nga8f24262017-12-19 11:54:24 -080068
69 /**
70 * For quick step, set a damping value to allow the button to stick closer its origin position
71 * when dragging before quick scrub is active.
72 */
73 private static final int SWITCH_STICKINESS = 4;
74
75 private NavigationBarView mNavigationBarView;
76 private GestureDetector mGestureDetector;
77
78 private boolean mDraggingActive;
79 private boolean mQuickScrubActive;
Winson Chung49658842018-02-08 12:52:21 -080080 private boolean mAllowQuickSwitch;
Matthew Nga8f24262017-12-19 11:54:24 -080081 private float mDownOffset;
82 private float mTranslation;
83 private int mTouchDownX;
84 private int mTouchDownY;
85 private boolean mDragPositive;
86 private boolean mIsVertical;
87 private boolean mIsRTL;
Matthew Ng7090a802018-01-19 13:36:22 -080088 private float mTrackAlpha;
89 private int mLightTrackColor;
90 private int mDarkTrackColor;
91 private float mDarkIntensity;
Winson Chungd10ca302018-02-14 10:13:41 -080092 private View mHomeButtonView;
Matthew Nga8f24262017-12-19 11:54:24 -080093
94 private final Handler mHandler = new Handler();
95 private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
96 private final Rect mTrackRect = new Rect();
Matthew Nga8f24262017-12-19 11:54:24 -080097 private final Paint mTrackPaint = new Paint();
98 private final int mScrollTouchSlop;
99 private final OverviewProxyService mOverviewEventSender;
Matthew Nga8f24262017-12-19 11:54:24 -0800100 private final int mTrackThickness;
101 private final int mTrackPadding;
102 private final ValueAnimator mTrackAnimator;
103 private final ValueAnimator mButtonAnimator;
104 private final AnimatorSet mQuickScrubEndAnimator;
105 private final Context mContext;
Matthew Ng7090a802018-01-19 13:36:22 -0800106 private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator();
Matthew Nga8f24262017-12-19 11:54:24 -0800107
108 private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
Matthew Ng7090a802018-01-19 13:36:22 -0800109 mTrackAlpha = (float) valueAnimator.getAnimatedValue();
Matthew Nga8f24262017-12-19 11:54:24 -0800110 mNavigationBarView.invalidate();
111 };
112
113 private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
114 int pos = (int) animator.getAnimatedValue();
115 if (!mQuickScrubActive) {
116 pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
117 }
Matthew Nga8f24262017-12-19 11:54:24 -0800118 if (mIsVertical) {
Winson Chungd10ca302018-02-14 10:13:41 -0800119 mHomeButtonView.setTranslationY(pos);
Matthew Nga8f24262017-12-19 11:54:24 -0800120 } else {
Winson Chungd10ca302018-02-14 10:13:41 -0800121 mHomeButtonView.setTranslationX(pos);
Matthew Nga8f24262017-12-19 11:54:24 -0800122 }
123 };
124
125 private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
126 @Override
127 public void onAnimationEnd(Animator animation) {
128 mNavigationBarView.getHomeButton().setClickable(true);
Winson Chungd10ca302018-02-14 10:13:41 -0800129 mHomeButtonView = null;
Matthew Nga8f24262017-12-19 11:54:24 -0800130 mQuickScrubActive = false;
131 mTranslation = 0;
132 }
133 };
134
135 private Runnable mLongPressRunnable = this::startQuickScrub;
136
137 private final GestureDetector.SimpleOnGestureListener mGestureListener =
138 new GestureDetector.SimpleOnGestureListener() {
139 @Override
140 public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
Winson Chung49658842018-02-08 12:52:21 -0800141 if (!isQuickScrubEnabled() || mQuickScrubActive || !mAllowQuickSwitch ||
Winson Chungc4e06202018-02-13 10:37:35 -0800142 mNavigationBarView.getDownHitTarget() != HIT_TARGET_HOME) {
Matthew Nga8f24262017-12-19 11:54:24 -0800143 return false;
144 }
145 float velocityX = mIsRTL ? -velX : velX;
146 float absVelY = Math.abs(velY);
147 final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
148 mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
149 if (isValidFling) {
150 mDraggingActive = false;
151 mButtonAnimator.setIntValues((int) mTranslation, 0);
152 mButtonAnimator.start();
153 mHandler.removeCallbacks(mLongPressRunnable);
154 try {
155 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
156 overviewProxy.onQuickSwitch();
Matthew Ngbd824572018-01-17 16:25:56 -0800157 if (DEBUG_OVERVIEW_PROXY) {
158 Log.d(TAG_OPS, "Quick Switch");
159 }
Matthew Nga8f24262017-12-19 11:54:24 -0800160 } catch (RemoteException e) {
161 Log.e(TAG, "Failed to send start of quick switch.", e);
162 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800163 return true;
Matthew Nga8f24262017-12-19 11:54:24 -0800164 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800165 return false;
Matthew Nga8f24262017-12-19 11:54:24 -0800166 }
167 };
168
169 public QuickScrubController(Context context) {
170 mContext = context;
171 mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Matthew Nga8f24262017-12-19 11:54:24 -0800172 mOverviewEventSender = Dependency.get(OverviewProxyService.class);
173 mGestureDetector = new GestureDetector(mContext, mGestureListener);
174 mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
175 mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
Matthew Ng7090a802018-01-19 13:36:22 -0800176 mTrackPaint.setAlpha(0);
Matthew Nga8f24262017-12-19 11:54:24 -0800177
178 mTrackAnimator = ObjectAnimator.ofFloat();
179 mTrackAnimator.addUpdateListener(mTrackAnimatorListener);
180 mButtonAnimator = ObjectAnimator.ofInt();
181 mButtonAnimator.addUpdateListener(mButtonTranslationListener);
182 mQuickScrubEndAnimator = new AnimatorSet();
183 mQuickScrubEndAnimator.playTogether(mTrackAnimator, mButtonAnimator);
184 mQuickScrubEndAnimator.setDuration(ANIM_DURATION_MS);
185 mQuickScrubEndAnimator.addListener(mQuickScrubEndListener);
186 mQuickScrubEndAnimator.setInterpolator(mQuickScrubEndInterpolator);
187 }
188
189 public void setComponents(NavigationBarView navigationBarView) {
190 mNavigationBarView = navigationBarView;
191 }
192
Winson Chung4faf38a2018-02-06 08:53:37 -0800193 /**
194 * @return true if we want to intercept touch events for quick scrub/switch and prevent proxying
195 * the event to the overview service.
196 */
Matthew Nga8f24262017-12-19 11:54:24 -0800197 @Override
198 public boolean onInterceptTouchEvent(MotionEvent event) {
199 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
200 final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
201 if (overviewProxy == null) {
202 homeButton.setDelayTouchFeedback(false);
203 return false;
204 }
Winson Chungde2a1242018-02-07 15:59:43 -0800205
206 return handleTouchEvent(event);
207 }
208
209 /**
210 * @return true if we want to handle touch events for quick scrub/switch and prevent proxying
211 * the event to the overview service.
212 */
213 @Override
214 public boolean onTouchEvent(MotionEvent event) {
215 return handleTouchEvent(event);
216 }
217
218 private boolean handleTouchEvent(MotionEvent event) {
219 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
220 final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
Winson Chung4faf38a2018-02-06 08:53:37 -0800221 if (mGestureDetector.onTouchEvent(event)) {
Winson Chungde2a1242018-02-07 15:59:43 -0800222 // If the fling has been handled on UP, then skip proxying the UP
Winson Chung4faf38a2018-02-06 08:53:37 -0800223 return true;
224 }
Matthew Nga8f24262017-12-19 11:54:24 -0800225 int action = event.getAction();
226 switch (action & MotionEvent.ACTION_MASK) {
227 case MotionEvent.ACTION_DOWN: {
228 int x = (int) event.getX();
229 int y = (int) event.getY();
Winson Chung0be8f082018-02-15 15:52:49 -0800230 // End any existing quickscrub animations before starting the new transition
231 if (mQuickScrubEndAnimator != null) {
232 mQuickScrubEndAnimator.end();
233 }
Winson Chungd10ca302018-02-14 10:13:41 -0800234 mHomeButtonView = homeButton.getCurrentView();
Winson Chungc4e06202018-02-13 10:37:35 -0800235 if (isQuickScrubEnabled()
236 && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME) {
Matthew Nga8f24262017-12-19 11:54:24 -0800237 mTouchDownX = x;
238 mTouchDownY = y;
239 homeButton.setDelayTouchFeedback(true);
240 mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
241 } else {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800242 homeButton.setDelayTouchFeedback(false);
Matthew Nga8f24262017-12-19 11:54:24 -0800243 mTouchDownX = mTouchDownY = -1;
244 }
Winson Chung49658842018-02-08 12:52:21 -0800245 mAllowQuickSwitch = true;
Matthew Nga8f24262017-12-19 11:54:24 -0800246 break;
247 }
248 case MotionEvent.ACTION_MOVE: {
249 if (mTouchDownX != -1) {
250 int x = (int) event.getX();
251 int y = (int) event.getY();
252 int xDiff = Math.abs(x - mTouchDownX);
253 int yDiff = Math.abs(y - mTouchDownY);
Matthew Nge0903c92018-01-17 15:32:41 -0800254 boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
255 boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
256 boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
Matthew Nga8f24262017-12-19 11:54:24 -0800257 int pos, touchDown, offset, trackSize;
Matthew Nge0903c92018-01-17 15:32:41 -0800258
Matthew Nga8f24262017-12-19 11:54:24 -0800259 if (mIsVertical) {
Matthew Nge0903c92018-01-17 15:32:41 -0800260 exceededTouchSlop = exceededTouchSlopY;
261 exceededPerpendicularTouchSlop = exceededTouchSlopX;
Matthew Nga8f24262017-12-19 11:54:24 -0800262 pos = y;
263 touchDown = mTouchDownY;
264 offset = pos - mTrackRect.top;
265 trackSize = mTrackRect.height();
266 } else {
Matthew Nge0903c92018-01-17 15:32:41 -0800267 exceededTouchSlop = exceededTouchSlopX;
268 exceededPerpendicularTouchSlop = exceededTouchSlopY;
Matthew Nga8f24262017-12-19 11:54:24 -0800269 pos = x;
270 touchDown = mTouchDownX;
271 offset = pos - mTrackRect.left;
272 trackSize = mTrackRect.width();
273 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800274 // Do not start scrubbing when dragging in the perpendicular direction if we
275 // haven't already started quickscrub
276 if (!mDraggingActive && !mQuickScrubActive && exceededPerpendicularTouchSlop) {
Matthew Nge0903c92018-01-17 15:32:41 -0800277 mHandler.removeCallbacksAndMessages(null);
278 return false;
279 }
Matthew Nga8f24262017-12-19 11:54:24 -0800280 if (!mDragPositive) {
281 offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
282 }
283
284 // Control the button movement
285 if (!mDraggingActive && exceededTouchSlop) {
286 boolean allowDrag = !mDragPositive
287 ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
288 if (allowDrag) {
289 mDownOffset = offset;
290 homeButton.setClickable(false);
291 mDraggingActive = true;
292 }
293 }
294 if (mDraggingActive && (mDragPositive && offset >= 0
295 || !mDragPositive && offset <= 0)) {
296 float scrubFraction =
297 Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
298 mTranslation = !mDragPositive
299 ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
300 : Utilities.clamp(offset - mDownOffset, 0, trackSize);
301 if (mQuickScrubActive) {
302 try {
303 overviewProxy.onQuickScrubProgress(scrubFraction);
Matthew Ngbd824572018-01-17 16:25:56 -0800304 if (DEBUG_OVERVIEW_PROXY) {
305 Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
306 }
Matthew Nga8f24262017-12-19 11:54:24 -0800307 } catch (RemoteException e) {
308 Log.e(TAG, "Failed to send progress of quick scrub.", e);
309 }
310 } else {
311 mTranslation /= SWITCH_STICKINESS;
312 }
313 if (mIsVertical) {
Winson Chungd10ca302018-02-14 10:13:41 -0800314 mHomeButtonView.setTranslationY(mTranslation);
Matthew Nga8f24262017-12-19 11:54:24 -0800315 } else {
Winson Chungd10ca302018-02-14 10:13:41 -0800316 mHomeButtonView.setTranslationX(mTranslation);
Matthew Nga8f24262017-12-19 11:54:24 -0800317 }
318 }
319 }
320 break;
321 }
322 case MotionEvent.ACTION_CANCEL:
323 case MotionEvent.ACTION_UP:
Winson Chungd10ca302018-02-14 10:13:41 -0800324 endQuickScrub(true /* animate */);
Matthew Nga8f24262017-12-19 11:54:24 -0800325 break;
326 }
327 return mDraggingActive || mQuickScrubActive;
328 }
329
330 @Override
331 public void onDraw(Canvas canvas) {
Matthew Ng7090a802018-01-19 13:36:22 -0800332 int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor,
333 mDarkTrackColor);
334 mTrackPaint.setColor(color);
335 mTrackPaint.setAlpha((int) (mTrackPaint.getAlpha() * mTrackAlpha));
Matthew Nga8f24262017-12-19 11:54:24 -0800336 canvas.drawRect(mTrackRect, mTrackPaint);
337 }
338
339 @Override
340 public void onLayout(boolean changed, int left, int top, int right, int bottom) {
341 final int width = right - left;
342 final int height = bottom - top;
343 final int x1, x2, y1, y2;
344 if (mIsVertical) {
345 x1 = (width - mTrackThickness) / 2;
346 x2 = x1 + mTrackThickness;
347 y1 = mDragPositive ? height / 2 : mTrackPadding;
348 y2 = y1 + height / 2 - mTrackPadding;
349 } else {
350 y1 = (height - mTrackThickness) / 2;
351 y2 = y1 + mTrackThickness;
352 x1 = mDragPositive ? width / 2 : mTrackPadding;
353 x2 = x1 + width / 2 - mTrackPadding;
354 }
355 mTrackRect.set(x1, y1, x2, y2);
Matthew Nga8f24262017-12-19 11:54:24 -0800356 }
357
358 @Override
359 public void onDarkIntensityChange(float intensity) {
Matthew Ng7090a802018-01-19 13:36:22 -0800360 mDarkIntensity = intensity;
361 mNavigationBarView.invalidate();
Matthew Nga8f24262017-12-19 11:54:24 -0800362 }
363
364 @Override
Matthew Nga8f24262017-12-19 11:54:24 -0800365 public void setBarState(boolean isVertical, boolean isRTL) {
Winson Chungd10ca302018-02-14 10:13:41 -0800366 final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL);
367 if (changed) {
368 // End quickscrub if the state changes mid-transition
369 endQuickScrub(false /* animate */);
370 }
Matthew Nga8f24262017-12-19 11:54:24 -0800371 mIsVertical = isVertical;
372 mIsRTL = isRTL;
373 try {
374 int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
375 mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
376 if (isRTL) {
377 mDragPositive = !mDragPositive;
378 }
379 } catch (RemoteException e) {
380 Slog.e(TAG, "Failed to get nav bar position.", e);
381 }
382 }
383
Matthew Ngf41f7c32018-01-22 17:57:05 -0800384 boolean isQuickScrubEnabled() {
Matthew Ngcc5bddd2018-02-13 16:00:34 -0800385 return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", true);
Matthew Ngf41f7c32018-01-22 17:57:05 -0800386 }
387
Matthew Nga8f24262017-12-19 11:54:24 -0800388 private void startQuickScrub() {
389 if (!mQuickScrubActive) {
390 mQuickScrubActive = true;
Matthew Ng7090a802018-01-19 13:36:22 -0800391 mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
392 mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
393 mTrackAnimator.setFloatValues(0, 1);
Matthew Nga8f24262017-12-19 11:54:24 -0800394 mTrackAnimator.start();
395 try {
396 mOverviewEventSender.getProxy().onQuickScrubStart();
Matthew Ngbd824572018-01-17 16:25:56 -0800397 if (DEBUG_OVERVIEW_PROXY) {
398 Log.d(TAG_OPS, "Quick Scrub Start");
399 }
Matthew Nga8f24262017-12-19 11:54:24 -0800400 } catch (RemoteException e) {
401 Log.e(TAG, "Failed to send start of quick scrub.", e);
402 }
403 }
404 }
405
Winson Chungd10ca302018-02-14 10:13:41 -0800406 private void endQuickScrub(boolean animate) {
Matthew Nga8f24262017-12-19 11:54:24 -0800407 mHandler.removeCallbacks(mLongPressRunnable);
408 if (mDraggingActive || mQuickScrubActive) {
409 mButtonAnimator.setIntValues((int) mTranslation, 0);
Matthew Ng7090a802018-01-19 13:36:22 -0800410 mTrackAnimator.setFloatValues(mTrackAlpha, 0);
Matthew Nga8f24262017-12-19 11:54:24 -0800411 mQuickScrubEndAnimator.start();
412 try {
413 mOverviewEventSender.getProxy().onQuickScrubEnd();
Matthew Ngbd824572018-01-17 16:25:56 -0800414 if (DEBUG_OVERVIEW_PROXY) {
415 Log.d(TAG_OPS, "Quick Scrub End");
416 }
Matthew Nga8f24262017-12-19 11:54:24 -0800417 } catch (RemoteException e) {
418 Log.e(TAG, "Failed to send end of quick scrub.", e);
419 }
Winson Chungd10ca302018-02-14 10:13:41 -0800420 if (!animate) {
421 mQuickScrubEndAnimator.end();
422 }
Matthew Nga8f24262017-12-19 11:54:24 -0800423 }
424 mDraggingActive = false;
425 }
426
Winson Chung49658842018-02-08 12:52:21 -0800427 public void cancelQuickSwitch() {
428 mAllowQuickSwitch = false;
429 mHandler.removeCallbacks(mLongPressRunnable);
430 }
431
Matthew Nga8f24262017-12-19 11:54:24 -0800432 private int getDimensionPixelSize(Context context, @DimenRes int resId) {
433 return context.getResources().getDimensionPixelSize(resId);
434 }
435}