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