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