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