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