1/* 2 * Copyright (C) 2014 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.recents.views; 18 19import android.content.Context; 20import android.view.InputDevice; 21import android.view.MotionEvent; 22import android.view.VelocityTracker; 23import android.view.View; 24import android.view.ViewConfiguration; 25import android.view.ViewParent; 26import com.android.systemui.recents.Constants; 27import com.android.systemui.recents.RecentsConfiguration; 28 29/* Handles touch events for a TaskStackView. */ 30class TaskStackViewTouchHandler implements SwipeHelper.Callback { 31 static int INACTIVE_POINTER_ID = -1; 32 33 RecentsConfiguration mConfig; 34 TaskStackView mSv; 35 TaskStackViewScroller mScroller; 36 VelocityTracker mVelocityTracker; 37 38 boolean mIsScrolling; 39 40 float mInitialP; 41 float mLastP; 42 float mTotalPMotion; 43 int mInitialMotionX, mInitialMotionY; 44 int mLastMotionX, mLastMotionY; 45 int mActivePointerId = INACTIVE_POINTER_ID; 46 TaskView mActiveTaskView = null; 47 48 int mMinimumVelocity; 49 int mMaximumVelocity; 50 // The scroll touch slop is used to calculate when we start scrolling 51 int mScrollTouchSlop; 52 // The page touch slop is used to calculate when we start swiping 53 float mPagingTouchSlop; 54 55 SwipeHelper mSwipeHelper; 56 boolean mInterceptedBySwipeHelper; 57 58 public TaskStackViewTouchHandler(Context context, TaskStackView sv, 59 RecentsConfiguration config, TaskStackViewScroller scroller) { 60 ViewConfiguration configuration = ViewConfiguration.get(context); 61 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 62 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 63 mScrollTouchSlop = configuration.getScaledTouchSlop(); 64 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 65 mSv = sv; 66 mScroller = scroller; 67 mConfig = config; 68 69 float densityScale = context.getResources().getDisplayMetrics().density; 70 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop); 71 mSwipeHelper.setMinAlpha(1f); 72 } 73 74 /** Velocity tracker helpers */ 75 void initOrResetVelocityTracker() { 76 if (mVelocityTracker == null) { 77 mVelocityTracker = VelocityTracker.obtain(); 78 } else { 79 mVelocityTracker.clear(); 80 } 81 } 82 void initVelocityTrackerIfNotExists() { 83 if (mVelocityTracker == null) { 84 mVelocityTracker = VelocityTracker.obtain(); 85 } 86 } 87 void recycleVelocityTracker() { 88 if (mVelocityTracker != null) { 89 mVelocityTracker.recycle(); 90 mVelocityTracker = null; 91 } 92 } 93 94 /** Returns the view at the specified coordinates */ 95 TaskView findViewAtPoint(int x, int y) { 96 int childCount = mSv.getChildCount(); 97 for (int i = childCount - 1; i >= 0; i--) { 98 TaskView tv = (TaskView) mSv.getChildAt(i); 99 if (tv.getVisibility() == View.VISIBLE) { 100 if (mSv.isTransformedTouchPointInView(x, y, tv)) { 101 return tv; 102 } 103 } 104 } 105 return null; 106 } 107 108 /** Constructs a simulated motion event for the current stack scroll. */ 109 MotionEvent createMotionEventForStackScroll(MotionEvent ev) { 110 MotionEvent pev = MotionEvent.obtainNoHistory(ev); 111 pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll())); 112 return pev; 113 } 114 115 /** Touch preprocessing for handling below */ 116 public boolean onInterceptTouchEvent(MotionEvent ev) { 117 // Return early if we have no children 118 boolean hasChildren = (mSv.getChildCount() > 0); 119 if (!hasChildren) { 120 return false; 121 } 122 123 // Pass through to swipe helper if we are swiping 124 mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); 125 if (mInterceptedBySwipeHelper) { 126 return true; 127 } 128 129 boolean wasScrolling = mScroller.isScrolling() || 130 (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning()); 131 int action = ev.getAction(); 132 switch (action & MotionEvent.ACTION_MASK) { 133 case MotionEvent.ACTION_DOWN: { 134 // Save the touch down info 135 mInitialMotionX = mLastMotionX = (int) ev.getX(); 136 mInitialMotionY = mLastMotionY = (int) ev.getY(); 137 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 138 mActivePointerId = ev.getPointerId(0); 139 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 140 // Stop the current scroll if it is still flinging 141 mScroller.stopScroller(); 142 mScroller.stopBoundScrollAnimation(); 143 // Initialize the velocity tracker 144 initOrResetVelocityTracker(); 145 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 146 break; 147 } 148 case MotionEvent.ACTION_MOVE: { 149 if (mActivePointerId == INACTIVE_POINTER_ID) break; 150 151 // Initialize the velocity tracker if necessary 152 initVelocityTrackerIfNotExists(); 153 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 154 155 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 156 int y = (int) ev.getY(activePointerIndex); 157 int x = (int) ev.getX(activePointerIndex); 158 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 159 // Save the touch move info 160 mIsScrolling = true; 161 // Disallow parents from intercepting touch events 162 final ViewParent parent = mSv.getParent(); 163 if (parent != null) { 164 parent.requestDisallowInterceptTouchEvent(true); 165 } 166 } 167 168 mLastMotionX = x; 169 mLastMotionY = y; 170 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 171 break; 172 } 173 case MotionEvent.ACTION_CANCEL: 174 case MotionEvent.ACTION_UP: { 175 // Animate the scroll back if we've cancelled 176 mScroller.animateBoundScroll(); 177 // Reset the drag state and the velocity tracker 178 mIsScrolling = false; 179 mActivePointerId = INACTIVE_POINTER_ID; 180 mActiveTaskView = null; 181 mTotalPMotion = 0; 182 recycleVelocityTracker(); 183 break; 184 } 185 } 186 187 return wasScrolling || mIsScrolling; 188 } 189 190 /** Handles touch events once we have intercepted them */ 191 public boolean onTouchEvent(MotionEvent ev) { 192 // Short circuit if we have no children 193 boolean hasChildren = (mSv.getChildCount() > 0); 194 if (!hasChildren) { 195 return false; 196 } 197 198 // Pass through to swipe helper if we are swiping 199 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 200 return true; 201 } 202 203 // Update the velocity tracker 204 initVelocityTrackerIfNotExists(); 205 206 int action = ev.getAction(); 207 switch (action & MotionEvent.ACTION_MASK) { 208 case MotionEvent.ACTION_DOWN: { 209 // Save the touch down info 210 mInitialMotionX = mLastMotionX = (int) ev.getX(); 211 mInitialMotionY = mLastMotionY = (int) ev.getY(); 212 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 213 mActivePointerId = ev.getPointerId(0); 214 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 215 // Stop the current scroll if it is still flinging 216 mScroller.stopScroller(); 217 mScroller.stopBoundScrollAnimation(); 218 // Initialize the velocity tracker 219 initOrResetVelocityTracker(); 220 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 221 // Disallow parents from intercepting touch events 222 final ViewParent parent = mSv.getParent(); 223 if (parent != null) { 224 parent.requestDisallowInterceptTouchEvent(true); 225 } 226 break; 227 } 228 case MotionEvent.ACTION_POINTER_DOWN: { 229 final int index = ev.getActionIndex(); 230 mActivePointerId = ev.getPointerId(index); 231 mLastMotionX = (int) ev.getX(index); 232 mLastMotionY = (int) ev.getY(index); 233 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 234 break; 235 } 236 case MotionEvent.ACTION_MOVE: { 237 if (mActivePointerId == INACTIVE_POINTER_ID) break; 238 239 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 240 241 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 242 int x = (int) ev.getX(activePointerIndex); 243 int y = (int) ev.getY(activePointerIndex); 244 int yTotal = Math.abs(y - mInitialMotionY); 245 float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y); 246 float deltaP = mLastP - curP; 247 if (!mIsScrolling) { 248 if (yTotal > mScrollTouchSlop) { 249 mIsScrolling = true; 250 // Disallow parents from intercepting touch events 251 final ViewParent parent = mSv.getParent(); 252 if (parent != null) { 253 parent.requestDisallowInterceptTouchEvent(true); 254 } 255 } 256 } 257 if (mIsScrolling) { 258 float curStackScroll = mScroller.getStackScroll(); 259 float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP); 260 if (Float.compare(overScrollAmount, 0f) != 0) { 261 // Bound the overscroll to a fixed amount, and inversely scale the y-movement 262 // relative to how close we are to the max overscroll 263 float maxOverScroll = mConfig.taskStackOverscrollPct; 264 deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount) 265 / maxOverScroll)); 266 } 267 mScroller.setStackScroll(curStackScroll + deltaP); 268 } 269 mLastMotionX = x; 270 mLastMotionY = y; 271 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 272 mTotalPMotion += Math.abs(deltaP); 273 break; 274 } 275 case MotionEvent.ACTION_UP: { 276 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 277 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 278 if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { 279 float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); 280 int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * 281 (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange - 282 Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); 283 mScroller.mScroller.fling(0, 284 mScroller.progressToScrollRange(mScroller.getStackScroll()), 285 0, velocity, 286 0, 0, 287 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP), 288 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP), 289 0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange + 290 overscrollRange); 291 // Invalidate to kick off computeScroll 292 mSv.invalidate(); 293 } else if (mScroller.isScrollOutOfBounds()) { 294 // Animate the scroll back into bounds 295 mScroller.animateBoundScroll(); 296 } 297 298 mActivePointerId = INACTIVE_POINTER_ID; 299 mIsScrolling = false; 300 mTotalPMotion = 0; 301 recycleVelocityTracker(); 302 break; 303 } 304 case MotionEvent.ACTION_POINTER_UP: { 305 int pointerIndex = ev.getActionIndex(); 306 int pointerId = ev.getPointerId(pointerIndex); 307 if (pointerId == mActivePointerId) { 308 // Select a new active pointer id and reset the motion state 309 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 310 mActivePointerId = ev.getPointerId(newPointerIndex); 311 mLastMotionX = (int) ev.getX(newPointerIndex); 312 mLastMotionY = (int) ev.getY(newPointerIndex); 313 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 314 mVelocityTracker.clear(); 315 } 316 break; 317 } 318 case MotionEvent.ACTION_CANCEL: { 319 if (mScroller.isScrollOutOfBounds()) { 320 // Animate the scroll back into bounds 321 mScroller.animateBoundScroll(); 322 } 323 mActivePointerId = INACTIVE_POINTER_ID; 324 mIsScrolling = false; 325 mTotalPMotion = 0; 326 recycleVelocityTracker(); 327 break; 328 } 329 } 330 return true; 331 } 332 333 /** Handles generic motion events */ 334 public boolean onGenericMotionEvent(MotionEvent ev) { 335 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 336 InputDevice.SOURCE_CLASS_POINTER) { 337 int action = ev.getAction(); 338 switch (action & MotionEvent.ACTION_MASK) { 339 case MotionEvent.ACTION_SCROLL: 340 // Find the front most task and scroll the next task to the front 341 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 342 if (vScroll > 0) { 343 if (mSv.ensureFocusedTask()) { 344 mSv.focusNextTask(true, false); 345 } 346 } else { 347 if (mSv.ensureFocusedTask()) { 348 mSv.focusNextTask(false, false); 349 } 350 } 351 return true; 352 } 353 } 354 return false; 355 } 356 357 /**** SwipeHelper Implementation ****/ 358 359 @Override 360 public View getChildAtPosition(MotionEvent ev) { 361 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 362 } 363 364 @Override 365 public boolean canChildBeDismissed(View v) { 366 return true; 367 } 368 369 @Override 370 public void onBeginDrag(View v) { 371 TaskView tv = (TaskView) v; 372 // Disable clipping with the stack while we are swiping 373 tv.setClipViewInStack(false); 374 // Disallow touch events from this task view 375 tv.setTouchEnabled(false); 376 // Disallow parents from intercepting touch events 377 final ViewParent parent = mSv.getParent(); 378 if (parent != null) { 379 parent.requestDisallowInterceptTouchEvent(true); 380 } 381 } 382 383 @Override 384 public void onSwipeChanged(View v, float delta) { 385 // Do nothing 386 } 387 388 @Override 389 public void onChildDismissed(View v) { 390 TaskView tv = (TaskView) v; 391 // Re-enable clipping with the stack (we will reuse this view) 392 tv.setClipViewInStack(true); 393 // Re-enable touch events from this task view 394 tv.setTouchEnabled(true); 395 // Remove the task view from the stack 396 mSv.onTaskViewDismissed(tv); 397 } 398 399 @Override 400 public void onSnapBackCompleted(View v) { 401 TaskView tv = (TaskView) v; 402 // Re-enable clipping with the stack 403 tv.setClipViewInStack(true); 404 // Re-enable touch events from this task view 405 tv.setTouchEnabled(true); 406 } 407 408 @Override 409 public void onDragCancelled(View v) { 410 // Do nothing 411 } 412} 413