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