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