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