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