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