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