blob: ee1d08872999ad52cd0ffa47d7148305db8381b5 [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;
22import android.animation.ObjectAnimator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.content.Context;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.Rect;
29import android.os.Handler;
30import android.os.RemoteException;
Matthew Ngf41f7c32018-01-22 17:57:05 -080031import android.os.SystemProperties;
Matthew Nga8f24262017-12-19 11:54:24 -080032import android.util.Log;
33import android.util.Slog;
34import android.view.Display;
35import android.view.GestureDetector;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.WindowManager;
40import android.view.WindowManagerGlobal;
41import android.view.animation.DecelerateInterpolator;
42import android.view.animation.Interpolator;
43import android.support.annotation.DimenRes;
44import com.android.systemui.Dependency;
45import com.android.systemui.OverviewProxyService;
46import com.android.systemui.R;
47import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
48import com.android.systemui.shared.recents.IOverviewProxy;
49import com.android.systemui.shared.recents.utilities.Utilities;
50
51import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
52import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
Matthew Ngbd824572018-01-17 16:25:56 -080053import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
54import static com.android.systemui.OverviewProxyService.TAG_OPS;
Matthew Nga8f24262017-12-19 11:54:24 -080055
56/**
57 * Class to detect gestures on the navigation bar and implement quick scrub and switch.
58 */
59public class QuickScrubController extends GestureDetector.SimpleOnGestureListener implements
60 GestureHelper {
61
62 private static final String TAG = "QuickScrubController";
63 private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
64 private static final int ANIM_DURATION_MS = 200;
65 private static final long LONG_PRESS_DELAY_MS = 150;
66
67 /**
68 * For quick step, set a damping value to allow the button to stick closer its origin position
69 * when dragging before quick scrub is active.
70 */
71 private static final int SWITCH_STICKINESS = 4;
72
73 private NavigationBarView mNavigationBarView;
74 private GestureDetector mGestureDetector;
75
76 private boolean mDraggingActive;
77 private boolean mQuickScrubActive;
78 private float mDownOffset;
79 private float mTranslation;
80 private int mTouchDownX;
81 private int mTouchDownY;
82 private boolean mDragPositive;
83 private boolean mIsVertical;
84 private boolean mIsRTL;
85 private float mMaxTrackPaintAlpha;
86
87 private final Handler mHandler = new Handler();
88 private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
89 private final Rect mTrackRect = new Rect();
90 private final Rect mHomeButtonRect = new Rect();
91 private final Paint mTrackPaint = new Paint();
92 private final int mScrollTouchSlop;
93 private final OverviewProxyService mOverviewEventSender;
94 private final Display mDisplay;
95 private final int mTrackThickness;
96 private final int mTrackPadding;
97 private final ValueAnimator mTrackAnimator;
98 private final ValueAnimator mButtonAnimator;
99 private final AnimatorSet mQuickScrubEndAnimator;
100 private final Context mContext;
101
102 private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
103 mTrackPaint.setAlpha(Math.round((float) valueAnimator.getAnimatedValue() * 255));
104 mNavigationBarView.invalidate();
105 };
106
107 private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
108 int pos = (int) animator.getAnimatedValue();
109 if (!mQuickScrubActive) {
110 pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
111 }
112 final View homeView = mNavigationBarView.getHomeButton().getCurrentView();
113 if (mIsVertical) {
114 homeView.setTranslationY(pos);
115 } else {
116 homeView.setTranslationX(pos);
117 }
118 };
119
120 private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
121 @Override
122 public void onAnimationEnd(Animator animation) {
123 mNavigationBarView.getHomeButton().setClickable(true);
124 mQuickScrubActive = false;
125 mTranslation = 0;
126 }
127 };
128
129 private Runnable mLongPressRunnable = this::startQuickScrub;
130
131 private final GestureDetector.SimpleOnGestureListener mGestureListener =
132 new GestureDetector.SimpleOnGestureListener() {
133 @Override
134 public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800135 if (!isQuickScrubEnabled() || mQuickScrubActive) {
Matthew Nga8f24262017-12-19 11:54:24 -0800136 return false;
137 }
138 float velocityX = mIsRTL ? -velX : velX;
139 float absVelY = Math.abs(velY);
140 final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
141 mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
142 if (isValidFling) {
143 mDraggingActive = false;
144 mButtonAnimator.setIntValues((int) mTranslation, 0);
145 mButtonAnimator.start();
146 mHandler.removeCallbacks(mLongPressRunnable);
147 try {
148 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
149 overviewProxy.onQuickSwitch();
Matthew Ngbd824572018-01-17 16:25:56 -0800150 if (DEBUG_OVERVIEW_PROXY) {
151 Log.d(TAG_OPS, "Quick Switch");
152 }
Matthew Nga8f24262017-12-19 11:54:24 -0800153 } catch (RemoteException e) {
154 Log.e(TAG, "Failed to send start of quick switch.", e);
155 }
156 }
157 return true;
158 }
159 };
160
161 public QuickScrubController(Context context) {
162 mContext = context;
163 mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
164 mDisplay = ((WindowManager) context.getSystemService(
165 Context.WINDOW_SERVICE)).getDefaultDisplay();
166 mOverviewEventSender = Dependency.get(OverviewProxyService.class);
167 mGestureDetector = new GestureDetector(mContext, mGestureListener);
168 mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
169 mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
170
171 mTrackAnimator = ObjectAnimator.ofFloat();
172 mTrackAnimator.addUpdateListener(mTrackAnimatorListener);
173 mButtonAnimator = ObjectAnimator.ofInt();
174 mButtonAnimator.addUpdateListener(mButtonTranslationListener);
175 mQuickScrubEndAnimator = new AnimatorSet();
176 mQuickScrubEndAnimator.playTogether(mTrackAnimator, mButtonAnimator);
177 mQuickScrubEndAnimator.setDuration(ANIM_DURATION_MS);
178 mQuickScrubEndAnimator.addListener(mQuickScrubEndListener);
179 mQuickScrubEndAnimator.setInterpolator(mQuickScrubEndInterpolator);
180 }
181
182 public void setComponents(NavigationBarView navigationBarView) {
183 mNavigationBarView = navigationBarView;
184 }
185
186 @Override
187 public boolean onInterceptTouchEvent(MotionEvent event) {
188 final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
189 final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
190 if (overviewProxy == null) {
191 homeButton.setDelayTouchFeedback(false);
192 return false;
193 }
194 mGestureDetector.onTouchEvent(event);
195 int action = event.getAction();
196 switch (action & MotionEvent.ACTION_MASK) {
197 case MotionEvent.ACTION_DOWN: {
198 int x = (int) event.getX();
199 int y = (int) event.getY();
Matthew Ngf41f7c32018-01-22 17:57:05 -0800200 if (isQuickScrubEnabled() && mHomeButtonRect.contains(x, y)) {
Matthew Nga8f24262017-12-19 11:54:24 -0800201 mTouchDownX = x;
202 mTouchDownY = y;
203 homeButton.setDelayTouchFeedback(true);
204 mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
205 } else {
Matthew Ngf41f7c32018-01-22 17:57:05 -0800206 homeButton.setDelayTouchFeedback(false);
Matthew Nga8f24262017-12-19 11:54:24 -0800207 mTouchDownX = mTouchDownY = -1;
208 }
209 break;
210 }
211 case MotionEvent.ACTION_MOVE: {
212 if (mTouchDownX != -1) {
213 int x = (int) event.getX();
214 int y = (int) event.getY();
215 int xDiff = Math.abs(x - mTouchDownX);
216 int yDiff = Math.abs(y - mTouchDownY);
Matthew Nge0903c92018-01-17 15:32:41 -0800217 boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
218 boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
219 boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
Matthew Nga8f24262017-12-19 11:54:24 -0800220 int pos, touchDown, offset, trackSize;
Matthew Nge0903c92018-01-17 15:32:41 -0800221
Matthew Nga8f24262017-12-19 11:54:24 -0800222 if (mIsVertical) {
Matthew Nge0903c92018-01-17 15:32:41 -0800223 exceededTouchSlop = exceededTouchSlopY;
224 exceededPerpendicularTouchSlop = exceededTouchSlopX;
Matthew Nga8f24262017-12-19 11:54:24 -0800225 pos = y;
226 touchDown = mTouchDownY;
227 offset = pos - mTrackRect.top;
228 trackSize = mTrackRect.height();
229 } else {
Matthew Nge0903c92018-01-17 15:32:41 -0800230 exceededTouchSlop = exceededTouchSlopX;
231 exceededPerpendicularTouchSlop = exceededTouchSlopY;
Matthew Nga8f24262017-12-19 11:54:24 -0800232 pos = x;
233 touchDown = mTouchDownX;
234 offset = pos - mTrackRect.left;
235 trackSize = mTrackRect.width();
236 }
Matthew Nge0903c92018-01-17 15:32:41 -0800237 // Do not start scrubbing when dragging in the perpendicular direction
238 if (!mDraggingActive && exceededPerpendicularTouchSlop) {
239 mHandler.removeCallbacksAndMessages(null);
240 return false;
241 }
Matthew Nga8f24262017-12-19 11:54:24 -0800242 if (!mDragPositive) {
243 offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
244 }
245
246 // Control the button movement
247 if (!mDraggingActive && exceededTouchSlop) {
248 boolean allowDrag = !mDragPositive
249 ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
250 if (allowDrag) {
251 mDownOffset = offset;
252 homeButton.setClickable(false);
253 mDraggingActive = true;
254 }
255 }
256 if (mDraggingActive && (mDragPositive && offset >= 0
257 || !mDragPositive && offset <= 0)) {
258 float scrubFraction =
259 Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
260 mTranslation = !mDragPositive
261 ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
262 : Utilities.clamp(offset - mDownOffset, 0, trackSize);
263 if (mQuickScrubActive) {
264 try {
265 overviewProxy.onQuickScrubProgress(scrubFraction);
Matthew Ngbd824572018-01-17 16:25:56 -0800266 if (DEBUG_OVERVIEW_PROXY) {
267 Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
268 }
Matthew Nga8f24262017-12-19 11:54:24 -0800269 } catch (RemoteException e) {
270 Log.e(TAG, "Failed to send progress of quick scrub.", e);
271 }
272 } else {
273 mTranslation /= SWITCH_STICKINESS;
274 }
275 if (mIsVertical) {
276 homeButton.getCurrentView().setTranslationY(mTranslation);
277 } else {
278 homeButton.getCurrentView().setTranslationX(mTranslation);
279 }
280 }
281 }
282 break;
283 }
284 case MotionEvent.ACTION_CANCEL:
285 case MotionEvent.ACTION_UP:
286 endQuickScrub();
287 break;
288 }
289 return mDraggingActive || mQuickScrubActive;
290 }
291
292 @Override
293 public void onDraw(Canvas canvas) {
294 canvas.drawRect(mTrackRect, mTrackPaint);
295 }
296
297 @Override
298 public void onLayout(boolean changed, int left, int top, int right, int bottom) {
299 final int width = right - left;
300 final int height = bottom - top;
301 final int x1, x2, y1, y2;
302 if (mIsVertical) {
303 x1 = (width - mTrackThickness) / 2;
304 x2 = x1 + mTrackThickness;
305 y1 = mDragPositive ? height / 2 : mTrackPadding;
306 y2 = y1 + height / 2 - mTrackPadding;
307 } else {
308 y1 = (height - mTrackThickness) / 2;
309 y2 = y1 + mTrackThickness;
310 x1 = mDragPositive ? width / 2 : mTrackPadding;
311 x2 = x1 + width / 2 - mTrackPadding;
312 }
313 mTrackRect.set(x1, y1, x2, y2);
314
315 // Get the touch rect of the home button location
316 View homeView = mNavigationBarView.getHomeButton().getCurrentView();
Matthew Nga5612a02018-01-17 12:28:41 -0800317 if (homeView != null) {
318 int[] globalHomePos = homeView.getLocationOnScreen();
319 int[] globalNavBarPos = mNavigationBarView.getLocationOnScreen();
320 int homeX = globalHomePos[0] - globalNavBarPos[0];
321 int homeY = globalHomePos[1] - globalNavBarPos[1];
322 mHomeButtonRect.set(homeX, homeY, homeX + homeView.getMeasuredWidth(),
323 homeY + homeView.getMeasuredHeight());
324 }
Matthew Nga8f24262017-12-19 11:54:24 -0800325 }
326
327 @Override
328 public void onDarkIntensityChange(float intensity) {
329 if (intensity == 0) {
330 mTrackPaint.setColor(mContext.getColor(R.color.quick_step_track_background_light));
331 } else if (intensity == 1) {
332 mTrackPaint.setColor(mContext.getColor(R.color.quick_step_track_background_dark));
333 }
334 mMaxTrackPaintAlpha = mTrackPaint.getAlpha() * 1f / 255;
335 mTrackPaint.setAlpha(0);
336 }
337
338 @Override
339 public boolean onTouchEvent(MotionEvent event) {
340 if (event.getAction() == MotionEvent.ACTION_UP) {
341 endQuickScrub();
342 }
343 return false;
344 }
345
346 @Override
347 public void setBarState(boolean isVertical, boolean isRTL) {
348 mIsVertical = isVertical;
349 mIsRTL = isRTL;
350 try {
351 int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
352 mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
353 if (isRTL) {
354 mDragPositive = !mDragPositive;
355 }
356 } catch (RemoteException e) {
357 Slog.e(TAG, "Failed to get nav bar position.", e);
358 }
359 }
360
Matthew Ngf41f7c32018-01-22 17:57:05 -0800361 boolean isQuickScrubEnabled() {
362 return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", false);
363 }
364
Matthew Nga8f24262017-12-19 11:54:24 -0800365 private void startQuickScrub() {
366 if (!mQuickScrubActive) {
367 mQuickScrubActive = true;
368 mTrackAnimator.setFloatValues(0, mMaxTrackPaintAlpha);
369 mTrackAnimator.start();
370 try {
371 mOverviewEventSender.getProxy().onQuickScrubStart();
Matthew Ngbd824572018-01-17 16:25:56 -0800372 if (DEBUG_OVERVIEW_PROXY) {
373 Log.d(TAG_OPS, "Quick Scrub Start");
374 }
Matthew Nga8f24262017-12-19 11:54:24 -0800375 } catch (RemoteException e) {
376 Log.e(TAG, "Failed to send start of quick scrub.", e);
377 }
378 }
379 }
380
381 private void endQuickScrub() {
382 mHandler.removeCallbacks(mLongPressRunnable);
383 if (mDraggingActive || mQuickScrubActive) {
384 mButtonAnimator.setIntValues((int) mTranslation, 0);
385 mTrackAnimator.setFloatValues(mTrackPaint.getAlpha() * 1f / 255, 0);
386 mQuickScrubEndAnimator.start();
387 try {
388 mOverviewEventSender.getProxy().onQuickScrubEnd();
Matthew Ngbd824572018-01-17 16:25:56 -0800389 if (DEBUG_OVERVIEW_PROXY) {
390 Log.d(TAG_OPS, "Quick Scrub End");
391 }
Matthew Nga8f24262017-12-19 11:54:24 -0800392 } catch (RemoteException e) {
393 Log.e(TAG, "Failed to send end of quick scrub.", e);
394 }
395 }
396 mDraggingActive = false;
397 }
398
399 private int getDimensionPixelSize(Context context, @DimenRes int resId) {
400 return context.getResources().getDimensionPixelSize(resId);
401 }
402}