TaskStackView.java revision 303e1ff1fec8b240b587bb18b981247a99833aa8
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.app.ActivityManager; 25import android.content.Context; 26import android.graphics.Canvas; 27import android.graphics.Rect; 28import android.graphics.Region; 29import android.view.MotionEvent; 30import android.view.VelocityTracker; 31import android.view.View; 32import android.view.ViewConfiguration; 33import android.view.ViewParent; 34import android.widget.FrameLayout; 35import android.widget.OverScroller; 36import android.widget.Toast; 37import com.android.systemui.recents.Console; 38import com.android.systemui.recents.Constants; 39import com.android.systemui.recents.RecentsConfiguration; 40import com.android.systemui.recents.RecentsTaskLoader; 41import com.android.systemui.recents.Utilities; 42import com.android.systemui.recents.model.Task; 43import com.android.systemui.recents.model.TaskStack; 44import com.android.systemui.recents.model.TaskStackCallbacks; 45 46import java.util.ArrayList; 47 48/** The TaskView callbacks */ 49interface TaskStackViewCallbacks { 50 public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t); 51} 52 53/* The visual representation of a task stack view */ 54public class TaskStackView extends FrameLayout implements TaskStackCallbacks, TaskViewCallbacks, 55 ViewPoolConsumer<TaskView, Task>, View.OnClickListener { 56 TaskStack mStack; 57 TaskStackViewTouchHandler mTouchHandler; 58 TaskStackViewCallbacks mCb; 59 ViewPool<TaskView, Task> mViewPool; 60 61 // The various rects that define the stack view 62 Rect mRect = new Rect(); 63 Rect mStackRect = new Rect(); 64 Rect mStackRectSansPeek = new Rect(); 65 Rect mTaskRect = new Rect(); 66 67 // The virtual stack scroll that we use for the card layout 68 int mStackScroll; 69 int mMinScroll; 70 int mMaxScroll; 71 OverScroller mScroller; 72 ObjectAnimator mScrollAnimator; 73 74 // Optimizations 75 int mHwLayersRefCount; 76 int mStackViewsAnimationDuration; 77 boolean mStackViewsDirty = true; 78 boolean mAwaitingFirstLayout = true; 79 80 public TaskStackView(Context context, TaskStack stack) { 81 super(context); 82 mStack = stack; 83 mStack.setCallbacks(this); 84 mScroller = new OverScroller(context); 85 mTouchHandler = new TaskStackViewTouchHandler(context, this); 86 mViewPool = new ViewPool<TaskView, Task>(context, this); 87 } 88 89 /** Sets the callbacks */ 90 void setCallbacks(TaskStackViewCallbacks cb) { 91 mCb = cb; 92 } 93 94 /** Requests that the views be synchronized with the model */ 95 void requestSynchronizeStackViewsWithModel() { 96 requestSynchronizeStackViewsWithModel(0); 97 } 98 void requestSynchronizeStackViewsWithModel(int duration) { 99 Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, 100 "[TaskStackView|requestSynchronize]", "", Console.AnsiYellow); 101 if (!mStackViewsDirty) { 102 invalidate(); 103 } 104 if (mAwaitingFirstLayout) { 105 // Skip the animation if we are awaiting first layout 106 mStackViewsAnimationDuration = 0; 107 } else { 108 mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); 109 } 110 mStackViewsDirty = true; 111 } 112 113 // XXX: Optimization: Use a mapping of Task -> View 114 private TaskView getChildViewForTask(Task t) { 115 int childCount = getChildCount(); 116 for (int i = 0; i < childCount; i++) { 117 TaskView tv = (TaskView) getChildAt(i); 118 if (tv.getTask() == t) { 119 return tv; 120 } 121 } 122 return null; 123 } 124 125 /** Update/get the transform */ 126 public TaskViewTransform getStackTransform(int indexInStack) { 127 TaskViewTransform transform = new TaskViewTransform(); 128 129 // Map the items to an continuous position relative to the current scroll 130 int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards; 131 float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); 132 float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); 133 float t = ((indexInStack * overlapHeight) - getStackScroll()) / overlapHeight; 134 float boundedT = Math.max(t, -(numPeekCards + 1)); 135 136 // Set the scale relative to its position 137 float minScale = Constants.Values.TaskStackView.StackPeekMinScale; 138 float scaleRange = 1f - minScale; 139 float scaleInc = scaleRange / numPeekCards; 140 float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc))); 141 float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2; 142 transform.scale = scale; 143 144 // Set the translation 145 if (boundedT < 0f) { 146 transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) / 147 numPeekCards) * peekHeight - scaleYOffset); 148 } else { 149 transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset); 150 } 151 152 // Update the rect and visibility 153 transform.rect.set(mTaskRect); 154 if (t < -(numPeekCards + 1)) { 155 transform.visible = false; 156 } else { 157 transform.rect.offset(0, transform.translationY); 158 Utilities.scaleRectAboutCenter(transform.rect, scale); 159 transform.visible = Rect.intersects(mRect, transform.rect); 160 } 161 transform.t = t; 162 return transform; 163 } 164 165 /** Synchronizes the views with the model */ 166 void synchronizeStackViewsWithModel() { 167 Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, 168 "[TaskStackView|synchronizeViewsWithModel]", 169 "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow); 170 if (mStackViewsDirty) { 171 172 // XXX: Optimization: Use binary search to find the visible range 173 // XXX: Optimize to not call getStackTransform() so many times 174 // XXX: Consider using TaskViewTransform pool to prevent allocations 175 // XXX: Iterate children views, update transforms and remove all that are not visible 176 // For all remaining tasks, update transforms and if visible add the view 177 178 // Update the visible state of all the tasks 179 ArrayList<Task> tasks = mStack.getTasks(); 180 int taskCount = tasks.size(); 181 for (int i = 0; i < taskCount; i++) { 182 Task task = tasks.get(i); 183 TaskViewTransform transform = getStackTransform(i); 184 TaskView tv = getChildViewForTask(task); 185 186 if (transform.visible) { 187 if (tv == null) { 188 tv = mViewPool.pickUpViewFromPool(task, task); 189 // When we are picking up a new view from the view pool, prepare it for any 190 // following animation by putting it in a reasonable place 191 if (mStackViewsAnimationDuration > 0 && i != 0) { 192 // XXX: We have to animate when filtering, etc. Maybe we should have a 193 // runnable that ensures that tasks are animated in a special way 194 // when they are entering the scene? 195 int fromIndex = (transform.t < 0) ? (i - 1) : (i + 1); 196 tv.updateViewPropertiesFromTask(null, getStackTransform(fromIndex), 0); 197 } 198 } 199 } else { 200 if (tv != null) { 201 mViewPool.returnViewToPool(tv); 202 } 203 } 204 } 205 206 // Update all the current view children 207 // NOTE: We have to iterate in reverse where because we are removing views directly 208 int childCount = getChildCount(); 209 for (int i = childCount - 1; i >= 0; i--) { 210 TaskView tv = (TaskView) getChildAt(i); 211 Task task = tv.getTask(); 212 TaskViewTransform transform = getStackTransform(mStack.indexOfTask(task)); 213 if (!transform.visible) { 214 mViewPool.returnViewToPool(tv); 215 } else { 216 tv.updateViewPropertiesFromTask(null, transform, mStackViewsAnimationDuration); 217 } 218 } 219 220 Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, 221 " [TaskStackView|viewChildren]", "" + getChildCount()); 222 223 mStackViewsAnimationDuration = 0; 224 mStackViewsDirty = false; 225 } 226 } 227 228 /** Sets the current stack scroll */ 229 public void setStackScroll(int value) { 230 mStackScroll = value; 231 requestSynchronizeStackViewsWithModel(); 232 } 233 234 /** Gets the current stack scroll */ 235 public int getStackScroll() { 236 return mStackScroll; 237 } 238 239 /** Animates the stack scroll into bounds */ 240 ObjectAnimator animateBoundScroll(int duration) { 241 int curScroll = getStackScroll(); 242 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); 243 if (newScroll != curScroll) { 244 // Enable hw layers on the stack 245 addHwLayersRefCount(); 246 247 // Abort any current animations 248 mScroller.abortAnimation(); 249 if (mScrollAnimator != null) { 250 mScrollAnimator.cancel(); 251 mScrollAnimator.removeAllListeners(); 252 } 253 254 // Start a new scroll animation 255 mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll); 256 mScrollAnimator.setDuration(duration); 257 mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 258 @Override 259 public void onAnimationUpdate(ValueAnimator animation) { 260 setStackScroll((Integer) animation.getAnimatedValue()); 261 } 262 }); 263 mScrollAnimator.addListener(new AnimatorListenerAdapter() { 264 @Override 265 public void onAnimationEnd(Animator animation) { 266 // Disable hw layers on the stack 267 decHwLayersRefCount(); 268 } 269 }); 270 mScrollAnimator.start(); 271 } 272 return mScrollAnimator; 273 } 274 275 /** Aborts any current stack scrolls */ 276 void abortBoundScrollAnimation() { 277 if (mScrollAnimator != null) { 278 mScrollAnimator.cancel(); 279 } 280 } 281 282 /** Bounds the current scroll if necessary */ 283 public boolean boundScroll() { 284 int curScroll = getStackScroll(); 285 int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); 286 if (newScroll != curScroll) { 287 setStackScroll(newScroll); 288 return true; 289 } 290 return false; 291 } 292 293 /** Returns whether the current scroll is out of bounds */ 294 boolean isScrollOutOfBounds() { 295 return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll); 296 } 297 298 /** Updates the min and max virtual scroll bounds */ 299 void updateMinMaxScroll(boolean boundScrollToNewMinMax) { 300 // Compute the min and max scroll values 301 int numTasks = Math.max(1, mStack.getTaskCount()); 302 int taskHeight = mTaskRect.height(); 303 int stackHeight = mStackRectSansPeek.height(); 304 int maxScrollHeight = taskHeight + (int) ((numTasks - 1) * 305 Constants.Values.TaskStackView.StackOverlapPct * taskHeight); 306 mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight; 307 mMaxScroll = maxScrollHeight - stackHeight; 308 309 // Debug logging 310 if (Constants.DebugFlags.UI.MeasureAndLayout) { 311 Console.log(" [TaskStack|minScroll] " + mMinScroll); 312 Console.log(" [TaskStack|maxScroll] " + mMaxScroll); 313 } 314 315 if (boundScrollToNewMinMax) { 316 boundScroll(); 317 } 318 } 319 320 /** Enables the hw layers and increments the hw layer requirement ref count */ 321 void addHwLayersRefCount() { 322 Console.log(Constants.DebugFlags.UI.HwLayers, 323 "[TaskStackView|addHwLayersRefCount] refCount: " + 324 mHwLayersRefCount + "->" + (mHwLayersRefCount + 1)); 325 if (mHwLayersRefCount == 0) { 326 // Enable hw layers on each of the children 327 int childCount = getChildCount(); 328 for (int i = 0; i < childCount; i++) { 329 TaskView tv = (TaskView) getChildAt(i); 330 tv.enableHwLayers(); 331 } 332 } 333 mHwLayersRefCount++; 334 } 335 336 /** Decrements the hw layer requirement ref count and disables the hw layers when we don't 337 need them anymore. */ 338 void decHwLayersRefCount() { 339 Console.log(Constants.DebugFlags.UI.HwLayers, 340 "[TaskStackView|decHwLayersRefCount] refCount: " + 341 mHwLayersRefCount + "->" + (mHwLayersRefCount - 1)); 342 mHwLayersRefCount--; 343 if (mHwLayersRefCount == 0) { 344 // Disable hw layers on each of the children 345 int childCount = getChildCount(); 346 for (int i = 0; i < childCount; i++) { 347 TaskView tv = (TaskView) getChildAt(i); 348 tv.disableHwLayers(); 349 } 350 } else if (mHwLayersRefCount < 0) { 351 throw new RuntimeException("Invalid hw layers ref count"); 352 } 353 } 354 355 @Override 356 public void computeScroll() { 357 if (mScroller.computeScrollOffset()) { 358 setStackScroll(mScroller.getCurrY()); 359 invalidate(); 360 361 // If we just finished scrolling, then disable the hw layers 362 if (mScroller.isFinished()) { 363 decHwLayersRefCount(); 364 } 365 } 366 } 367 368 @Override 369 public boolean onInterceptTouchEvent(MotionEvent ev) { 370 return mTouchHandler.onInterceptTouchEvent(ev); 371 } 372 373 @Override 374 public boolean onTouchEvent(MotionEvent ev) { 375 return mTouchHandler.onTouchEvent(ev); 376 } 377 378 @Override 379 public void dispatchDraw(Canvas canvas) { 380 Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "", 381 Console.AnsiPurple); 382 synchronizeStackViewsWithModel(); 383 super.dispatchDraw(canvas); 384 } 385 386 @Override 387 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 388 if (Constants.DebugFlags.App.EnableTaskStackClipping) { 389 TaskView tv = (TaskView) child; 390 TaskView nextTv = null; 391 int curIndex = indexOfChild(tv); 392 if (curIndex < (getChildCount() - 1)) { 393 // Clip against the next view (if we aren't animating its alpha) 394 nextTv = (TaskView) getChildAt(curIndex + 1); 395 if (nextTv.getAlpha() == 1f) { 396 Rect curRect = tv.getClippingRect(Utilities.tmpRect, false); 397 Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true); 398 RecentsConfiguration config = RecentsConfiguration.getInstance(); 399 // The hit rects are relative to the task view, which needs to be offset by the 400 // system bar height 401 curRect.offset(0, config.systemInsets.top); 402 nextRect.offset(0, config.systemInsets.top); 403 // Compute the clip region 404 Region clipRegion = new Region(); 405 clipRegion.op(curRect, Region.Op.UNION); 406 clipRegion.op(nextRect, Region.Op.DIFFERENCE); 407 // Clip the canvas 408 int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 409 canvas.clipRegion(clipRegion); 410 boolean invalidate = super.drawChild(canvas, child, drawingTime); 411 canvas.restoreToCount(saveCount); 412 return invalidate; 413 } 414 } 415 } 416 return super.drawChild(canvas, child, drawingTime); 417 } 418 419 /** Computes the stack and task rects */ 420 public void computeRects(int width, int height) { 421 // Note: We let the stack view be the full height because we want the cards to go under the 422 // navigation bar if possible. However, the stack rects which we use to calculate 423 // max scroll, etc. need to take the nav bar into account 424 425 // Compute the stack rects 426 RecentsConfiguration config = RecentsConfiguration.getInstance(); 427 mRect.set(0, 0, width, height); 428 mStackRect.set(mRect); 429 mStackRect.bottom -= config.systemInsets.bottom; 430 431 int smallestDimension = Math.min(width, height); 432 int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f); 433 mStackRect.inset(padding, padding); 434 mStackRectSansPeek.set(mStackRect); 435 mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); 436 437 // Compute the task rect 438 if (RecentsConfiguration.getInstance().layoutVerticalStack) { 439 int minHeight = (int) (mStackRect.height() - 440 (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height())); 441 int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height())); 442 int centerX = mStackRect.centerX(); 443 mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top, 444 centerX + size / 2, mStackRectSansPeek.top + size); 445 } else { 446 int size = Math.min(mStackRect.width(), mStackRect.height()); 447 int centerY = mStackRect.centerY(); 448 mTaskRect.set(mStackRectSansPeek.top, centerY - size / 2, 449 mStackRectSansPeek.top + size, centerY + size / 2); 450 } 451 452 // Update the scroll bounds 453 updateMinMaxScroll(false); 454 } 455 456 @Override 457 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 458 int width = MeasureSpec.getSize(widthMeasureSpec); 459 int height = MeasureSpec.getSize(heightMeasureSpec); 460 Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]", 461 "width: " + width + " height: " + height + 462 " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen); 463 464 // Compute our stack/task rects 465 computeRects(width, height); 466 467 // Debug logging 468 if (Constants.DebugFlags.UI.MeasureAndLayout) { 469 Console.log(" [TaskStack|fullRect] " + mRect); 470 Console.log(" [TaskStack|stackRect] " + mStackRect); 471 Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); 472 Console.log(" [TaskStack|taskRect] " + mTaskRect); 473 } 474 475 // If this is the first layout, then scroll to the front of the stack and synchronize the 476 // stack views immediately 477 if (mAwaitingFirstLayout) { 478 setStackScroll(mMaxScroll); 479 requestSynchronizeStackViewsWithModel(); 480 synchronizeStackViewsWithModel(); 481 482 // Animate the icon of the first task view 483 if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) { 484 TaskView tv = (TaskView) getChildAt(getChildCount() - 1); 485 if (tv != null) { 486 tv.animateOnEnterRecents(); 487 } 488 } 489 } 490 491 // Measure each of the children 492 int childCount = getChildCount(); 493 for (int i = 0; i < childCount; i++) { 494 TaskView t = (TaskView) getChildAt(i); 495 t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY), 496 MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY)); 497 } 498 499 setMeasuredDimension(width, height); 500 } 501 502 @Override 503 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 504 Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]", 505 "" + new Rect(left, top, right, bottom), Console.AnsiGreen); 506 507 // Debug logging 508 if (Constants.DebugFlags.UI.MeasureAndLayout) { 509 Console.log(" [TaskStack|fullRect] " + mRect); 510 Console.log(" [TaskStack|stackRect] " + mStackRect); 511 Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); 512 Console.log(" [TaskStack|taskRect] " + mTaskRect); 513 } 514 515 // Layout each of the children 516 int childCount = getChildCount(); 517 for (int i = 0; i < childCount; i++) { 518 TaskView t = (TaskView) getChildAt(i); 519 t.layout(mTaskRect.left, mStackRectSansPeek.top, 520 mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height()); 521 } 522 523 if (!mAwaitingFirstLayout) { 524 requestSynchronizeStackViewsWithModel(); 525 } else { 526 mAwaitingFirstLayout = false; 527 } 528 } 529 530 @Override 531 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 532 super.onScrollChanged(l, t, oldl, oldt); 533 requestSynchronizeStackViewsWithModel(); 534 } 535 536 public boolean isTransformedTouchPointInView(float x, float y, View child) { 537 return isTransformedTouchPointInView(x, y, child, null); 538 } 539 540 /**** TaskStackCallbacks Implementation ****/ 541 542 @Override 543 public void onStackTaskAdded(TaskStack stack, Task t) { 544 requestSynchronizeStackViewsWithModel(); 545 } 546 547 @Override 548 public void onStackTaskRemoved(TaskStack stack, Task t) { 549 // Remove the view associated with this task, we can't rely on updateTransforms 550 // to work here because the task is no longer in the list 551 int childCount = getChildCount(); 552 for (int i = childCount - 1; i >= 0; i--) { 553 TaskView tv = (TaskView) getChildAt(i); 554 if (tv.getTask() == t) { 555 mViewPool.returnViewToPool(tv); 556 break; 557 } 558 } 559 560 updateMinMaxScroll(true); 561 requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration); 562 } 563 564 @Override 565 public void onStackFiltered(TaskStack stack) { 566 requestSynchronizeStackViewsWithModel(); 567 } 568 569 @Override 570 public void onStackUnfiltered(TaskStack stack) { 571 requestSynchronizeStackViewsWithModel(); 572 } 573 574 /**** ViewPoolConsumer Implementation ****/ 575 576 @Override 577 public TaskView createView(Context context) { 578 Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]"); 579 return new TaskView(context); 580 } 581 582 @Override 583 public void prepareViewToEnterPool(TaskView tv) { 584 Task task = tv.getTask(); 585 tv.resetViewProperties(); 586 Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]", 587 tv.getTask() + " tv: " + tv); 588 589 // Report that this tasks's data is no longer being used 590 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 591 loader.unloadTaskData(task); 592 tv.unbindFromTask(); 593 594 // Detach the view from the hierarchy 595 detachViewFromParent(tv); 596 597 // Disable hw layers on this view 598 tv.disableHwLayers(); 599 } 600 601 @Override 602 public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) { 603 Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]", 604 "isNewView: " + isNewView); 605 606 // Setup and attach the view to the window 607 Task task = prepareData; 608 // We try and rebind the task (this MUST be done before the task filled) 609 tv.bindToTask(task, this); 610 // Request that this tasks's data be filled 611 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 612 loader.loadTaskData(task); 613 tv.syncToTask(); 614 615 // Find the index where this task should be placed in the children 616 int insertIndex = -1; 617 int childCount = getChildCount(); 618 for (int i = 0; i < childCount; i++) { 619 Task tvTask = ((TaskView) getChildAt(i)).getTask(); 620 if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) { 621 insertIndex = i; 622 break; 623 } 624 } 625 626 // Add/attach the view to the hierarchy 627 Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]", 628 "" + insertIndex); 629 if (isNewView) { 630 addView(tv, insertIndex); 631 tv.setOnClickListener(this); 632 } else { 633 attachViewToParent(tv, insertIndex, tv.getLayoutParams()); 634 } 635 636 // Enable hw layers on this view if hw layers are enabled on the stack 637 if (mHwLayersRefCount > 0) { 638 tv.enableHwLayers(); 639 } 640 } 641 642 @Override 643 public boolean hasPreferredData(TaskView tv, Task preferredData) { 644 return (tv.getTask() == preferredData); 645 } 646 647 /**** TaskViewCallbacks Implementation ****/ 648 649 @Override 650 public void onTaskIconClicked(TaskView tv) { 651 Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]", 652 tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(), 653 Console.AnsiCyan); 654 if (Constants.DebugFlags.App.EnableTaskFiltering) { 655 if (mStack.hasFilteredTasks()) { 656 mStack.unfilterTasks(); 657 } else { 658 mStack.filterTasks(tv.getTask()); 659 } 660 } else { 661 Toast.makeText(getContext(), "Task Filtering TBD", Toast.LENGTH_SHORT).show(); 662 } 663 } 664 665 /**** View.OnClickListener Implementation ****/ 666 667 @Override 668 public void onClick(View v) { 669 TaskView tv = (TaskView) v; 670 Task task = tv.getTask(); 671 Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]", 672 task + " cb: " + mCb); 673 674 if (mCb != null) { 675 mCb.onTaskLaunched(this, tv, mStack, task); 676 } 677 } 678} 679 680/* Handles touch events */ 681class TaskStackViewTouchHandler { 682 static int INACTIVE_POINTER_ID = -1; 683 684 TaskStackView mSv; 685 VelocityTracker mVelocityTracker; 686 687 boolean mIsScrolling; 688 boolean mIsSwiping; 689 690 int mInitialMotionX, mInitialMotionY; 691 int mLastMotionX, mLastMotionY; 692 int mActivePointerId = INACTIVE_POINTER_ID; 693 TaskView mActiveTaskView = null; 694 695 int mTotalScrollMotion; 696 int mMinimumVelocity; 697 int mMaximumVelocity; 698 // The scroll touch slop is used to calculate when we start scrolling 699 int mScrollTouchSlop; 700 // The swipe touch slop is used to calculate when we start swiping left/right, this takes 701 // precendence over the scroll touch slop in case the user makes a gesture that starts scrolling 702 // but is intended to be a swipe 703 int mSwipeTouchSlop; 704 // After a certain amount of scrolling, we should start ignoring checks for swiping 705 int mMaxScrollMotionToRejectSwipe; 706 707 public TaskStackViewTouchHandler(Context context, TaskStackView sv) { 708 ViewConfiguration configuration = ViewConfiguration.get(context); 709 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 710 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 711 mScrollTouchSlop = configuration.getScaledTouchSlop(); 712 mSwipeTouchSlop = 2 * mScrollTouchSlop; 713 mMaxScrollMotionToRejectSwipe = 4 * mScrollTouchSlop; 714 mSv = sv; 715 } 716 717 /** Velocity tracker helpers */ 718 void initOrResetVelocityTracker() { 719 if (mVelocityTracker == null) { 720 mVelocityTracker = VelocityTracker.obtain(); 721 } else { 722 mVelocityTracker.clear(); 723 } 724 } 725 void initVelocityTrackerIfNotExists() { 726 if (mVelocityTracker == null) { 727 mVelocityTracker = VelocityTracker.obtain(); 728 } 729 } 730 void recycleVelocityTracker() { 731 if (mVelocityTracker != null) { 732 mVelocityTracker.recycle(); 733 mVelocityTracker = null; 734 } 735 } 736 737 /** Returns the view at the specified coordinates */ 738 TaskView findViewAtPoint(int x, int y) { 739 int childCount = mSv.getChildCount(); 740 for (int i = childCount - 1; i >= 0; i--) { 741 TaskView tv = (TaskView) mSv.getChildAt(i); 742 if (tv.getVisibility() == View.VISIBLE) { 743 if (mSv.isTransformedTouchPointInView(x, y, tv)) { 744 return tv; 745 } 746 } 747 } 748 return null; 749 } 750 751 /** Touch preprocessing for handling below */ 752 public boolean onInterceptTouchEvent(MotionEvent ev) { 753 Console.log(Constants.DebugFlags.UI.TouchEvents, 754 "[TaskStackViewTouchHandler|interceptTouchEvent]", 755 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 756 757 boolean hasChildren = (mSv.getChildCount() > 0); 758 if (!hasChildren) { 759 return false; 760 } 761 762 boolean wasScrolling = !mSv.mScroller.isFinished() || 763 (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning()); 764 int action = ev.getAction(); 765 switch (action & MotionEvent.ACTION_MASK) { 766 case MotionEvent.ACTION_DOWN: { 767 // Save the touch down info 768 mInitialMotionX = mLastMotionX = (int) ev.getX(); 769 mInitialMotionY = mLastMotionY = (int) ev.getY(); 770 mActivePointerId = ev.getPointerId(0); 771 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 772 // Stop the current scroll if it is still flinging 773 mSv.mScroller.abortAnimation(); 774 mSv.abortBoundScrollAnimation(); 775 // Initialize the velocity tracker 776 initOrResetVelocityTracker(); 777 mVelocityTracker.addMovement(ev); 778 // Check if the scroller is finished yet 779 mIsScrolling = !mSv.mScroller.isFinished(); 780 mIsSwiping = false; 781 break; 782 } 783 case MotionEvent.ACTION_MOVE: { 784 if (mActivePointerId == INACTIVE_POINTER_ID) break; 785 786 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 787 int y = (int) ev.getY(activePointerIndex); 788 int x = (int) ev.getX(activePointerIndex); 789 if (mActiveTaskView != null && 790 mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && 791 Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && 792 Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { 793 // Start swiping and stop scrolling 794 mIsScrolling = false; 795 mIsSwiping = true; 796 System.out.println("SWIPING: " + mActiveTaskView); 797 // Initialize the velocity tracker if necessary 798 initOrResetVelocityTracker(); 799 mVelocityTracker.addMovement(ev); 800 // Disallow parents from intercepting touch events 801 final ViewParent parent = mSv.getParent(); 802 if (parent != null) { 803 parent.requestDisallowInterceptTouchEvent(true); 804 } 805 // Enable HW layers 806 mSv.addHwLayersRefCount(); 807 } else if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 808 // Save the touch move info 809 mIsScrolling = true; 810 // Initialize the velocity tracker if necessary 811 initVelocityTrackerIfNotExists(); 812 mVelocityTracker.addMovement(ev); 813 // Disallow parents from intercepting touch events 814 final ViewParent parent = mSv.getParent(); 815 if (parent != null) { 816 parent.requestDisallowInterceptTouchEvent(true); 817 } 818 // Enable HW layers 819 mSv.addHwLayersRefCount(); 820 } 821 822 mLastMotionX = x; 823 mLastMotionY = y; 824 break; 825 } 826 case MotionEvent.ACTION_CANCEL: 827 case MotionEvent.ACTION_UP: { 828 // Animate the scroll back if we've cancelled 829 mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); 830 // Reset the drag state and the velocity tracker 831 mIsScrolling = false; 832 mIsSwiping = false; 833 mActivePointerId = INACTIVE_POINTER_ID; 834 mActiveTaskView = null; 835 mTotalScrollMotion = 0; 836 recycleVelocityTracker(); 837 break; 838 } 839 } 840 841 return wasScrolling || mIsScrolling || mIsSwiping; 842 } 843 844 /** Handles touch events once we have intercepted them */ 845 public boolean onTouchEvent(MotionEvent ev) { 846 Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, 847 "[TaskStackViewTouchHandler|touchEvent]", 848 Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); 849 850 // Short circuit if we have no children 851 boolean hasChildren = (mSv.getChildCount() > 0); 852 if (!hasChildren) { 853 return false; 854 } 855 856 // Update the velocity tracker 857 initVelocityTrackerIfNotExists(); 858 mVelocityTracker.addMovement(ev); 859 860 int action = ev.getAction(); 861 switch (action & MotionEvent.ACTION_MASK) { 862 case MotionEvent.ACTION_DOWN: { 863 // Save the touch down info 864 mInitialMotionX = mLastMotionX = (int) ev.getX(); 865 mInitialMotionY = mLastMotionY = (int) ev.getY(); 866 mActivePointerId = ev.getPointerId(0); 867 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 868 // Stop the current scroll if it is still flinging 869 mSv.mScroller.abortAnimation(); 870 mSv.abortBoundScrollAnimation(); 871 // Initialize the velocity tracker 872 initOrResetVelocityTracker(); 873 mVelocityTracker.addMovement(ev); 874 // XXX: Set mIsScrolling or mIsSwiping? 875 // Disallow parents from intercepting touch events 876 final ViewParent parent = mSv.getParent(); 877 if (parent != null) { 878 parent.requestDisallowInterceptTouchEvent(true); 879 } 880 break; 881 } 882 case MotionEvent.ACTION_MOVE: { 883 if (mActivePointerId == INACTIVE_POINTER_ID) break; 884 885 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 886 int x = (int) ev.getX(activePointerIndex); 887 int y = (int) ev.getY(activePointerIndex); 888 int deltaY = mLastMotionY - y; 889 int deltaX = x - mLastMotionX; 890 if (!mIsSwiping) { 891 if (mActiveTaskView != null && 892 mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && 893 Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && 894 Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { 895 mIsScrolling = false; 896 mIsSwiping = true; 897 System.out.println("SWIPING: " + mActiveTaskView); 898 // Initialize the velocity tracker if necessary 899 initOrResetVelocityTracker(); 900 mVelocityTracker.addMovement(ev); 901 // Disallow parents from intercepting touch events 902 final ViewParent parent = mSv.getParent(); 903 if (parent != null) { 904 parent.requestDisallowInterceptTouchEvent(true); 905 } 906 // Enable HW layers 907 mSv.addHwLayersRefCount(); 908 } 909 } 910 if (!mIsSwiping && !mIsScrolling) { 911 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 912 mIsScrolling = true; 913 // Initialize the velocity tracker 914 initOrResetVelocityTracker(); 915 mVelocityTracker.addMovement(ev); 916 // Disallow parents from intercepting touch events 917 final ViewParent parent = mSv.getParent(); 918 if (parent != null) { 919 parent.requestDisallowInterceptTouchEvent(true); 920 } 921 // Enable HW layers 922 mSv.addHwLayersRefCount(); 923 } 924 } 925 if (mIsScrolling) { 926 mSv.setStackScroll(mSv.getStackScroll() + deltaY); 927 if (mSv.isScrollOutOfBounds()) { 928 mVelocityTracker.clear(); 929 } 930 } else if (mIsSwiping) { 931 mActiveTaskView.setTranslationX(mActiveTaskView.getTranslationX() + deltaX); 932 } 933 mLastMotionX = x; 934 mLastMotionY = y; 935 mTotalScrollMotion += Math.abs(deltaY); 936 break; 937 } 938 case MotionEvent.ACTION_UP: { 939 if (mIsScrolling || mIsSwiping) { 940 final TaskView activeTv = mActiveTaskView; 941 final VelocityTracker velocityTracker = mVelocityTracker; 942 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 943 944 if (mIsSwiping) { 945 int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); 946 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 947 // Fling to dismiss 948 int newScrollX = (int) (Math.signum(initialVelocity) * 949 activeTv.getMeasuredWidth()); 950 int duration = Math.min(Constants.Values.TaskStackView.Animation.SwipeDismissDuration, 951 (int) (Math.abs(newScrollX - activeTv.getScrollX()) * 952 1000f / Math.abs(initialVelocity))); 953 activeTv.animate() 954 .translationX(newScrollX) 955 .alpha(0f) 956 .setDuration(duration) 957 .setListener(new AnimatorListenerAdapter() { 958 @Override 959 public void onAnimationEnd(Animator animation) { 960 Task task = activeTv.getTask(); 961 Activity activity = (Activity) mSv.getContext(); 962 963 // We have to disable the listener to ensure that we 964 // don't hit this again 965 activeTv.animate().setListener(null); 966 967 // Remove the task from the view 968 mSv.mStack.removeTask(task); 969 970 // Remove any stored data from the loader 971 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 972 loader.deleteTaskData(task); 973 974 // Remove the task from activity manager 975 final ActivityManager am = (ActivityManager) 976 activity.getSystemService(Context.ACTIVITY_SERVICE); 977 if (am != null) { 978 am.removeTask(activeTv.getTask().id, 979 ActivityManager.REMOVE_TASK_KILL_PROCESS); 980 } 981 982 // If there are no remaining tasks, then just close the activity 983 if (mSv.mStack.getTaskCount() == 0) { 984 activity.finish(); 985 } 986 987 // Disable HW layers 988 mSv.decHwLayersRefCount(); 989 } 990 }) 991 .start(); 992 // Enable HW layers 993 mSv.addHwLayersRefCount(); 994 } else { 995 // Animate it back into place 996 // XXX: Make this animation a function of the velocity OR distance 997 int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; 998 activeTv.animate() 999 .translationX(0) 1000 .setDuration(duration) 1001 .setListener(new AnimatorListenerAdapter() { 1002 @Override 1003 public void onAnimationEnd(Animator animation) { 1004 // Disable HW layers 1005 mSv.decHwLayersRefCount(); 1006 } 1007 }) 1008 .start(); 1009 // Enable HW layers 1010 mSv.addHwLayersRefCount(); 1011 } 1012 } else { 1013 int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); 1014 if ((Math.abs(velocity) > mMinimumVelocity)) { 1015 Console.log(Constants.DebugFlags.UI.TouchEvents, 1016 "[TaskStackViewTouchHandler|fling]", 1017 "scroll: " + mSv.getStackScroll() + " velocity: " + velocity, 1018 Console.AnsiGreen); 1019 // Enable HW layers on the stack 1020 mSv.addHwLayersRefCount(); 1021 // Fling scroll 1022 mSv.mScroller.fling(0, mSv.getStackScroll(), 1023 0, -velocity, 1024 0, 0, 1025 mSv.mMinScroll, mSv.mMaxScroll, 1026 0, 0); 1027 // Invalidate to kick off computeScroll 1028 mSv.invalidate(); 1029 } else if (mSv.isScrollOutOfBounds()) { 1030 // Animate the scroll back into bounds 1031 // XXX: Make this animation a function of the velocity OR distance 1032 mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); 1033 } 1034 } 1035 } 1036 1037 mActivePointerId = INACTIVE_POINTER_ID; 1038 mIsScrolling = false; 1039 mIsSwiping = false; 1040 mTotalScrollMotion = 0; 1041 recycleVelocityTracker(); 1042 // Disable HW layers 1043 mSv.decHwLayersRefCount(); 1044 break; 1045 } 1046 case MotionEvent.ACTION_CANCEL: { 1047 if (mIsScrolling || mIsSwiping) { 1048 if (mIsSwiping) { 1049 // Animate it back into place 1050 // XXX: Make this animation a function of the velocity OR distance 1051 int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; 1052 mActiveTaskView.animate() 1053 .translationX(0) 1054 .setDuration(duration) 1055 .start(); 1056 } else { 1057 // Animate the scroll back into bounds 1058 // XXX: Make this animation a function of the velocity OR distance 1059 mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); 1060 } 1061 } 1062 1063 mActivePointerId = INACTIVE_POINTER_ID; 1064 mIsScrolling = false; 1065 mIsSwiping = false; 1066 mTotalScrollMotion = 0; 1067 recycleVelocityTracker(); 1068 // Disable HW layers 1069 mSv.decHwLayersRefCount(); 1070 break; 1071 } 1072 } 1073 return true; 1074 } 1075} 1076