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