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