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