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