TaskStackViewTouchHandler.java revision 606b3da71ab64e489e8f65e0b1092138dbfaf7b7
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 // Check if the scroller is finished yet 147 mIsScrolling = mScroller.isScrolling(); 148 break; 149 } 150 case MotionEvent.ACTION_MOVE: { 151 if (mActivePointerId == INACTIVE_POINTER_ID) break; 152 153 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 154 int y = (int) ev.getY(activePointerIndex); 155 int x = (int) ev.getX(activePointerIndex); 156 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 157 // Save the touch move info 158 mIsScrolling = true; 159 // Initialize the velocity tracker if necessary 160 initVelocityTrackerIfNotExists(); 161 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 162 // Disallow parents from intercepting touch events 163 final ViewParent parent = mSv.getParent(); 164 if (parent != null) { 165 parent.requestDisallowInterceptTouchEvent(true); 166 } 167 } 168 169 mLastMotionX = x; 170 mLastMotionY = y; 171 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 172 break; 173 } 174 case MotionEvent.ACTION_CANCEL: 175 case MotionEvent.ACTION_UP: { 176 // Animate the scroll back if we've cancelled 177 mScroller.animateBoundScroll(); 178 // Reset the drag state and the velocity tracker 179 mIsScrolling = false; 180 mActivePointerId = INACTIVE_POINTER_ID; 181 mActiveTaskView = null; 182 mTotalPMotion = 0; 183 recycleVelocityTracker(); 184 break; 185 } 186 } 187 188 return wasScrolling || mIsScrolling; 189 } 190 191 /** Handles touch events once we have intercepted them */ 192 public boolean onTouchEvent(MotionEvent ev) { 193 // Short circuit if we have no children 194 boolean hasChildren = (mSv.getChildCount() > 0); 195 if (!hasChildren) { 196 return false; 197 } 198 199 // Pass through to swipe helper if we are swiping 200 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 201 return true; 202 } 203 204 // Update the velocity tracker 205 initVelocityTrackerIfNotExists(); 206 207 int action = ev.getAction(); 208 switch (action & MotionEvent.ACTION_MASK) { 209 case MotionEvent.ACTION_DOWN: { 210 // Save the touch down info 211 mInitialMotionX = mLastMotionX = (int) ev.getX(); 212 mInitialMotionY = mLastMotionY = (int) ev.getY(); 213 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 214 mActivePointerId = ev.getPointerId(0); 215 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 216 // Stop the current scroll if it is still flinging 217 mScroller.stopScroller(); 218 mScroller.stopBoundScrollAnimation(); 219 // Initialize the velocity tracker 220 initOrResetVelocityTracker(); 221 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 222 // Disallow parents from intercepting touch events 223 final ViewParent parent = mSv.getParent(); 224 if (parent != null) { 225 parent.requestDisallowInterceptTouchEvent(true); 226 } 227 break; 228 } 229 case MotionEvent.ACTION_POINTER_DOWN: { 230 final int index = ev.getActionIndex(); 231 mActivePointerId = ev.getPointerId(index); 232 mLastMotionX = (int) ev.getX(index); 233 mLastMotionY = (int) ev.getY(index); 234 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 235 break; 236 } 237 case MotionEvent.ACTION_MOVE: { 238 if (mActivePointerId == INACTIVE_POINTER_ID) break; 239 240 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 241 int x = (int) ev.getX(activePointerIndex); 242 int y = (int) ev.getY(activePointerIndex); 243 int yTotal = Math.abs(y - mInitialMotionY); 244 float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y); 245 float deltaP = mLastP - curP; 246 if (!mIsScrolling) { 247 if (yTotal > mScrollTouchSlop) { 248 mIsScrolling = true; 249 // Initialize the velocity tracker 250 initOrResetVelocityTracker(); 251 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 252 // Disallow parents from intercepting touch events 253 final ViewParent parent = mSv.getParent(); 254 if (parent != null) { 255 parent.requestDisallowInterceptTouchEvent(true); 256 } 257 } 258 } 259 if (mIsScrolling) { 260 float curStackScroll = mScroller.getStackScroll(); 261 float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP); 262 if (Float.compare(overScrollAmount, 0f) != 0) { 263 // Bound the overscroll to a fixed amount, and inversely scale the y-movement 264 // relative to how close we are to the max overscroll 265 float maxOverScroll = mConfig.taskStackOverscrollPct; 266 deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount) 267 / maxOverScroll)); 268 } 269 mScroller.setStackScroll(curStackScroll + deltaP); 270 if (mScroller.isScrollOutOfBounds()) { 271 mVelocityTracker.clear(); 272 } else { 273 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 274 } 275 } 276 mLastMotionX = x; 277 mLastMotionY = y; 278 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 279 mTotalPMotion += Math.abs(deltaP); 280 break; 281 } 282 case MotionEvent.ACTION_UP: { 283 final VelocityTracker velocityTracker = mVelocityTracker; 284 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 285 int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); 286 if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { 287 int overscrollRange = (int) (Math.min(1f, 288 Math.abs((float) velocity / mMaximumVelocity)) * 289 Constants.Values.TaskStackView.TaskStackOverscrollRange); 290 // Fling scroll 291 mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()), 292 0, velocity, 293 0, 0, 294 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP), 295 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP), 296 0, overscrollRange); 297 // Invalidate to kick off computeScroll 298 mSv.invalidate(); 299 } else if (mScroller.isScrollOutOfBounds()) { 300 // Animate the scroll back into bounds 301 mScroller.animateBoundScroll(); 302 } 303 304 mActivePointerId = INACTIVE_POINTER_ID; 305 mIsScrolling = false; 306 mTotalPMotion = 0; 307 recycleVelocityTracker(); 308 break; 309 } 310 case MotionEvent.ACTION_POINTER_UP: { 311 int pointerIndex = ev.getActionIndex(); 312 int pointerId = ev.getPointerId(pointerIndex); 313 if (pointerId == mActivePointerId) { 314 // Select a new active pointer id and reset the motion state 315 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 316 mActivePointerId = ev.getPointerId(newPointerIndex); 317 mLastMotionX = (int) ev.getX(newPointerIndex); 318 mLastMotionY = (int) ev.getY(newPointerIndex); 319 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 320 mVelocityTracker.clear(); 321 } 322 break; 323 } 324 case MotionEvent.ACTION_CANCEL: { 325 if (mScroller.isScrollOutOfBounds()) { 326 // Animate the scroll back into bounds 327 mScroller.animateBoundScroll(); 328 } 329 mActivePointerId = INACTIVE_POINTER_ID; 330 mIsScrolling = false; 331 mTotalPMotion = 0; 332 recycleVelocityTracker(); 333 break; 334 } 335 } 336 return true; 337 } 338 339 /** Handles generic motion events */ 340 public boolean onGenericMotionEvent(MotionEvent ev) { 341 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 342 InputDevice.SOURCE_CLASS_POINTER) { 343 int action = ev.getAction(); 344 switch (action & MotionEvent.ACTION_MASK) { 345 case MotionEvent.ACTION_SCROLL: 346 // Find the front most task and scroll the next task to the front 347 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 348 if (vScroll > 0) { 349 if (mSv.ensureFocusedTask()) { 350 mSv.focusNextTask(true, false); 351 } 352 } else { 353 if (mSv.ensureFocusedTask()) { 354 mSv.focusNextTask(false, false); 355 } 356 } 357 return true; 358 } 359 } 360 return false; 361 } 362 363 /**** SwipeHelper Implementation ****/ 364 365 @Override 366 public View getChildAtPosition(MotionEvent ev) { 367 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 368 } 369 370 @Override 371 public boolean canChildBeDismissed(View v) { 372 return true; 373 } 374 375 @Override 376 public void onBeginDrag(View v) { 377 TaskView tv = (TaskView) v; 378 // Disable clipping with the stack while we are swiping 379 tv.setClipViewInStack(false); 380 // Disallow touch events from this task view 381 tv.setTouchEnabled(false); 382 // Disallow parents from intercepting touch events 383 final ViewParent parent = mSv.getParent(); 384 if (parent != null) { 385 parent.requestDisallowInterceptTouchEvent(true); 386 } 387 } 388 389 @Override 390 public void onSwipeChanged(View v, float delta) { 391 // Do nothing 392 } 393 394 @Override 395 public void onChildDismissed(View v) { 396 TaskView tv = (TaskView) v; 397 // Re-enable clipping with the stack (we will reuse this view) 398 tv.setClipViewInStack(true); 399 // Re-enable touch events from this task view 400 tv.setTouchEnabled(true); 401 // Remove the task view from the stack 402 mSv.onTaskViewDismissed(tv); 403 } 404 405 @Override 406 public void onSnapBackCompleted(View v) { 407 TaskView tv = (TaskView) v; 408 // Re-enable clipping with the stack 409 tv.setClipViewInStack(true); 410 // Re-enable touch events from this task view 411 tv.setTouchEnabled(true); 412 } 413 414 @Override 415 public void onDragCancelled(View v) { 416 // Do nothing 417 } 418} 419