TaskStackView.java revision 863db8a4e7906826bc105f49a4596dc7b336088a
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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.ValueAnimator; 23import android.app.Activity; 24import android.content.ComponentName; 25import android.content.Context; 26import android.content.Intent; 27import android.graphics.Canvas; 28import android.graphics.Rect; 29import android.graphics.Region; 30import android.util.Pair; 31import android.view.LayoutInflater; 32import android.view.MotionEvent; 33import android.view.VelocityTracker; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewParent; 37import android.widget.FrameLayout; 38import android.widget.OverScroller; 39import com.android.systemui.R; 40import com.android.systemui.recents.Console; 41import com.android.systemui.recents.Constants; 42import com.android.systemui.recents.RecentsConfiguration; 43import com.android.systemui.recents.RecentsPackageMonitor; 44import com.android.systemui.recents.RecentsTaskLoader; 45import com.android.systemui.recents.Utilities; 46import com.android.systemui.recents.model.Task; 47import com.android.systemui.recents.model.TaskStack; 48 49import java.util.ArrayList; 50import java.util.HashMap; 51import java.util.Set; 52 53 54/* The visual representation of a task stack view */ 55public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks, 56 TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>, 57 View.OnClickListener, RecentsPackageMonitor.PackageCallbacks { 58 59 /** The TaskView callbacks */ 60 interface TaskStackViewCallbacks { 61 public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t); 62 public void onTaskAppInfoLaunched(Task t); 63 public void onTaskRemoved(Task t); 64 } 65 66 TaskStack mStack; 67 TaskStackViewTouchHandler mTouchHandler; 68 TaskStackViewCallbacks mCb; 69 ViewPool<TaskView, Task> mViewPool; 70 71 // The various rects that define the stack view 72 Rect mRect = new Rect(); 73 Rect mStackRect = new Rect(); 74 Rect mStackRectSansPeek = new Rect(); 75 Rect mTaskRect = new Rect(); 76 77 // The virtual stack scroll that we use for the card layout 78 int mStackScroll; 79 int mMinScroll; 80 int mMaxScroll; 81 int mStashedScroll; 82 int mFocusedTaskIndex = -1; 83 OverScroller mScroller; 84 ObjectAnimator mScrollAnimator; 85 86 // Optimizations 87 int mHwLayersRefCount; 88 int mStackViewsAnimationDuration; 89 boolean mStackViewsDirty = true; 90 boolean mAwaitingFirstLayout = true; 91 int[] mTmpVisibleRange = new int[2]; 92 Rect mTmpRect = new Rect(); 93 Rect mTmpRect2 = new Rect(); 94 LayoutInflater mInflater; 95 96 public TaskStackView(Context context, TaskStack stack) { 97 super(context); 98 mStack = stack; 99 mStack.setCallbacks(this); 100 mScroller = new OverScroller(context); 101 mTouchHandler = new TaskStackViewTouchHandler(context, this); 102 mViewPool = new ViewPool<TaskView, Task>(context, this); 103 mInflater = LayoutInflater.from(context); 104 } 105 106 /** Sets the callbacks */ 107 void setCallbacks(TaskStackViewCallbacks cb) { 108 mCb = cb; 109 } 110 111 /** Requests that the views be synchronized with the model */ 112 void requestSynchronizeStackViewsWithModel() { 113 requestSynchronizeStackViewsWithModel(0); 114 } 115 void requestSynchronizeStackViewsWithModel(int duration) { 116 Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel, 117 "[TaskStackView|requestSynchronize]", "" + duration + "ms", Console.AnsiYellow); 118 if (!mStackViewsDirty) { 119 invalidate(); 120 } 121 if (mAwaitingFirstLayout) { 122 // Skip the animation if we are awaiting first layout 123 mStackViewsAnimationDuration = 0; 124 } else { 125 mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); 126 } 127 mStackViewsDirty = true; 128 } 129 130 // XXX: Optimization: Use a mapping of Task -> View 131 private TaskView getChildViewForTask(Task t) { 132 int childCount = getChildCount(); 133 for (int i = 0; i < childCount; i++) { 134 TaskView tv = (TaskView) getChildAt(i); 135 if (tv.getTask() == t) { 136 return tv; 137 } 138 } 139 return null; 140 } 141 142 /** Update/get the transform */ 143 public TaskViewTransform getStackTransform(int indexInStack, int stackScroll) { 144 TaskViewTransform transform = new TaskViewTransform(); 145 146 // Return early if we have an invalid index 147 if (indexInStack < 0) return transform; 148 149 // Map the items to an continuous position relative to the specified scroll 150 int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards; 151 float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); 152 float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); 153 float t = ((indexInStack * overlapHeight) - stackScroll) / overlapHeight; 154 float boundedT = Math.max(t, -(numPeekCards + 1)); 155 156 // Set the scale relative to its position 157 float minScale = Constants.Values.TaskStackView.StackPeekMinScale; 158 float scaleRange = 1f - minScale; 159 float scaleInc = scaleRange / numPeekCards; 160 float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc))); 161 float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2; 162 transform.scale = scale; 163 164 // Set the translation 165 if (boundedT < 0f) { 166 transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) / 167 numPeekCards) * peekHeight - scaleYOffset); 168 } else { 169 transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset); 170 } 171 172 // Set the alphas 173 transform.dismissAlpha = Math.max(-1f, Math.min(0f, t)) + 1f; 174 175 // Update the rect and visibility 176 transform.rect.set(mTaskRect); 177 if (t < -(numPeekCards + 1)) { 178 transform.visible = false; 179 } else { 180 transform.rect.offset(0, transform.translationY); 181 Utilities.scaleRectAboutCenter(transform.rect, transform.scale); 182 transform.visible = Rect.intersects(mRect, transform.rect); 183 } 184 transform.t = t; 185 return transform; 186 } 187 188 /** 189 * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. 190 */ 191 private ArrayList<TaskViewTransform> getStackTransforms(ArrayList<Task> tasks, 192 int stackScroll, 193 int[] visibleRangeOut, 194 boolean boundTranslationsToRect) { 195 // XXX: Optimization: Use binary search to find the visible range 196 197 ArrayList<TaskViewTransform> taskTransforms = new ArrayList<TaskViewTransform>(); 198 int taskCount = tasks.size(); 199 int firstVisibleIndex = -1; 200 int lastVisibleIndex = -1; 201 for (int i = 0; i < taskCount; i++) { 202 TaskViewTransform transform = getStackTransform(i, stackScroll); 203 taskTransforms.add(transform); 204 if (transform.visible) { 205 if (firstVisibleIndex < 0) { 206 firstVisibleIndex = i; 207 } 208 lastVisibleIndex = i; 209 } 210 211 if (boundTranslationsToRect) { 212 transform.translationY = Math.min(transform.translationY, mRect.bottom); 213 } 214 } 215 if (visibleRangeOut != null) { 216 visibleRangeOut[0] = firstVisibleIndex; 217 visibleRangeOut[1] = lastVisibleIndex; 218 } 219 return taskTransforms; 220 } 221 222 /** Synchronizes the views with the model */ 223 void synchronizeStackViewsWithModel() { 224 Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel, 225 "[TaskStackView|synchronizeViewsWithModel]", 226 "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow); 227 if (mStackViewsDirty) { 228 // XXX: Consider using TaskViewTransform pool to prevent allocations 229 // XXX: Iterate children views, update transforms and remove all that are not visible 230 // For all remaining tasks, update transforms and if visible add the view 231 232 // Get all the task transforms 233 int[] visibleRange = mTmpVisibleRange; 234 int stackScroll = getStackScroll(); 235 ArrayList<Task> tasks = mStack.getTasks(); 236 ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(tasks, stackScroll, 237 visibleRange, false); 238 239 // Update the visible state of all the tasks 240 int taskCount = tasks.size(); 241 for (int i = 0; i < taskCount; i++) { 242 Task task = tasks.get(i); 243 TaskViewTransform transform = taskTransforms.get(i); 244 TaskView tv = getChildViewForTask(task); 245 246 if (transform.visible) { 247 if (tv == null) { 248 tv = mViewPool.pickUpViewFromPool(task, task); 249 // When we are picking up a new view from the view pool, prepare it for any 250 // following animation by putting it in a reasonable place 251 if (mStackViewsAnimationDuration > 0 && i != 0) { 252 int fromIndex = (transform.t < 0) ? (visibleRange[0] - 1) : 253 (visibleRange[1] + 1); 254 tv.updateViewPropertiesToTaskTransform(null, 255 getStackTransform(fromIndex, stackScroll), 0); 256 } 257 } 258 } else { 259 if (tv != null) { 260 mViewPool.returnViewToPool(tv); 261 } 262 } 263 } 264 265 // Update all the remaining view children 266 // NOTE: We have to iterate in reverse where because we are removing views directly 267 int childCount = getChildCount(); 268 for (int i = childCount - 1; i >= 0; i--) { 269 TaskView tv = (TaskView) getChildAt(i); 270 Task task = tv.getTask(); 271 int taskIndex = mStack.indexOfTask(task); 272 if (taskIndex < 0 || !taskTransforms.get(taskIndex).visible) { 273 mViewPool.returnViewToPool(tv); 274 } else { 275 tv.updateViewPropertiesToTaskTransform(null, taskTransforms.get(taskIndex), 276 mStackViewsAnimationDuration); 277 } 278 } 279 280 Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel, 281 " [TaskStackView|viewChildren]", "" + getChildCount()); 282 283 mStackViewsAnimationDuration = 0; 284 mStackViewsDirty = false; 285 } 286 } 287 288 /** Sets the current stack scroll */ 289 public void setStackScroll(int value) { 290 mStackScroll = value; 291 requestSynchronizeStackViewsWithModel(); 292 } 293 /** Sets the current stack scroll without synchronizing the stack view with the model */ 294 public void setStackScrollRaw(int value) { 295 mStackScroll = value; 296 } 297 298 /** 299 * Returns the scroll to such that the task transform at that index will have t=0. (If the scroll 300 * is not bounded) 301 */ 302 int getStackScrollForTaskIndex(int i) { 303 int taskHeight = mTaskRect.height(); 304 return (int) (i * Constants.Values.TaskStackView.StackOverlapPct * taskHeight); 305 } 306 307 /** Gets the current stack scroll */ 308 public int getStackScroll() { 309 return mStackScroll; 310 } 311 312 /** Animates the stack scroll into bounds */ 313 ObjectAnimator animateBoundScroll() { 314 int curScroll = getStackScroll(); 315 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); 316 if (newScroll != curScroll) { 317 // Enable hw layers on the stack 318 addHwLayersRefCount("animateBoundScroll"); 319 320 // Start a new scroll animation 321 animateScroll(curScroll, newScroll, new Runnable() { 322 @Override 323 public void run() { 324 // Disable hw layers on the stack 325 decHwLayersRefCount("animateBoundScroll"); 326 } 327 }); 328 } 329 return mScrollAnimator; 330 } 331 332 /** Animates the stack scroll */ 333 void animateScroll(int curScroll, int newScroll, final Runnable postRunnable) { 334 // Abort any current animations 335 abortScroller(); 336 abortBoundScrollAnimation(); 337 338 mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll); 339 mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll - 340 curScroll, 250)); 341 mScrollAnimator.setInterpolator(RecentsConfiguration.getInstance().defaultBezierInterpolator); 342 mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 343 @Override 344 public void onAnimationUpdate(ValueAnimator animation) { 345 setStackScroll((Integer) animation.getAnimatedValue()); 346 } 347 }); 348 mScrollAnimator.addListener(new AnimatorListenerAdapter() { 349 @Override 350 public void onAnimationEnd(Animator animation) { 351 if (postRunnable != null) { 352 postRunnable.run(); 353 } 354 mScrollAnimator.removeAllListeners(); 355 } 356 }); 357 mScrollAnimator.start(); 358 } 359 360 /** Aborts any current stack scrolls */ 361 void abortBoundScrollAnimation() { 362 if (mScrollAnimator != null) { 363 mScrollAnimator.cancel(); 364 } 365 } 366 367 /** Aborts the scroller and any current fling */ 368 void abortScroller() { 369 if (!mScroller.isFinished()) { 370 // Abort the scroller 371 mScroller.abortAnimation(); 372 // And disable hw layers on the stack 373 decHwLayersRefCount("flingScroll"); 374 } 375 } 376 377 /** Bounds the current scroll if necessary */ 378 public boolean boundScroll() { 379 int curScroll = getStackScroll(); 380 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); 381 if (newScroll != curScroll) { 382 setStackScroll(newScroll); 383 return true; 384 } 385 return false; 386 } 387 388 /** 389 * Bounds the current scroll if necessary, but does not synchronize the stack view with the 390 * model. 391 */ 392 public boolean boundScrollRaw() { 393 int curScroll = getStackScroll(); 394 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); 395 if (newScroll != curScroll) { 396 setStackScrollRaw(newScroll); 397 return true; 398 } 399 return false; 400 } 401 402 403 /** Returns the amount that the scroll is out of bounds */ 404 int getScrollAmountOutOfBounds(int scroll) { 405 if (scroll < mMinScroll) { 406 return mMinScroll - scroll; 407 } else if (scroll > mMaxScroll) { 408 return scroll - mMaxScroll; 409 } 410 return 0; 411 } 412 413 /** Returns whether the specified scroll is out of bounds */ 414 boolean isScrollOutOfBounds() { 415 return getScrollAmountOutOfBounds(getStackScroll()) != 0; 416 } 417 418 /** Updates the min and max virtual scroll bounds */ 419 void updateMinMaxScroll(boolean boundScrollToNewMinMax) { 420 // Compute the min and max scroll values 421 int numTasks = Math.max(1, mStack.getTaskCount()); 422 int taskHeight = mTaskRect.height(); 423 int stackHeight = mStackRectSansPeek.height(); 424 int maxScrollHeight = taskHeight + (int) ((numTasks - 1) * 425 Constants.Values.TaskStackView.StackOverlapPct * taskHeight); 426 427 if (numTasks <= 1) { 428 // If there is only one task, then center the task in the stack rect (sans peek) 429 mMinScroll = mMaxScroll = -(stackHeight - taskHeight) / 2; 430 } else { 431 mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight; 432 mMaxScroll = maxScrollHeight - stackHeight; 433 } 434 435 // Debug logging 436 if (Constants.Log.UI.MeasureAndLayout) { 437 Console.log(" [TaskStack|minScroll] " + mMinScroll); 438 Console.log(" [TaskStack|maxScroll] " + mMaxScroll); 439 } 440 441 if (boundScrollToNewMinMax) { 442 boundScroll(); 443 } 444 } 445 446 /** Focuses the task at the specified index in the stack */ 447 void focusTask(int taskIndex, boolean scrollToNewPosition) { 448 Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "" + taskIndex); 449 if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) { 450 mFocusedTaskIndex = taskIndex; 451 452 // Focus the view if possible, otherwise, focus the view after we scroll into position 453 Task t = mStack.getTasks().get(taskIndex); 454 TaskView tv = getChildViewForTask(t); 455 Runnable postScrollRunnable = null; 456 if (tv != null) { 457 tv.setFocusedTask(); 458 Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "Requesting focus"); 459 } else { 460 postScrollRunnable = new Runnable() { 461 @Override 462 public void run() { 463 Task t = mStack.getTasks().get(mFocusedTaskIndex); 464 TaskView tv = getChildViewForTask(t); 465 if (tv != null) { 466 tv.setFocusedTask(); 467 Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", 468 "Requesting focus after scroll animation"); 469 } 470 } 471 }; 472 } 473 474 if (scrollToNewPosition) { 475 // Scroll the view into position 476 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, 477 getStackScrollForTaskIndex(taskIndex))); 478 479 animateScroll(getStackScroll(), newScroll, postScrollRunnable); 480 } else { 481 if (postScrollRunnable != null) { 482 postScrollRunnable.run(); 483 } 484 } 485 } 486 } 487 488 /** Focuses the next task in the stack */ 489 void focusNextTask(boolean forward) { 490 Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusNextTask]", "" + mFocusedTaskIndex); 491 492 // Find the next index to focus 493 int numTasks = mStack.getTaskCount(); 494 if (mFocusedTaskIndex < 0) { 495 mFocusedTaskIndex = numTasks - 1; 496 } 497 if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) { 498 mFocusedTaskIndex = Math.max(0, Math.min(numTasks - 1, 499 mFocusedTaskIndex + (forward ? -1 : 1))); 500 } 501 focusTask(mFocusedTaskIndex, true); 502 } 503 504 /** Enables the hw layers and increments the hw layer requirement ref count */ 505 void addHwLayersRefCount(String reason) { 506 Console.log(Constants.Log.UI.HwLayers, 507 "[TaskStackView|addHwLayersRefCount] refCount: " + 508 mHwLayersRefCount + "->" + (mHwLayersRefCount + 1) + " " + reason); 509 if (mHwLayersRefCount == 0) { 510 // Enable hw layers on each of the children 511 int childCount = getChildCount(); 512 for (int i = 0; i < childCount; i++) { 513 TaskView tv = (TaskView) getChildAt(i); 514 tv.enableHwLayers(); 515 } 516 } 517 mHwLayersRefCount++; 518 } 519 520 /** Decrements the hw layer requirement ref count and disables the hw layers when we don't 521 need them anymore. */ 522 void decHwLayersRefCount(String reason) { 523 Console.log(Constants.Log.UI.HwLayers, 524 "[TaskStackView|decHwLayersRefCount] refCount: " + 525 mHwLayersRefCount + "->" + (mHwLayersRefCount - 1) + " " + reason); 526 mHwLayersRefCount--; 527 if (mHwLayersRefCount == 0) { 528 // Disable hw layers on each of the children 529 int childCount = getChildCount(); 530 for (int i = 0; i < childCount; i++) { 531 TaskView tv = (TaskView) getChildAt(i); 532 tv.disableHwLayers(); 533 } 534 } else if (mHwLayersRefCount < 0) { 535 new Throwable("Invalid hw layers ref count").printStackTrace(); 536 Console.logError(getContext(), "Invalid HW layers ref count"); 537 } 538 } 539 540 @Override 541 public void computeScroll() { 542 if (mScroller.computeScrollOffset()) { 543 setStackScroll(mScroller.getCurrY()); 544 invalidate(); 545 546 // If we just finished scrolling, then disable the hw layers 547 if (mScroller.isFinished()) { 548 decHwLayersRefCount("finishedFlingScroll"); 549 } 550 } 551 } 552 553 @Override 554 public boolean onInterceptTouchEvent(MotionEvent ev) { 555 return mTouchHandler.onInterceptTouchEvent(ev); 556 } 557 558 @Override 559 public boolean onTouchEvent(MotionEvent ev) { 560 return mTouchHandler.onTouchEvent(ev); 561 } 562 563 @Override 564 public void dispatchDraw(Canvas canvas) { 565 Console.log(Constants.Log.UI.Draw, "[TaskStackView|dispatchDraw]", "", 566 Console.AnsiPurple); 567 synchronizeStackViewsWithModel(); 568 super.dispatchDraw(canvas); 569 } 570 571 @Override 572 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 573 if (Constants.DebugFlags.App.EnableTaskStackClipping) { 574 TaskView tv = (TaskView) child; 575 TaskView nextTv = null; 576 int curIndex = indexOfChild(tv); 577 if ((curIndex > -1) && (curIndex < (getChildCount() - 1))) { 578 // Clip against the next view (if we aren't animating its alpha) 579 nextTv = (TaskView) getChildAt(curIndex + 1); 580 if (nextTv.getAlpha() == 1f) { 581 Rect curRect = tv.getClippingRect(mTmpRect); 582 Rect nextRect = nextTv.getClippingRect(mTmpRect2); 583 RecentsConfiguration config = RecentsConfiguration.getInstance(); 584 // The hit rects are relative to the task view, which needs to be offset by the 585 // system bar height 586 curRect.offset(0, config.systemInsets.top); 587 nextRect.offset(0, config.systemInsets.top); 588 // Compute the clip region 589 Region clipRegion = new Region(); 590 clipRegion.op(curRect, Region.Op.UNION); 591 clipRegion.op(nextRect, Region.Op.DIFFERENCE); 592 // Clip the canvas 593 int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 594 canvas.clipRegion(clipRegion); 595 boolean invalidate = super.drawChild(canvas, child, drawingTime); 596 canvas.restoreToCount(saveCount); 597 return invalidate; 598 } 599 } 600 } 601 return super.drawChild(canvas, child, drawingTime); 602 } 603 604 /** Computes the stack and task rects */ 605 public void computeRects(int width, int height, int insetLeft, int insetBottom) { 606 // Note: We let the stack view be the full height because we want the cards to go under the 607 // navigation bar if possible. However, the stack rects which we use to calculate 608 // max scroll, etc. need to take the nav bar into account 609 610 // Compute the stack rects 611 mRect.set(0, 0, width, height); 612 mStackRect.set(mRect); 613 mStackRect.left += insetLeft; 614 mStackRect.bottom -= insetBottom; 615 616 int smallestDimension = Math.min(width, height); 617 int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f); 618 if (Constants.DebugFlags.App.EnableSearchLayout) { 619 mStackRect.top += padding; 620 mStackRect.left += padding; 621 mStackRect.right -= padding; 622 mStackRect.bottom -= padding; 623 } else { 624 mStackRect.inset(padding, padding); 625 } 626 mStackRectSansPeek.set(mStackRect); 627 mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); 628 629 // Compute the task rect 630 int minHeight = (int) (mStackRect.height() - 631 (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height())); 632 int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height())); 633 int left = mStackRect.left + (mStackRect.width() - size) / 2; 634 mTaskRect.set(left, mStackRectSansPeek.top, 635 left + size, mStackRectSansPeek.top + size); 636 637 // Update the scroll bounds 638 updateMinMaxScroll(false); 639 } 640 641 /** 642 * This is called with the size of the space not including the top or right insets, or the 643 * search bar height in portrait (but including the search bar width in landscape, since we want 644 * to draw under it. 645 */ 646 @Override 647 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 648 int width = MeasureSpec.getSize(widthMeasureSpec); 649 int height = MeasureSpec.getSize(heightMeasureSpec); 650 Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|measure]", 651 "width: " + width + " height: " + height + 652 " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen); 653 654 // Compute our stack/task rects 655 RecentsConfiguration config = RecentsConfiguration.getInstance(); 656 Rect taskStackBounds = new Rect(); 657 config.getTaskStackBounds(width, height, taskStackBounds); 658 computeRects(width, height, taskStackBounds.left, config.systemInsets.bottom); 659 660 // Debug logging 661 if (Constants.Log.UI.MeasureAndLayout) { 662 Console.log(" [TaskStack|fullRect] " + mRect); 663 Console.log(" [TaskStack|stackRect] " + mStackRect); 664 Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); 665 Console.log(" [TaskStack|taskRect] " + mTaskRect); 666 } 667 668 // If this is the first layout, then scroll to the front of the stack and synchronize the 669 // stack views immediately 670 if (mAwaitingFirstLayout) { 671 setStackScroll(mMaxScroll); 672 requestSynchronizeStackViewsWithModel(); 673 synchronizeStackViewsWithModel(); 674 675 // Update the focused task index to be the next item to the top task 676 if (config.launchedFromAltTab) { 677 focusTask(Math.max(0, mStack.getTaskCount() - 2), false); 678 } 679 680 // Animate the task bar of the first task view 681 if (config.launchedWithThumbnailAnimation) { 682 TaskView tv = (TaskView) getChildAt(getChildCount() - 1); 683 if (tv != null) { 684 tv.animateOnEnterRecents(); 685 } 686 } 687 } 688 689 // Measure each of the children 690 int childCount = getChildCount(); 691 for (int i = 0; i < childCount; i++) { 692 TaskView t = (TaskView) getChildAt(i); 693 t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY), 694 MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY)); 695 } 696 697 setMeasuredDimension(width, height); 698 } 699 700 /** 701 * This is called with the size of the space not including the top or right insets, or the 702 * search bar height in portrait (but including the search bar width in landscape, since we want 703 * to draw under it. 704 */ 705 @Override 706 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 707 Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|layout]", 708 "" + new Rect(left, top, right, bottom), Console.AnsiGreen); 709 710 // Debug logging 711 if (Constants.Log.UI.MeasureAndLayout) { 712 Console.log(" [TaskStack|fullRect] " + mRect); 713 Console.log(" [TaskStack|stackRect] " + mStackRect); 714 Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); 715 Console.log(" [TaskStack|taskRect] " + mTaskRect); 716 } 717 718 // Layout each of the children 719 int childCount = getChildCount(); 720 for (int i = 0; i < childCount; i++) { 721 TaskView t = (TaskView) getChildAt(i); 722 t.layout(mTaskRect.left, mStackRectSansPeek.top, 723 mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height()); 724 } 725 726 if (mAwaitingFirstLayout) { 727 mAwaitingFirstLayout = false; 728 } 729 } 730 731 @Override 732 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 733 super.onScrollChanged(l, t, oldl, oldt); 734 requestSynchronizeStackViewsWithModel(); 735 } 736 737 public boolean isTransformedTouchPointInView(float x, float y, View child) { 738 return isTransformedTouchPointInView(x, y, child, null); 739 } 740 741 /**** TaskStackCallbacks Implementation ****/ 742 743 @Override 744 public void onStackTaskAdded(TaskStack stack, Task t) { 745 requestSynchronizeStackViewsWithModel(); 746 } 747 748 @Override 749 public void onStackTaskRemoved(TaskStack stack, Task t) { 750 // Remove the view associated with this task, we can't rely on updateTransforms 751 // to work here because the task is no longer in the list 752 int childCount = getChildCount(); 753 for (int i = childCount - 1; i >= 0; i--) { 754 TaskView tv = (TaskView) getChildAt(i); 755 if (tv.getTask() == t) { 756 mViewPool.returnViewToPool(tv); 757 break; 758 } 759 } 760 761 // Update the min/max scroll and animate other task views into their new positions 762 updateMinMaxScroll(true); 763 int movement = (int) (Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height()); 764 requestSynchronizeStackViewsWithModel(Utilities.calculateTranslationAnimationDuration(movement)); 765 766 // If there are no remaining tasks, then either unfilter the current stack, or just close 767 // the activity if there are no filtered stacks 768 if (mStack.getTaskCount() == 0) { 769 boolean shouldFinishActivity = true; 770 if (mStack.hasFilteredTasks()) { 771 mStack.unfilterTasks(); 772 shouldFinishActivity = (mStack.getTaskCount() == 0); 773 } 774 if (shouldFinishActivity) { 775 Activity activity = (Activity) getContext(); 776 activity.finish(); 777 } 778 } 779 } 780 781 /** 782 * Creates the animations for all the children views that need to be removed or to move views 783 * to their un/filtered position when we are un/filtering a stack, and returns the duration 784 * for these animations. 785 */ 786 int getExitTransformsForFilterAnimation(ArrayList<Task> curTasks, 787 ArrayList<TaskViewTransform> curTaskTransforms, 788 ArrayList<Task> tasks, ArrayList<TaskViewTransform> taskTransforms, 789 HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut, 790 ArrayList<TaskView> childrenToRemoveOut, 791 RecentsConfiguration config) { 792 // Animate all of the existing views out of view (if they are not in the visible range in 793 // the new stack) or to their final positions in the new stack 794 int movement = 0; 795 int childCount = getChildCount(); 796 for (int i = 0; i < childCount; i++) { 797 TaskView tv = (TaskView) getChildAt(i); 798 Task task = tv.getTask(); 799 int taskIndex = tasks.indexOf(task); 800 TaskViewTransform toTransform; 801 802 // If the view is no longer visible, then we should just animate it out 803 boolean willBeInvisible = taskIndex < 0 || !taskTransforms.get(taskIndex).visible; 804 if (willBeInvisible) { 805 if (taskIndex < 0) { 806 toTransform = curTaskTransforms.get(curTasks.indexOf(task)); 807 } else { 808 toTransform = new TaskViewTransform(taskTransforms.get(taskIndex)); 809 } 810 tv.prepareTaskTransformForFilterTaskVisible(toTransform); 811 childrenToRemoveOut.add(tv); 812 } else { 813 toTransform = taskTransforms.get(taskIndex); 814 // Use the movement of the visible views to calculate the duration of the animation 815 movement = Math.max(movement, Math.abs(toTransform.translationY - 816 (int) tv.getTranslationY())); 817 } 818 childViewTransformsOut.put(tv, new Pair(0, toTransform)); 819 } 820 return Utilities.calculateTranslationAnimationDuration(movement, 821 config.filteringCurrentViewsMinAnimDuration); 822 } 823 824 /** 825 * Creates the animations for all the children views that need to be animated in when we are 826 * un/filtering a stack, and returns the duration for these animations. 827 */ 828 int getEnterTransformsForFilterAnimation(ArrayList<Task> tasks, 829 ArrayList<TaskViewTransform> taskTransforms, 830 HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut, 831 RecentsConfiguration config) { 832 int offset = 0; 833 int movement = 0; 834 int taskCount = tasks.size(); 835 for (int i = taskCount - 1; i >= 0; i--) { 836 Task task = tasks.get(i); 837 TaskViewTransform toTransform = taskTransforms.get(i); 838 if (toTransform.visible) { 839 TaskView tv = getChildViewForTask(task); 840 if (tv == null) { 841 // For views that are not already visible, animate them in 842 tv = mViewPool.pickUpViewFromPool(task, task); 843 844 // Compose a new transform to fade and slide the new task in 845 TaskViewTransform fromTransform = new TaskViewTransform(toTransform); 846 tv.prepareTaskTransformForFilterTaskHidden(fromTransform); 847 tv.updateViewPropertiesToTaskTransform(null, fromTransform, 0); 848 849 int startDelay = offset * 850 Constants.Values.TaskStackView.FilterStartDelay; 851 childViewTransformsOut.put(tv, new Pair(startDelay, toTransform)); 852 853 // Use the movement of the new views to calculate the duration of the animation 854 movement = Math.max(movement, 855 Math.abs(toTransform.translationY - fromTransform.translationY)); 856 offset++; 857 } 858 } 859 } 860 return Utilities.calculateTranslationAnimationDuration(movement, 861 config.filteringNewViewsMinAnimDuration); 862 } 863 864 /** Orchestrates the animations of the current child views and any new views. */ 865 void doFilteringAnimation(ArrayList<Task> curTasks, 866 ArrayList<TaskViewTransform> curTaskTransforms, 867 final ArrayList<Task> tasks, 868 final ArrayList<TaskViewTransform> taskTransforms) { 869 final RecentsConfiguration config = RecentsConfiguration.getInstance(); 870 871 // Calculate the transforms to animate out all the existing views if they are not in the 872 // new visible range (or to their final positions in the stack if they are) 873 final ArrayList<TaskView> childrenToRemove = new ArrayList<TaskView>(); 874 final HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransforms = 875 new HashMap<TaskView, Pair<Integer, TaskViewTransform>>(); 876 int duration = getExitTransformsForFilterAnimation(curTasks, curTaskTransforms, tasks, 877 taskTransforms, childViewTransforms, childrenToRemove, config); 878 879 // If all the current views are in the visible range of the new stack, then don't wait for 880 // views to animate out and animate all the new views into their place 881 final boolean unifyNewViewAnimation = childrenToRemove.isEmpty(); 882 if (unifyNewViewAnimation) { 883 int inDuration = getEnterTransformsForFilterAnimation(tasks, taskTransforms, 884 childViewTransforms, config); 885 duration = Math.max(duration, inDuration); 886 } 887 888 // Animate all the views to their final transforms 889 for (final TaskView tv : childViewTransforms.keySet()) { 890 Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv); 891 tv.animate().cancel(); 892 tv.animate() 893 .setStartDelay(t.first) 894 .withEndAction(new Runnable() { 895 @Override 896 public void run() { 897 childViewTransforms.remove(tv); 898 if (childViewTransforms.isEmpty()) { 899 // Return all the removed children to the view pool 900 for (TaskView tv : childrenToRemove) { 901 mViewPool.returnViewToPool(tv); 902 } 903 904 if (!unifyNewViewAnimation) { 905 // For views that are not already visible, animate them in 906 childViewTransforms.clear(); 907 int duration = getEnterTransformsForFilterAnimation(tasks, 908 taskTransforms, childViewTransforms, config); 909 for (final TaskView tv : childViewTransforms.keySet()) { 910 Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv); 911 tv.animate().setStartDelay(t.first); 912 tv.updateViewPropertiesToTaskTransform(null, t.second, duration); 913 } 914 } 915 } 916 } 917 }); 918 tv.updateViewPropertiesToTaskTransform(null, t.second, duration); 919 } 920 } 921 922 @Override 923 public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks, 924 Task filteredTask) { 925 // Stash the scroll and filtered task for us to restore to when we unfilter 926 mStashedScroll = getStackScroll(); 927 928 // Calculate the current task transforms 929 ArrayList<TaskViewTransform> curTaskTransforms = 930 getStackTransforms(curTasks, getStackScroll(), null, true); 931 932 // Scroll the item to the top of the stack (sans-peek) rect so that we can see it better 933 updateMinMaxScroll(false); 934 float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); 935 setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight)); 936 boundScrollRaw(); 937 938 // Compute the transforms of the items in the new stack after setting the new scroll 939 final ArrayList<Task> tasks = mStack.getTasks(); 940 final ArrayList<TaskViewTransform> taskTransforms = 941 getStackTransforms(mStack.getTasks(), getStackScroll(), null, true); 942 943 // Animate 944 doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); 945 } 946 947 @Override 948 public void onStackUnfiltered(TaskStack newStack, final ArrayList<Task> curTasks) { 949 // Calculate the current task transforms 950 final ArrayList<TaskViewTransform> curTaskTransforms = 951 getStackTransforms(curTasks, getStackScroll(), null, true); 952 953 // Restore the stashed scroll 954 updateMinMaxScroll(false); 955 setStackScrollRaw(mStashedScroll); 956 boundScrollRaw(); 957 958 // Compute the transforms of the items in the new stack after restoring the stashed scroll 959 final ArrayList<Task> tasks = mStack.getTasks(); 960 final ArrayList<TaskViewTransform> taskTransforms = 961 getStackTransforms(tasks, getStackScroll(), null, true); 962 963 // Animate 964 doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); 965 966 // Clear the saved vars 967 mStashedScroll = 0; 968 } 969 970 /**** ViewPoolConsumer Implementation ****/ 971 972 @Override 973 public TaskView createView(Context context) { 974 Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]"); 975 return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false); 976 } 977 978 @Override 979 public void prepareViewToEnterPool(TaskView tv) { 980 Task task = tv.getTask(); 981 tv.resetViewProperties(); 982 Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]", 983 tv.getTask() + " tv: " + tv); 984 985 // Report that this tasks's data is no longer being used 986 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 987 loader.unloadTaskData(task); 988 989 // Detach the view from the hierarchy 990 detachViewFromParent(tv); 991 992 // Disable hw layers on this view 993 tv.disableHwLayers(); 994 } 995 996 @Override 997 public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) { 998 Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]", 999 "isNewView: " + isNewView); 1000 1001 // Setup and attach the view to the window 1002 Task task = prepareData; 1003 // We try and rebind the task (this MUST be done before the task filled) 1004 tv.onTaskBound(task); 1005 // Request that this tasks's data be filled 1006 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 1007 loader.loadTaskData(task); 1008 1009 // Find the index where this task should be placed in the children 1010 int insertIndex = -1; 1011 int childCount = getChildCount(); 1012 for (int i = 0; i < childCount; i++) { 1013 Task tvTask = ((TaskView) getChildAt(i)).getTask(); 1014 if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) { 1015 insertIndex = i; 1016 break; 1017 } 1018 } 1019 1020 // Add/attach the view to the hierarchy 1021 Console.log(Constants.Log.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]", 1022 "" + insertIndex); 1023 if (isNewView) { 1024 addView(tv, insertIndex); 1025 1026 // Set the callbacks and listeners for this new view 1027 tv.setOnClickListener(this); 1028 tv.setCallbacks(this); 1029 } else { 1030 attachViewToParent(tv, insertIndex, tv.getLayoutParams()); 1031 } 1032 1033 // Enable hw layers on this view if hw layers are enabled on the stack 1034 if (mHwLayersRefCount > 0) { 1035 tv.enableHwLayers(); 1036 } 1037 } 1038 1039 @Override 1040 public boolean hasPreferredData(TaskView tv, Task preferredData) { 1041 return (tv.getTask() == preferredData); 1042 } 1043 1044 /**** TaskViewCallbacks Implementation ****/ 1045 1046 @Override 1047 public void onTaskIconClicked(TaskView tv) { 1048 Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Icon]", 1049 tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(), 1050 Console.AnsiCyan); 1051 if (Constants.DebugFlags.App.EnableTaskFiltering) { 1052 if (mStack.hasFilteredTasks()) { 1053 mStack.unfilterTasks(); 1054 } else { 1055 mStack.filterTasks(tv.getTask()); 1056 } 1057 } 1058 } 1059 1060 @Override 1061 public void onTaskAppInfoClicked(TaskView tv) { 1062 if (mCb != null) { 1063 mCb.onTaskAppInfoLaunched(tv.getTask()); 1064 } 1065 } 1066 1067 @Override 1068 public void onTaskDismissed(TaskView tv) { 1069 Task task = tv.getTask(); 1070 // Remove the task from the view 1071 mStack.removeTask(task); 1072 // Notify the callback that we've removed the task and it can clean up after it 1073 mCb.onTaskRemoved(task); 1074 } 1075 1076 /**** View.OnClickListener Implementation ****/ 1077 1078 @Override 1079 public void onClick(View v) { 1080 TaskView tv = (TaskView) v; 1081 Task task = tv.getTask(); 1082 Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]", 1083 task + " cb: " + mCb); 1084 1085 if (mCb != null) { 1086 mCb.onTaskLaunched(this, tv, mStack, task); 1087 } 1088 } 1089 1090 /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ 1091 1092 @Override 1093 public void onComponentRemoved(Set<ComponentName> cns) { 1094 RecentsConfiguration config = RecentsConfiguration.getInstance(); 1095 // For other tasks, just remove them directly if they no longer exist 1096 ArrayList<Task> tasks = mStack.getTasks(); 1097 for (int i = tasks.size() - 1; i >= 0; i--) { 1098 final Task t = tasks.get(i); 1099 if (cns.contains(t.key.baseIntent.getComponent())) { 1100 TaskView tv = getChildViewForTask(t); 1101 if (tv != null) { 1102 // For visible children, defer removing the task until after the animation 1103 tv.animateRemoval(new Runnable() { 1104 @Override 1105 public void run() { 1106 mStack.removeTask(t); 1107 } 1108 }); 1109 } else { 1110 // Otherwise, remove the task from the stack immediately 1111 mStack.removeTask(t); 1112 } 1113 } 1114 } 1115 } 1116} 1117 1118/* Handles touch events */ 1119class TaskStackViewTouchHandler implements SwipeHelper.Callback { 1120 static int INACTIVE_POINTER_ID = -1; 1121 1122 TaskStackView mSv; 1123 VelocityTracker mVelocityTracker; 1124 1125 boolean mIsScrolling; 1126 1127 int mInitialMotionX, mInitialMotionY; 1128 int mLastMotionX, mLastMotionY; 1129 int mActivePointerId = INACTIVE_POINTER_ID; 1130 TaskView mActiveTaskView = null; 1131 1132 int mTotalScrollMotion; 1133 int mMinimumVelocity; 1134 int mMaximumVelocity; 1135 // The scroll touch slop is used to calculate when we start scrolling 1136 int mScrollTouchSlop; 1137 // The page touch slop is used to calculate when we start swiping 1138 float mPagingTouchSlop; 1139 1140 SwipeHelper mSwipeHelper; 1141 boolean mInterceptedBySwipeHelper; 1142 1143 public TaskStackViewTouchHandler(Context context, TaskStackView sv) { 1144 ViewConfiguration configuration = ViewConfiguration.get(context); 1145 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 1146 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 1147 mScrollTouchSlop = configuration.getScaledTouchSlop(); 1148 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 1149 mSv = sv; 1150 1151 1152 float densityScale = context.getResources().getDisplayMetrics().density; 1153 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop); 1154 mSwipeHelper.setMinAlpha(1f); 1155 } 1156 1157 /** Velocity tracker helpers */ 1158 void initOrResetVelocityTracker() { 1159 if (mVelocityTracker == null) { 1160 mVelocityTracker = VelocityTracker.obtain(); 1161 } else { 1162 mVelocityTracker.clear(); 1163 } 1164 } 1165 void initVelocityTrackerIfNotExists() { 1166 if (mVelocityTracker == null) { 1167 mVelocityTracker = VelocityTracker.obtain(); 1168 } 1169 } 1170 void recycleVelocityTracker() { 1171 if (mVelocityTracker != null) { 1172 mVelocityTracker.recycle(); 1173 mVelocityTracker = null; 1174 } 1175 } 1176 1177 /** Returns the view at the specified coordinates */ 1178 TaskView findViewAtPoint(int x, int y) { 1179 int childCount = mSv.getChildCount(); 1180 for (int i = childCount - 1; i >= 0; i--) { 1181 TaskView tv = (TaskView) mSv.getChildAt(i); 1182 if (tv.getVisibility() == View.VISIBLE) { 1183 if (mSv.isTransformedTouchPointInView(x, y, tv)) { 1184 return tv; 1185 } 1186 } 1187 } 1188 return null; 1189 } 1190 1191 /** Touch preprocessing for handling below */ 1192 public boolean onInterceptTouchEvent(MotionEvent ev) { 1193 Console.log(Constants.Log.UI.TouchEvents, 1194 "[TaskStackViewTouchHandler|interceptTouchEvent]", 1195 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 1196 1197 // Return early if we have no children 1198 boolean hasChildren = (mSv.getChildCount() > 0); 1199 if (!hasChildren) { 1200 return false; 1201 } 1202 1203 // Pass through to swipe helper if we are swiping 1204 mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); 1205 if (mInterceptedBySwipeHelper) { 1206 return true; 1207 } 1208 1209 boolean wasScrolling = !mSv.mScroller.isFinished() || 1210 (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning()); 1211 int action = ev.getAction(); 1212 switch (action & MotionEvent.ACTION_MASK) { 1213 case MotionEvent.ACTION_DOWN: { 1214 // Save the touch down info 1215 mInitialMotionX = mLastMotionX = (int) ev.getX(); 1216 mInitialMotionY = mLastMotionY = (int) ev.getY(); 1217 mActivePointerId = ev.getPointerId(0); 1218 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 1219 // Stop the current scroll if it is still flinging 1220 mSv.abortScroller(); 1221 mSv.abortBoundScrollAnimation(); 1222 // Initialize the velocity tracker 1223 initOrResetVelocityTracker(); 1224 mVelocityTracker.addMovement(ev); 1225 // Check if the scroller is finished yet 1226 mIsScrolling = !mSv.mScroller.isFinished(); 1227 break; 1228 } 1229 case MotionEvent.ACTION_MOVE: { 1230 if (mActivePointerId == INACTIVE_POINTER_ID) break; 1231 1232 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 1233 int y = (int) ev.getY(activePointerIndex); 1234 int x = (int) ev.getX(activePointerIndex); 1235 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 1236 // Save the touch move info 1237 mIsScrolling = true; 1238 // Initialize the velocity tracker if necessary 1239 initVelocityTrackerIfNotExists(); 1240 mVelocityTracker.addMovement(ev); 1241 // Disallow parents from intercepting touch events 1242 final ViewParent parent = mSv.getParent(); 1243 if (parent != null) { 1244 parent.requestDisallowInterceptTouchEvent(true); 1245 } 1246 // Enable HW layers 1247 mSv.addHwLayersRefCount("stackScroll"); 1248 } 1249 1250 mLastMotionX = x; 1251 mLastMotionY = y; 1252 break; 1253 } 1254 case MotionEvent.ACTION_CANCEL: 1255 case MotionEvent.ACTION_UP: { 1256 // Animate the scroll back if we've cancelled 1257 mSv.animateBoundScroll(); 1258 // Disable HW layers 1259 if (mIsScrolling) { 1260 mSv.decHwLayersRefCount("stackScroll"); 1261 } 1262 // Reset the drag state and the velocity tracker 1263 mIsScrolling = false; 1264 mActivePointerId = INACTIVE_POINTER_ID; 1265 mActiveTaskView = null; 1266 mTotalScrollMotion = 0; 1267 recycleVelocityTracker(); 1268 break; 1269 } 1270 } 1271 1272 return wasScrolling || mIsScrolling; 1273 } 1274 1275 /** Handles touch events once we have intercepted them */ 1276 public boolean onTouchEvent(MotionEvent ev) { 1277 Console.log(Constants.Log.UI.TouchEvents, 1278 "[TaskStackViewTouchHandler|touchEvent]", 1279 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 1280 1281 // Short circuit if we have no children 1282 boolean hasChildren = (mSv.getChildCount() > 0); 1283 if (!hasChildren) { 1284 return false; 1285 } 1286 1287 // Pass through to swipe helper if we are swiping 1288 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 1289 return true; 1290 } 1291 1292 // Update the velocity tracker 1293 initVelocityTrackerIfNotExists(); 1294 mVelocityTracker.addMovement(ev); 1295 1296 int action = ev.getAction(); 1297 switch (action & MotionEvent.ACTION_MASK) { 1298 case MotionEvent.ACTION_DOWN: { 1299 // Save the touch down info 1300 mInitialMotionX = mLastMotionX = (int) ev.getX(); 1301 mInitialMotionY = mLastMotionY = (int) ev.getY(); 1302 mActivePointerId = ev.getPointerId(0); 1303 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 1304 // Stop the current scroll if it is still flinging 1305 mSv.abortScroller(); 1306 mSv.abortBoundScrollAnimation(); 1307 // Initialize the velocity tracker 1308 initOrResetVelocityTracker(); 1309 mVelocityTracker.addMovement(ev); 1310 // Disallow parents from intercepting touch events 1311 final ViewParent parent = mSv.getParent(); 1312 if (parent != null) { 1313 parent.requestDisallowInterceptTouchEvent(true); 1314 } 1315 break; 1316 } 1317 case MotionEvent.ACTION_POINTER_DOWN: { 1318 final int index = ev.getActionIndex(); 1319 mActivePointerId = ev.getPointerId(index); 1320 mLastMotionX = (int) ev.getX(index); 1321 mLastMotionY = (int) ev.getY(index); 1322 break; 1323 } 1324 case MotionEvent.ACTION_MOVE: { 1325 if (mActivePointerId == INACTIVE_POINTER_ID) break; 1326 1327 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 1328 int x = (int) ev.getX(activePointerIndex); 1329 int y = (int) ev.getY(activePointerIndex); 1330 int yTotal = Math.abs(y - mInitialMotionY); 1331 int deltaY = mLastMotionY - y; 1332 if (!mIsScrolling) { 1333 if (yTotal > mScrollTouchSlop) { 1334 mIsScrolling = true; 1335 // Initialize the velocity tracker 1336 initOrResetVelocityTracker(); 1337 mVelocityTracker.addMovement(ev); 1338 // Disallow parents from intercepting touch events 1339 final ViewParent parent = mSv.getParent(); 1340 if (parent != null) { 1341 parent.requestDisallowInterceptTouchEvent(true); 1342 } 1343 // Enable HW layers 1344 mSv.addHwLayersRefCount("stackScroll"); 1345 } 1346 } 1347 if (mIsScrolling) { 1348 int curStackScroll = mSv.getStackScroll(); 1349 int overScrollAmount = mSv.getScrollAmountOutOfBounds(curStackScroll + deltaY); 1350 if (overScrollAmount != 0) { 1351 // Bound the overscroll to a fixed amount, and inversely scale the y-movement 1352 // relative to how close we are to the max overscroll 1353 float maxOverScroll = mSv.mTaskRect.height() / 3f; 1354 deltaY = Math.round(deltaY * (1f - (Math.min(maxOverScroll, overScrollAmount) 1355 / maxOverScroll))); 1356 } 1357 mSv.setStackScroll(curStackScroll + deltaY); 1358 if (mSv.isScrollOutOfBounds()) { 1359 mVelocityTracker.clear(); 1360 } 1361 } 1362 mLastMotionX = x; 1363 mLastMotionY = y; 1364 mTotalScrollMotion += Math.abs(deltaY); 1365 break; 1366 } 1367 case MotionEvent.ACTION_UP: { 1368 final VelocityTracker velocityTracker = mVelocityTracker; 1369 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1370 int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); 1371 1372 if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { 1373 // Enable HW layers on the stack 1374 mSv.addHwLayersRefCount("flingScroll"); 1375 // XXX: Make this animation a function of the velocity AND distance 1376 int overscrollRange = (int) (Math.min(1f, 1377 Math.abs((float) velocity / mMaximumVelocity)) * 1378 Constants.Values.TaskStackView.TaskStackOverscrollRange); 1379 1380 Console.log(Constants.Log.UI.TouchEvents, 1381 "[TaskStackViewTouchHandler|fling]", 1382 "scroll: " + mSv.getStackScroll() + " velocity: " + velocity + 1383 " maxVelocity: " + mMaximumVelocity + 1384 " overscrollRange: " + overscrollRange, 1385 Console.AnsiGreen); 1386 1387 // Fling scroll 1388 mSv.mScroller.fling(0, mSv.getStackScroll(), 1389 0, -velocity, 1390 0, 0, 1391 mSv.mMinScroll, mSv.mMaxScroll, 1392 0, overscrollRange); 1393 // Invalidate to kick off computeScroll 1394 mSv.invalidate(); 1395 } else if (mSv.isScrollOutOfBounds()) { 1396 // Animate the scroll back into bounds 1397 // XXX: Make this animation a function of the velocity OR distance 1398 mSv.animateBoundScroll(); 1399 } 1400 1401 if (mIsScrolling) { 1402 // Disable HW layers 1403 mSv.decHwLayersRefCount("stackScroll"); 1404 } 1405 mActivePointerId = INACTIVE_POINTER_ID; 1406 mIsScrolling = false; 1407 mTotalScrollMotion = 0; 1408 recycleVelocityTracker(); 1409 break; 1410 } 1411 case MotionEvent.ACTION_POINTER_UP: { 1412 int pointerIndex = ev.getActionIndex(); 1413 int pointerId = ev.getPointerId(pointerIndex); 1414 if (pointerId == mActivePointerId) { 1415 // Select a new active pointer id and reset the motion state 1416 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 1417 mActivePointerId = ev.getPointerId(newPointerIndex); 1418 mLastMotionX = (int) ev.getX(newPointerIndex); 1419 mLastMotionY = (int) ev.getY(newPointerIndex); 1420 mVelocityTracker.clear(); 1421 } 1422 break; 1423 } 1424 case MotionEvent.ACTION_CANCEL: { 1425 if (mIsScrolling) { 1426 // Disable HW layers 1427 mSv.decHwLayersRefCount("stackScroll"); 1428 } 1429 if (mSv.isScrollOutOfBounds()) { 1430 // Animate the scroll back into bounds 1431 // XXX: Make this animation a function of the velocity OR distance 1432 mSv.animateBoundScroll(); 1433 } 1434 mActivePointerId = INACTIVE_POINTER_ID; 1435 mIsScrolling = false; 1436 mTotalScrollMotion = 0; 1437 recycleVelocityTracker(); 1438 break; 1439 } 1440 } 1441 return true; 1442 } 1443 1444 /**** SwipeHelper Implementation ****/ 1445 1446 @Override 1447 public View getChildAtPosition(MotionEvent ev) { 1448 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 1449 } 1450 1451 @Override 1452 public boolean canChildBeDismissed(View v) { 1453 return true; 1454 } 1455 1456 @Override 1457 public void onBeginDrag(View v) { 1458 // Enable HW layers 1459 mSv.addHwLayersRefCount("swipeBegin"); 1460 // Disallow parents from intercepting touch events 1461 final ViewParent parent = mSv.getParent(); 1462 if (parent != null) { 1463 parent.requestDisallowInterceptTouchEvent(true); 1464 } 1465 } 1466 1467 @Override 1468 public void onChildDismissed(View v) { 1469 TaskView tv = (TaskView) v; 1470 mSv.onTaskDismissed(tv); 1471 1472 // Disable HW layers 1473 mSv.decHwLayersRefCount("swipeComplete"); 1474 } 1475 1476 @Override 1477 public void onSnapBackCompleted(View v) { 1478 // Do Nothing 1479 } 1480 1481 @Override 1482 public void onDragCancelled(View v) { 1483 // Disable HW layers 1484 mSv.decHwLayersRefCount("swipeCancelled"); 1485 } 1486} 1487