blob: 6bfaaf49b65b200ab4f27c21f47a12f31af17bd4 [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;
Matthew Nga8f24262017-12-19 11:54:24 -080056
57/**
58 * Class to detect gestures on the navigation bar and implement quick scrub and switch.
59 */
60public class QuickScrubController extends GestureDetector.SimpleOnGestureListener implements
61 GestureHelper {
62
63 private static final String TAG = "QuickScrubController";
64 private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
65 private static final int ANIM_DURATION_MS = 200;
66 private static final long LONG_PRESS_DELAY_MS = 150;
67
68 /**
69 * For quick step, set a damping value to allow the button to stick closer its origin position
70 * when dragging before quick scrub is active.
71 */
72 private static final int SWITCH_STICKINESS = 4;
73
74 private NavigationBarView mNavigationBarView;
75 private GestureDetector mGestureDetector;
76
77 private boolean mDraggingActive;
78 private boolean mQuickScrubActive;
79 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;
Matthew Nga8f24262017-12-19 11:54:24 -080090
91 private final Handler mHandler = new Handler();
92 private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
93 private final Rect mTrackRect = new Rect();
94 private final Rect mHomeButtonRect = new Rect();
95 private final Paint mTrackPaint = new Paint();
96 private final int mScrollTouchSlop;
97 private final OverviewProxyService mOverviewEventSender;
98 private final Display mDisplay;
99 private final int mTrackThickness;
100 private final int mTrackPadding;
101 private final ValueAnimator mTrackAnimator;
102 private final ValueAnimator mButtonAnimator;
103 private final AnimatorSet mQuickScrubEndAnimator;
104 private final Context mContext;
Matthew Ng7090a802018-01-19 13:36:22 -0800105 private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator();
Matthew Nga8f24262017-12-19 11:54:24 -0800106
107 private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
Matthew Ng7090a802018-01-19 13:36:22 -0800108 mTrackAlpha = (float) valueAnimator.getAnimatedValue();
Matthew Nga8f24262017-12-19 11:54:24 -0800109 mNavigationBarView.invalidate();
110 };
111
112 private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
113 int pos = (int) animator.getAnimatedValue();
114 if (!mQuickScrubActive) {
115 pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
116 }
117 final View homeView = mNavigationBarView.getHomeButton().getCurrentView();
118 if (mIsVertical) {
119 homeView.setTranslationY(pos);
120 } else {
121 homeView.setTranslationX(pos);
122 }
123 };
124
125 private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
126 @Override
127 public void onAnimationEnd(Animator animation) {
128 mNavigationBarView.getHomeButton().setClickable(true);
129 mQuickScrubActive = false;
130 mTranslation = 0;
131 }
132 };
133
134 private Runnable mLongPressRunnable = this::startQuickScrub;
135
136 private final GestureDetector.SimpleOnGestureListener mGestureListener =
137 new GestureDetector.SimpleOnGestureListener() {
138 @Override
139 public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800140 if (!isQuickScrubEnabled() || mQuickScrubActive) {
Matthew Nga8f24262017-12-19 11:54:24 -0800141 return false;
142 }
143 float velocityX = mIsRTL ? -velX : velX;
144 float absVelY = Math.abs(velY);
145 final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
146 mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
147 if (isValidFling) {
148 mDraggingActive = false;
149 mButtonAnimator.setIntValues((int) mTranslation, 0);
150 mButtonAnimator.start();
151 mHandler.removeCallbacks(mLongPressRunnable);
152 try {
153 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
154 overviewProxy.onQuickSwitch();
Matthew Ngbd824572018-01-17 16:25:56 -0800155 if (DEBUG_OVERVIEW_PROXY) {
156 Log.d(TAG_OPS, "Quick Switch");
157 }
Matthew Nga8f24262017-12-19 11:54:24 -0800158 } catch (RemoteException e) {
159 Log.e(TAG, "Failed to send start of quick switch.", e);
160 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800161 return true;
Matthew Nga8f24262017-12-19 11:54:24 -0800162 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800163 return false;
Matthew Nga8f24262017-12-19 11:54:24 -0800164 }
165 };
166
167 public QuickScrubController(Context context) {
168 mContext = context;
169 mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
170 mDisplay = ((WindowManager) context.getSystemService(
171 Context.WINDOW_SERVICE)).getDefaultDisplay();
172 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 Chung4faf38a2018-02-06 08:53:37 -0800205 if (mGestureDetector.onTouchEvent(event)) {
206 // If the fling has been handled, then skip proxying the UP
207 return true;
208 }
Matthew Nga8f24262017-12-19 11:54:24 -0800209 int action = event.getAction();
210 switch (action & MotionEvent.ACTION_MASK) {
211 case MotionEvent.ACTION_DOWN: {
212 int x = (int) event.getX();
213 int y = (int) event.getY();
Matthew Ngf41f7c32018-01-22 17:57:05 -0800214 if (isQuickScrubEnabled() && mHomeButtonRect.contains(x, y)) {
Matthew Nga8f24262017-12-19 11:54:24 -0800215 mTouchDownX = x;
216 mTouchDownY = y;
217 homeButton.setDelayTouchFeedback(true);
218 mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
219 } else {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800220 homeButton.setDelayTouchFeedback(false);
Matthew Nga8f24262017-12-19 11:54:24 -0800221 mTouchDownX = mTouchDownY = -1;
222 }
223 break;
224 }
225 case MotionEvent.ACTION_MOVE: {
226 if (mTouchDownX != -1) {
227 int x = (int) event.getX();
228 int y = (int) event.getY();
229 int xDiff = Math.abs(x - mTouchDownX);
230 int yDiff = Math.abs(y - mTouchDownY);
Matthew Nge0903c92018-01-17 15:32:41 -0800231 boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
232 boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
233 boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
Matthew Nga8f24262017-12-19 11:54:24 -0800234 int pos, touchDown, offset, trackSize;
Matthew Nge0903c92018-01-17 15:32:41 -0800235
Matthew Nga8f24262017-12-19 11:54:24 -0800236 if (mIsVertical) {
Matthew Nge0903c92018-01-17 15:32:41 -0800237 exceededTouchSlop = exceededTouchSlopY;
238 exceededPerpendicularTouchSlop = exceededTouchSlopX;
Matthew Nga8f24262017-12-19 11:54:24 -0800239 pos = y;
240 touchDown = mTouchDownY;
241 offset = pos - mTrackRect.top;
242 trackSize = mTrackRect.height();
243 } else {
Matthew Nge0903c92018-01-17 15:32:41 -0800244 exceededTouchSlop = exceededTouchSlopX;
245 exceededPerpendicularTouchSlop = exceededTouchSlopY;
Matthew Nga8f24262017-12-19 11:54:24 -0800246 pos = x;
247 touchDown = mTouchDownX;
248 offset = pos - mTrackRect.left;
249 trackSize = mTrackRect.width();
250 }
Winson Chung4faf38a2018-02-06 08:53:37 -0800251 // Do not start scrubbing when dragging in the perpendicular direction if we
252 // haven't already started quickscrub
253 if (!mDraggingActive && !mQuickScrubActive && exceededPerpendicularTouchSlop) {
Matthew Nge0903c92018-01-17 15:32:41 -0800254 mHandler.removeCallbacksAndMessages(null);
255 return false;
256 }
Matthew Nga8f24262017-12-19 11:54:24 -0800257 if (!mDragPositive) {
258 offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
259 }
260
261 // Control the button movement
262 if (!mDraggingActive && exceededTouchSlop) {
263 boolean allowDrag = !mDragPositive
264 ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
265 if (allowDrag) {
266 mDownOffset = offset;
267 homeButton.setClickable(false);
268 mDraggingActive = true;
269 }
270 }
271 if (mDraggingActive && (mDragPositive && offset >= 0
272 || !mDragPositive && offset <= 0)) {
273 float scrubFraction =
274 Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
275 mTranslation = !mDragPositive
276 ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
277 : Utilities.clamp(offset - mDownOffset, 0, trackSize);
278 if (mQuickScrubActive) {
279 try {
280 overviewProxy.onQuickScrubProgress(scrubFraction);
Matthew Ngbd824572018-01-17 16:25:56 -0800281 if (DEBUG_OVERVIEW_PROXY) {
282 Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
283 }
Matthew Nga8f24262017-12-19 11:54:24 -0800284 } catch (RemoteException e) {
285 Log.e(TAG, "Failed to send progress of quick scrub.", e);
286 }
287 } else {
288 mTranslation /= SWITCH_STICKINESS;
289 }
290 if (mIsVertical) {
291 homeButton.getCurrentView().setTranslationY(mTranslation);
292 } else {
293 homeButton.getCurrentView().setTranslationX(mTranslation);
294 }
295 }
296 }
297 break;
298 }
299 case MotionEvent.ACTION_CANCEL:
300 case MotionEvent.ACTION_UP:
301 endQuickScrub();
302 break;
303 }
304 return mDraggingActive || mQuickScrubActive;
305 }
306
Winson Chung4faf38a2018-02-06 08:53:37 -0800307 /**
308 * @return true if we want to handle touch events for quick scrub/switch and prevent proxying
309 * the event to the overview service.
310 */
311 @Override
312 public boolean onTouchEvent(MotionEvent event) {
313 if (mGestureDetector.onTouchEvent(event)) {
314 // If the fling has been handled, then skip proxying the UP
315 return true;
316 }
317 if (event.getAction() == MotionEvent.ACTION_UP) {
318 endQuickScrub();
319 }
320 return mDraggingActive || mQuickScrubActive;
321 }
322
Matthew Nga8f24262017-12-19 11:54:24 -0800323 @Override
324 public void onDraw(Canvas canvas) {
Matthew Ng7090a802018-01-19 13:36:22 -0800325 int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor,
326 mDarkTrackColor);
327 mTrackPaint.setColor(color);
328 mTrackPaint.setAlpha((int) (mTrackPaint.getAlpha() * mTrackAlpha));
Matthew Nga8f24262017-12-19 11:54:24 -0800329 canvas.drawRect(mTrackRect, mTrackPaint);
330 }
331
332 @Override
333 public void onLayout(boolean changed, int left, int top, int right, int bottom) {
334 final int width = right - left;
335 final int height = bottom - top;
336 final int x1, x2, y1, y2;
337 if (mIsVertical) {
338 x1 = (width - mTrackThickness) / 2;
339 x2 = x1 + mTrackThickness;
340 y1 = mDragPositive ? height / 2 : mTrackPadding;
341 y2 = y1 + height / 2 - mTrackPadding;
342 } else {
343 y1 = (height - mTrackThickness) / 2;
344 y2 = y1 + mTrackThickness;
345 x1 = mDragPositive ? width / 2 : mTrackPadding;
346 x2 = x1 + width / 2 - mTrackPadding;
347 }
348 mTrackRect.set(x1, y1, x2, y2);
349
350 // Get the touch rect of the home button location
351 View homeView = mNavigationBarView.getHomeButton().getCurrentView();
Matthew Nga5612a02018-01-17 12:28:41 -0800352 if (homeView != null) {
353 int[] globalHomePos = homeView.getLocationOnScreen();
354 int[] globalNavBarPos = mNavigationBarView.getLocationOnScreen();
355 int homeX = globalHomePos[0] - globalNavBarPos[0];
356 int homeY = globalHomePos[1] - globalNavBarPos[1];
357 mHomeButtonRect.set(homeX, homeY, homeX + homeView.getMeasuredWidth(),
358 homeY + homeView.getMeasuredHeight());
359 }
Matthew Nga8f24262017-12-19 11:54:24 -0800360 }
361
362 @Override
363 public void onDarkIntensityChange(float intensity) {
Matthew Ng7090a802018-01-19 13:36:22 -0800364 mDarkIntensity = intensity;
365 mNavigationBarView.invalidate();
Matthew Nga8f24262017-12-19 11:54:24 -0800366 }
367
368 @Override
Matthew Nga8f24262017-12-19 11:54:24 -0800369 public void setBarState(boolean isVertical, boolean isRTL) {
370 mIsVertical = isVertical;
371 mIsRTL = isRTL;
372 try {
373 int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
374 mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
375 if (isRTL) {
376 mDragPositive = !mDragPositive;
377 }
378 } catch (RemoteException e) {
379 Slog.e(TAG, "Failed to get nav bar position.", e);
380 }
381 }
382
Matthew Ngf41f7c32018-01-22 17:57:05 -0800383 boolean isQuickScrubEnabled() {
384 return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", false);
385 }
386
Matthew Nga8f24262017-12-19 11:54:24 -0800387 private void startQuickScrub() {
388 if (!mQuickScrubActive) {
389 mQuickScrubActive = true;
Matthew Ng7090a802018-01-19 13:36:22 -0800390 mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
391 mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
392 mTrackAnimator.setFloatValues(0, 1);
Matthew Nga8f24262017-12-19 11:54:24 -0800393 mTrackAnimator.start();
394 try {
395 mOverviewEventSender.getProxy().onQuickScrubStart();
Matthew Ngbd824572018-01-17 16:25:56 -0800396 if (DEBUG_OVERVIEW_PROXY) {
397 Log.d(TAG_OPS, "Quick Scrub Start");
398 }
Matthew Nga8f24262017-12-19 11:54:24 -0800399 } catch (RemoteException e) {
400 Log.e(TAG, "Failed to send start of quick scrub.", e);
401 }
402 }
403 }
404
405 private void endQuickScrub() {
406 mHandler.removeCallbacks(mLongPressRunnable);
407 if (mDraggingActive || mQuickScrubActive) {
408 mButtonAnimator.setIntValues((int) mTranslation, 0);
Matthew Ng7090a802018-01-19 13:36:22 -0800409 mTrackAnimator.setFloatValues(mTrackAlpha, 0);
Matthew Nga8f24262017-12-19 11:54:24 -0800410 mQuickScrubEndAnimator.start();
411 try {
412 mOverviewEventSender.getProxy().onQuickScrubEnd();
Matthew Ngbd824572018-01-17 16:25:56 -0800413 if (DEBUG_OVERVIEW_PROXY) {
414 Log.d(TAG_OPS, "Quick Scrub End");
415 }
Matthew Nga8f24262017-12-19 11:54:24 -0800416 } catch (RemoteException e) {
417 Log.e(TAG, "Failed to send end of quick scrub.", e);
418 }
419 }
420 mDraggingActive = false;
421 }
422
423 private int getDimensionPixelSize(Context context, @DimenRes int resId) {
424 return context.getResources().getDimensionPixelSize(resId);
425 }
426}