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