blob: dc0ea1cba653786f9399ef098e2112b1065d955a [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;
32import android.util.Log;
33import android.util.Slog;
Matthew Nga8f24262017-12-19 11:54:24 -080034import android.view.GestureDetector;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewConfiguration;
Matthew Nga8f24262017-12-19 11:54:24 -080038import android.view.WindowManagerGlobal;
39import android.view.animation.DecelerateInterpolator;
40import android.view.animation.Interpolator;
41import android.support.annotation.DimenRes;
42import com.android.systemui.Dependency;
43import com.android.systemui.OverviewProxyService;
44import com.android.systemui.R;
45import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
46import com.android.systemui.shared.recents.IOverviewProxy;
47import com.android.systemui.shared.recents.utilities.Utilities;
48
49import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
50import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
Matthew Ngbd824572018-01-17 16:25:56 -080051import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
52import static com.android.systemui.OverviewProxyService.TAG_OPS;
Winson Chungc4e06202018-02-13 10:37:35 -080053import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
Matthew Nga8f24262017-12-19 11:54:24 -080054
55/**
56 * Class to detect gestures on the navigation bar and implement quick scrub and switch.
57 */
58public class QuickScrubController extends GestureDetector.SimpleOnGestureListener implements
59 GestureHelper {
60
61 private static final String TAG = "QuickScrubController";
62 private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
63 private static final int ANIM_DURATION_MS = 200;
Winson Chungde2a1242018-02-07 15:59:43 -080064 private static final long LONG_PRESS_DELAY_MS = 225;
Matthew Nga8f24262017-12-19 11:54:24 -080065
66 /**
67 * For quick step, set a damping value to allow the button to stick closer its origin position
68 * when dragging before quick scrub is active.
69 */
70 private static final int SWITCH_STICKINESS = 4;
71
72 private NavigationBarView mNavigationBarView;
73 private GestureDetector mGestureDetector;
74
75 private boolean mDraggingActive;
76 private boolean mQuickScrubActive;
Winson Chung49658842018-02-08 12:52:21 -080077 private boolean mAllowQuickSwitch;
Matthew Ngdb2734c2018-02-16 16:02:20 -080078 private boolean mRecentsAnimationStarted;
Matthew Nga8f24262017-12-19 11:54:24 -080079 private float mDownOffset;
80 private float mTranslation;
81 private int mTouchDownX;
82 private int mTouchDownY;
83 private boolean mDragPositive;
84 private boolean mIsVertical;
85 private boolean mIsRTL;
Matthew Ng7090a802018-01-19 13:36:22 -080086 private float mTrackAlpha;
87 private int mLightTrackColor;
88 private int mDarkTrackColor;
89 private float mDarkIntensity;
Winson Chungd10ca302018-02-14 10:13:41 -080090 private View mHomeButtonView;
Matthew Nga8f24262017-12-19 11:54:24 -080091
92 private final Handler mHandler = new Handler();
93 private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
94 private final Rect mTrackRect = new Rect();
Matthew Nga8f24262017-12-19 11:54:24 -080095 private final Paint mTrackPaint = new Paint();
96 private final int mScrollTouchSlop;
97 private final OverviewProxyService mOverviewEventSender;
Matthew Nga8f24262017-12-19 11:54:24 -080098 private final int mTrackThickness;
99 private final int mTrackPadding;
100 private final ValueAnimator mTrackAnimator;
101 private final ValueAnimator mButtonAnimator;
102 private final AnimatorSet mQuickScrubEndAnimator;
103 private final Context mContext;
Matthew Ng7090a802018-01-19 13:36:22 -0800104 private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator();
Matthew Nga8f24262017-12-19 11:54:24 -0800105
106 private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
Matthew Ng7090a802018-01-19 13:36:22 -0800107 mTrackAlpha = (float) valueAnimator.getAnimatedValue();
Matthew Nga8f24262017-12-19 11:54:24 -0800108 mNavigationBarView.invalidate();
109 };
110
111 private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
112 int pos = (int) animator.getAnimatedValue();
113 if (!mQuickScrubActive) {
114 pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
115 }
Matthew Nga8f24262017-12-19 11:54:24 -0800116 if (mIsVertical) {
Winson Chungd10ca302018-02-14 10:13:41 -0800117 mHomeButtonView.setTranslationY(pos);
Matthew Nga8f24262017-12-19 11:54:24 -0800118 } else {
Winson Chungd10ca302018-02-14 10:13:41 -0800119 mHomeButtonView.setTranslationX(pos);
Matthew Nga8f24262017-12-19 11:54:24 -0800120 }
121 };
122
123 private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
124 @Override
125 public void onAnimationEnd(Animator animation) {
126 mNavigationBarView.getHomeButton().setClickable(true);
Winson Chungd10ca302018-02-14 10:13:41 -0800127 mHomeButtonView = null;
Matthew Nga8f24262017-12-19 11:54:24 -0800128 mQuickScrubActive = false;
129 mTranslation = 0;
130 }
131 };
132
133 private Runnable mLongPressRunnable = this::startQuickScrub;
134
135 private final GestureDetector.SimpleOnGestureListener mGestureListener =
136 new GestureDetector.SimpleOnGestureListener() {
137 @Override
138 public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
Matthew Ng8f25fb962018-01-16 17:17:24 -0800139 if (!mNavigationBarView.isQuickScrubEnabled() || mQuickScrubActive
140 || !mAllowQuickSwitch
141 || mNavigationBarView.getDownHitTarget() != HIT_TARGET_HOME) {
Matthew Nga8f24262017-12-19 11:54:24 -0800142 return false;
143 }
144 float velocityX = mIsRTL ? -velX : velX;
145 float absVelY = Math.abs(velY);
146 final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
147 mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
148 if (isValidFling) {
149 mDraggingActive = false;
150 mButtonAnimator.setIntValues((int) mTranslation, 0);
151 mButtonAnimator.start();
152 mHandler.removeCallbacks(mLongPressRunnable);
153 try {
154 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
155 overviewProxy.onQuickSwitch();
Matthew Ngbd824572018-01-17 16:25:56 -0800156 if (DEBUG_OVERVIEW_PROXY) {
157 Log.d(TAG_OPS, "Quick Switch");
158 }
Matthew Nga8f24262017-12-19 11:54:24 -0800159 } catch (RemoteException e) {
160 Log.e(TAG, "Failed to send start of quick switch.", e);
161 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800162 return true;
Matthew Nga8f24262017-12-19 11:54:24 -0800163 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800164 return false;
Matthew Nga8f24262017-12-19 11:54:24 -0800165 }
166 };
167
168 public QuickScrubController(Context context) {
169 mContext = context;
170 mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Matthew Nga8f24262017-12-19 11:54:24 -0800171 mOverviewEventSender = Dependency.get(OverviewProxyService.class);
172 mGestureDetector = new GestureDetector(mContext, mGestureListener);
173 mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
174 mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
Matthew Ng7090a802018-01-19 13:36:22 -0800175 mTrackPaint.setAlpha(0);
Matthew Nga8f24262017-12-19 11:54:24 -0800176
177 mTrackAnimator = ObjectAnimator.ofFloat();
178 mTrackAnimator.addUpdateListener(mTrackAnimatorListener);
179 mButtonAnimator = ObjectAnimator.ofInt();
180 mButtonAnimator.addUpdateListener(mButtonTranslationListener);
181 mQuickScrubEndAnimator = new AnimatorSet();
182 mQuickScrubEndAnimator.playTogether(mTrackAnimator, mButtonAnimator);
183 mQuickScrubEndAnimator.setDuration(ANIM_DURATION_MS);
184 mQuickScrubEndAnimator.addListener(mQuickScrubEndListener);
185 mQuickScrubEndAnimator.setInterpolator(mQuickScrubEndInterpolator);
186 }
187
188 public void setComponents(NavigationBarView navigationBarView) {
189 mNavigationBarView = navigationBarView;
190 }
191
Winson Chung4faf38a2018-02-06 08:53:37 -0800192 /**
193 * @return true if we want to intercept touch events for quick scrub/switch and prevent proxying
194 * the event to the overview service.
195 */
Matthew Nga8f24262017-12-19 11:54:24 -0800196 @Override
197 public boolean onInterceptTouchEvent(MotionEvent event) {
Matthew Nga8f24262017-12-19 11:54:24 -0800198 final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
Matthew Ng8f25fb962018-01-16 17:17:24 -0800199 if (!mNavigationBarView.isQuickScrubEnabled()) {
Matthew Nga8f24262017-12-19 11:54:24 -0800200 homeButton.setDelayTouchFeedback(false);
201 return false;
202 }
Winson Chungde2a1242018-02-07 15:59:43 -0800203
204 return handleTouchEvent(event);
205 }
206
207 /**
208 * @return true if we want to handle touch events for quick scrub/switch and prevent proxying
209 * the event to the overview service.
210 */
211 @Override
212 public boolean onTouchEvent(MotionEvent event) {
213 return handleTouchEvent(event);
214 }
215
216 private boolean handleTouchEvent(MotionEvent event) {
217 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
218 final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
Winson Chung4faf38a2018-02-06 08:53:37 -0800219 if (mGestureDetector.onTouchEvent(event)) {
Winson Chungde2a1242018-02-07 15:59:43 -0800220 // If the fling has been handled on UP, then skip proxying the UP
Winson Chung4faf38a2018-02-06 08:53:37 -0800221 return true;
222 }
Matthew Nga8f24262017-12-19 11:54:24 -0800223 int action = event.getAction();
224 switch (action & MotionEvent.ACTION_MASK) {
225 case MotionEvent.ACTION_DOWN: {
226 int x = (int) event.getX();
227 int y = (int) event.getY();
Winson Chung0be8f082018-02-15 15:52:49 -0800228 // End any existing quickscrub animations before starting the new transition
229 if (mQuickScrubEndAnimator != null) {
230 mQuickScrubEndAnimator.end();
231 }
Winson Chungd10ca302018-02-14 10:13:41 -0800232 mHomeButtonView = homeButton.getCurrentView();
Matthew Ng8f25fb962018-01-16 17:17:24 -0800233 if (mNavigationBarView.isQuickScrubEnabled()
Winson Chungc4e06202018-02-13 10:37:35 -0800234 && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME) {
Matthew Nga8f24262017-12-19 11:54:24 -0800235 mTouchDownX = x;
236 mTouchDownY = y;
237 homeButton.setDelayTouchFeedback(true);
238 mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
239 } else {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800240 homeButton.setDelayTouchFeedback(false);
Matthew Nga8f24262017-12-19 11:54:24 -0800241 mTouchDownX = mTouchDownY = -1;
242 }
Winson Chung49658842018-02-08 12:52:21 -0800243 mAllowQuickSwitch = true;
Matthew Nga8f24262017-12-19 11:54:24 -0800244 break;
245 }
246 case MotionEvent.ACTION_MOVE: {
247 if (mTouchDownX != -1) {
248 int x = (int) event.getX();
249 int y = (int) event.getY();
250 int xDiff = Math.abs(x - mTouchDownX);
251 int yDiff = Math.abs(y - mTouchDownY);
Matthew Nge0903c92018-01-17 15:32:41 -0800252 boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
253 boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
254 boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
Matthew Nga8f24262017-12-19 11:54:24 -0800255 int pos, touchDown, offset, trackSize;
Matthew Nge0903c92018-01-17 15:32:41 -0800256
Matthew Nga8f24262017-12-19 11:54:24 -0800257 if (mIsVertical) {
Matthew Nge0903c92018-01-17 15:32:41 -0800258 exceededTouchSlop = exceededTouchSlopY;
259 exceededPerpendicularTouchSlop = exceededTouchSlopX;
Matthew Nga8f24262017-12-19 11:54:24 -0800260 pos = y;
261 touchDown = mTouchDownY;
262 offset = pos - mTrackRect.top;
263 trackSize = mTrackRect.height();
264 } else {
Matthew Nge0903c92018-01-17 15:32:41 -0800265 exceededTouchSlop = exceededTouchSlopX;
266 exceededPerpendicularTouchSlop = exceededTouchSlopY;
Matthew Nga8f24262017-12-19 11:54:24 -0800267 pos = x;
268 touchDown = mTouchDownX;
269 offset = pos - mTrackRect.left;
270 trackSize = mTrackRect.width();
271 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800272 // Do not start scrubbing when dragging in the perpendicular direction if we
273 // haven't already started quickscrub
274 if (!mDraggingActive && !mQuickScrubActive && exceededPerpendicularTouchSlop) {
Matthew Nge0903c92018-01-17 15:32:41 -0800275 mHandler.removeCallbacksAndMessages(null);
276 return false;
277 }
Matthew Nga8f24262017-12-19 11:54:24 -0800278 if (!mDragPositive) {
279 offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
280 }
281
282 // Control the button movement
Matthew Ngdb2734c2018-02-16 16:02:20 -0800283 if (!mDraggingActive && exceededTouchSlop && !mRecentsAnimationStarted) {
Matthew Nga8f24262017-12-19 11:54:24 -0800284 boolean allowDrag = !mDragPositive
285 ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
286 if (allowDrag) {
287 mDownOffset = offset;
288 homeButton.setClickable(false);
289 mDraggingActive = true;
290 }
291 }
292 if (mDraggingActive && (mDragPositive && offset >= 0
293 || !mDragPositive && offset <= 0)) {
294 float scrubFraction =
295 Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
296 mTranslation = !mDragPositive
297 ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
298 : Utilities.clamp(offset - mDownOffset, 0, trackSize);
299 if (mQuickScrubActive) {
300 try {
Matthew Ng8f25fb962018-01-16 17:17:24 -0800301 mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
Matthew Ngbd824572018-01-17 16:25:56 -0800302 if (DEBUG_OVERVIEW_PROXY) {
303 Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
304 }
Matthew Nga8f24262017-12-19 11:54:24 -0800305 } catch (RemoteException e) {
306 Log.e(TAG, "Failed to send progress of quick scrub.", e);
307 }
308 } else {
309 mTranslation /= SWITCH_STICKINESS;
310 }
311 if (mIsVertical) {
Winson Chungd10ca302018-02-14 10:13:41 -0800312 mHomeButtonView.setTranslationY(mTranslation);
Matthew Nga8f24262017-12-19 11:54:24 -0800313 } else {
Winson Chungd10ca302018-02-14 10:13:41 -0800314 mHomeButtonView.setTranslationX(mTranslation);
Matthew Nga8f24262017-12-19 11:54:24 -0800315 }
316 }
317 }
318 break;
319 }
320 case MotionEvent.ACTION_CANCEL:
321 case MotionEvent.ACTION_UP:
Winson Chungd10ca302018-02-14 10:13:41 -0800322 endQuickScrub(true /* animate */);
Matthew Nga8f24262017-12-19 11:54:24 -0800323 break;
324 }
325 return mDraggingActive || mQuickScrubActive;
326 }
327
328 @Override
329 public void onDraw(Canvas canvas) {
Matthew Ng7090a802018-01-19 13:36:22 -0800330 int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor,
331 mDarkTrackColor);
332 mTrackPaint.setColor(color);
333 mTrackPaint.setAlpha((int) (mTrackPaint.getAlpha() * mTrackAlpha));
Matthew Nga8f24262017-12-19 11:54:24 -0800334 canvas.drawRect(mTrackRect, mTrackPaint);
335 }
336
337 @Override
338 public void onLayout(boolean changed, int left, int top, int right, int bottom) {
339 final int width = right - left;
340 final int height = bottom - top;
341 final int x1, x2, y1, y2;
342 if (mIsVertical) {
343 x1 = (width - mTrackThickness) / 2;
344 x2 = x1 + mTrackThickness;
345 y1 = mDragPositive ? height / 2 : mTrackPadding;
346 y2 = y1 + height / 2 - mTrackPadding;
347 } else {
348 y1 = (height - mTrackThickness) / 2;
349 y2 = y1 + mTrackThickness;
350 x1 = mDragPositive ? width / 2 : mTrackPadding;
351 x2 = x1 + width / 2 - mTrackPadding;
352 }
353 mTrackRect.set(x1, y1, x2, y2);
Matthew Nga8f24262017-12-19 11:54:24 -0800354 }
355
356 @Override
357 public void onDarkIntensityChange(float intensity) {
Matthew Ng7090a802018-01-19 13:36:22 -0800358 mDarkIntensity = intensity;
359 mNavigationBarView.invalidate();
Matthew Nga8f24262017-12-19 11:54:24 -0800360 }
361
362 @Override
Matthew Nga8f24262017-12-19 11:54:24 -0800363 public void setBarState(boolean isVertical, boolean isRTL) {
Winson Chungd10ca302018-02-14 10:13:41 -0800364 final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL);
365 if (changed) {
366 // End quickscrub if the state changes mid-transition
367 endQuickScrub(false /* animate */);
368 }
Matthew Nga8f24262017-12-19 11:54:24 -0800369 mIsVertical = isVertical;
370 mIsRTL = isRTL;
371 try {
372 int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
373 mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
374 if (isRTL) {
375 mDragPositive = !mDragPositive;
376 }
377 } catch (RemoteException e) {
378 Slog.e(TAG, "Failed to get nav bar position.", e);
379 }
380 }
381
382 private void startQuickScrub() {
383 if (!mQuickScrubActive) {
384 mQuickScrubActive = true;
Matthew Ng7090a802018-01-19 13:36:22 -0800385 mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
386 mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
387 mTrackAnimator.setFloatValues(0, 1);
Matthew Nga8f24262017-12-19 11:54:24 -0800388 mTrackAnimator.start();
389 try {
390 mOverviewEventSender.getProxy().onQuickScrubStart();
Matthew Ngbd824572018-01-17 16:25:56 -0800391 if (DEBUG_OVERVIEW_PROXY) {
392 Log.d(TAG_OPS, "Quick Scrub Start");
393 }
Matthew Nga8f24262017-12-19 11:54:24 -0800394 } catch (RemoteException e) {
395 Log.e(TAG, "Failed to send start of quick scrub.", e);
396 }
397 }
398 }
399
Winson Chungd10ca302018-02-14 10:13:41 -0800400 private void endQuickScrub(boolean animate) {
Matthew Nga8f24262017-12-19 11:54:24 -0800401 mHandler.removeCallbacks(mLongPressRunnable);
402 if (mDraggingActive || mQuickScrubActive) {
403 mButtonAnimator.setIntValues((int) mTranslation, 0);
Matthew Ng7090a802018-01-19 13:36:22 -0800404 mTrackAnimator.setFloatValues(mTrackAlpha, 0);
Matthew Nga8f24262017-12-19 11:54:24 -0800405 mQuickScrubEndAnimator.start();
406 try {
407 mOverviewEventSender.getProxy().onQuickScrubEnd();
Matthew Ngbd824572018-01-17 16:25:56 -0800408 if (DEBUG_OVERVIEW_PROXY) {
409 Log.d(TAG_OPS, "Quick Scrub End");
410 }
Matthew Nga8f24262017-12-19 11:54:24 -0800411 } catch (RemoteException e) {
412 Log.e(TAG, "Failed to send end of quick scrub.", e);
413 }
Winson Chungd10ca302018-02-14 10:13:41 -0800414 if (!animate) {
415 mQuickScrubEndAnimator.end();
416 }
Matthew Nga8f24262017-12-19 11:54:24 -0800417 }
418 mDraggingActive = false;
419 }
420
Matthew Ngdb2734c2018-02-16 16:02:20 -0800421 public void setRecentsAnimationStarted(boolean started) {
422 mRecentsAnimationStarted = started;
423 if (started) {
424 cancelQuickSwitch();
425 }
426 }
427
Winson Chung49658842018-02-08 12:52:21 -0800428 public void cancelQuickSwitch() {
429 mAllowQuickSwitch = false;
430 mHandler.removeCallbacks(mLongPressRunnable);
431 }
432
Matthew Nga8f24262017-12-19 11:54:24 -0800433 private int getDimensionPixelSize(Context context, @DimenRes int resId) {
434 return context.getResources().getDimensionPixelSize(resId);
435 }
436}