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