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