TaskStackViewTouchHandler.java revision ebfc6981828b0699eef85c58b23a61f2cac41af3
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; 26import com.android.systemui.recents.RecentsConfiguration; 27 28/* Handles touch events for a TaskStackView. */ 29class TaskStackViewTouchHandler implements SwipeHelper.Callback { 30 static int INACTIVE_POINTER_ID = -1; 31 32 RecentsConfiguration mConfig; 33 TaskStackView mSv; 34 TaskStackViewScroller mScroller; 35 VelocityTracker mVelocityTracker; 36 37 boolean mIsScrolling; 38 39 float mInitialP; 40 float mLastP; 41 float mTotalPMotion; 42 int mInitialMotionX, mInitialMotionY; 43 int mLastMotionX, mLastMotionY; 44 int mActivePointerId = INACTIVE_POINTER_ID; 45 TaskView mActiveTaskView = null; 46 47 int mMinimumVelocity; 48 int mMaximumVelocity; 49 // The scroll touch slop is used to calculate when we start scrolling 50 int mScrollTouchSlop; 51 // The page touch slop is used to calculate when we start swiping 52 float mPagingTouchSlop; 53 54 SwipeHelper mSwipeHelper; 55 boolean mInterceptedBySwipeHelper; 56 57 public TaskStackViewTouchHandler(Context context, TaskStackView sv, 58 RecentsConfiguration config, TaskStackViewScroller scroller) { 59 ViewConfiguration configuration = ViewConfiguration.get(context); 60 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 61 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 62 mScrollTouchSlop = configuration.getScaledTouchSlop(); 63 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 64 mSv = sv; 65 mScroller = scroller; 66 mConfig = config; 67 68 float densityScale = context.getResources().getDisplayMetrics().density; 69 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop); 70 mSwipeHelper.setMinAlpha(1f); 71 } 72 73 /** Velocity tracker helpers */ 74 void initOrResetVelocityTracker() { 75 if (mVelocityTracker == null) { 76 mVelocityTracker = VelocityTracker.obtain(); 77 } else { 78 mVelocityTracker.clear(); 79 } 80 } 81 void initVelocityTrackerIfNotExists() { 82 if (mVelocityTracker == null) { 83 mVelocityTracker = VelocityTracker.obtain(); 84 } 85 } 86 void recycleVelocityTracker() { 87 if (mVelocityTracker != null) { 88 mVelocityTracker.recycle(); 89 mVelocityTracker = null; 90 } 91 } 92 93 /** Returns the view at the specified coordinates */ 94 TaskView findViewAtPoint(int x, int y) { 95 int childCount = mSv.getChildCount(); 96 for (int i = childCount - 1; i >= 0; i--) { 97 TaskView tv = (TaskView) mSv.getChildAt(i); 98 if (tv.getVisibility() == View.VISIBLE) { 99 if (mSv.isTransformedTouchPointInView(x, y, tv)) { 100 return tv; 101 } 102 } 103 } 104 return null; 105 } 106 107 /** Constructs a simulated motion event for the current stack scroll. */ 108 MotionEvent createMotionEventForStackScroll(MotionEvent ev) { 109 MotionEvent pev = MotionEvent.obtainNoHistory(ev); 110 pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll())); 111 return pev; 112 } 113 114 /** Touch preprocessing for handling below */ 115 public boolean onInterceptTouchEvent(MotionEvent ev) { 116 // Return early if we have no children 117 boolean hasChildren = (mSv.getChildCount() > 0); 118 if (!hasChildren) { 119 return false; 120 } 121 122 // Pass through to swipe helper if we are swiping 123 mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); 124 if (mInterceptedBySwipeHelper) { 125 return true; 126 } 127 128 boolean wasScrolling = mScroller.isScrolling() || 129 (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning()); 130 int action = ev.getAction(); 131 switch (action & MotionEvent.ACTION_MASK) { 132 case MotionEvent.ACTION_DOWN: { 133 // Save the touch down info 134 mInitialMotionX = mLastMotionX = (int) ev.getX(); 135 mInitialMotionY = mLastMotionY = (int) ev.getY(); 136 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 137 mActivePointerId = ev.getPointerId(0); 138 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 139 // Stop the current scroll if it is still flinging 140 mScroller.stopScroller(); 141 mScroller.stopBoundScrollAnimation(); 142 // Initialize the velocity tracker 143 initOrResetVelocityTracker(); 144 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 145 // Check if the scroller is finished yet 146 mIsScrolling = mScroller.isScrolling(); 147 break; 148 } 149 case MotionEvent.ACTION_MOVE: { 150 if (mActivePointerId == INACTIVE_POINTER_ID) break; 151 152 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 153 int y = (int) ev.getY(activePointerIndex); 154 int x = (int) ev.getX(activePointerIndex); 155 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 156 // Save the touch move info 157 mIsScrolling = true; 158 // Initialize the velocity tracker if necessary 159 initVelocityTrackerIfNotExists(); 160 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 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 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 /**** SwipeHelper Implementation ****/ 340 341 @Override 342 public View getChildAtPosition(MotionEvent ev) { 343 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 344 } 345 346 @Override 347 public boolean canChildBeDismissed(View v) { 348 return true; 349 } 350 351 @Override 352 public void onBeginDrag(View v) { 353 TaskView tv = (TaskView) v; 354 // Disable clipping with the stack while we are swiping 355 tv.setClipViewInStack(false); 356 // Disallow touch events from this task view 357 tv.setTouchEnabled(false); 358 // Hide the footer 359 tv.animateFooterVisibility(false, mSv.mConfig.taskViewLockToAppShortAnimDuration); 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 // Re-enable clipping with the stack (we will reuse this view) 376 tv.setClipViewInStack(true); 377 // Re-enable touch events from this task view 378 tv.setTouchEnabled(true); 379 // Remove the task view from the stack 380 mSv.onTaskViewDismissed(tv); 381 } 382 383 @Override 384 public void onSnapBackCompleted(View v) { 385 TaskView tv = (TaskView) v; 386 // Re-enable clipping with the stack 387 tv.setClipViewInStack(true); 388 // Re-enable touch events from this task view 389 tv.setTouchEnabled(true); 390 // Restore the footer 391 tv.animateFooterVisibility(true, mSv.mConfig.taskViewLockToAppShortAnimDuration); 392 } 393 394 @Override 395 public void onDragCancelled(View v) { 396 // Do nothing 397 } 398} 399