TaskView.java revision 083baf99ff1228e96ede96aac88c8200c4fdc2b2
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.content.Context; 24import android.graphics.Canvas; 25import android.graphics.Outline; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.util.AttributeSet; 29import android.view.View; 30import android.view.ViewPropertyAnimator; 31import android.view.animation.AccelerateInterpolator; 32import android.widget.FrameLayout; 33import com.android.systemui.R; 34import com.android.systemui.recents.misc.Console; 35import com.android.systemui.recents.Constants; 36import com.android.systemui.recents.RecentsConfiguration; 37import com.android.systemui.recents.model.Task; 38 39 40/* A task view */ 41public class TaskView extends FrameLayout implements Task.TaskCallbacks, View.OnClickListener, 42 View.OnLongClickListener { 43 /** The TaskView callbacks */ 44 interface TaskViewCallbacks { 45 public void onTaskViewAppIconClicked(TaskView tv); 46 public void onTaskViewAppInfoClicked(TaskView tv); 47 public void onTaskViewDismissed(TaskView tv); 48 } 49 50 RecentsConfiguration mConfig; 51 52 int mDim; 53 int mMaxDim; 54 AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(); 55 56 Task mTask; 57 boolean mTaskDataLoaded; 58 boolean mIsFocused; 59 boolean mIsStub; 60 boolean mClipViewInStack; 61 Rect mTmpRect = new Rect(); 62 Paint mLayerPaint = new Paint(); 63 64 TaskThumbnailView mThumbnailView; 65 TaskBarView mBarView; 66 TaskViewCallbacks mCb; 67 68 // Optimizations 69 ValueAnimator.AnimatorUpdateListener mUpdateDimListener = 70 new ValueAnimator.AnimatorUpdateListener() { 71 @Override 72 public void onAnimationUpdate(ValueAnimator animation) { 73 updateDimOverlayFromScale(); 74 } 75 }; 76 Runnable mEnableThumbnailClip = new Runnable() { 77 @Override 78 public void run() { 79 mThumbnailView.updateTaskBarClip(mBarView); 80 } 81 }; 82 Runnable mDisableThumbnailClip = new Runnable() { 83 @Override 84 public void run() { 85 mThumbnailView.disableClipTaskBarView(); 86 } 87 }; 88 89 90 public TaskView(Context context) { 91 this(context, null); 92 } 93 94 public TaskView(Context context, AttributeSet attrs) { 95 this(context, attrs, 0); 96 } 97 98 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 99 this(context, attrs, defStyleAttr, 0); 100 } 101 102 public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 103 super(context, attrs, defStyleAttr, defStyleRes); 104 mConfig = RecentsConfiguration.getInstance(); 105 setWillNotDraw(false); 106 setClipToOutline(true); 107 setDim(getDim()); 108 } 109 110 @Override 111 protected void onFinishInflate() { 112 mMaxDim = mConfig.taskStackMaxDim; 113 114 // By default, all views are clipped to other views in their stack 115 mClipViewInStack = true; 116 117 // Bind the views 118 mBarView = (TaskBarView) findViewById(R.id.task_view_bar); 119 mThumbnailView = (TaskThumbnailView) findViewById(R.id.task_view_thumbnail); 120 121 if (mTaskDataLoaded) { 122 onTaskDataLoaded(); 123 } 124 } 125 126 @Override 127 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 128 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 129 130 // Update the outline 131 Outline o = new Outline(); 132 o.setRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), 133 mConfig.taskViewRoundedCornerRadiusPx); 134 setOutline(o); 135 } 136 137 /** Set callback */ 138 void setCallbacks(TaskViewCallbacks cb) { 139 mCb = cb; 140 } 141 142 /** Gets the task */ 143 Task getTask() { 144 return mTask; 145 } 146 147 /** Synchronizes this view's properties with the task's transform */ 148 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) { 149 if (Console.Enabled) { 150 Console.log(Constants.Log.UI.Draw, "[TaskView|updateViewPropertiesToTaskTransform]", 151 "duration: " + duration, Console.AnsiPurple); 152 } 153 154 // Update the bar view 155 mBarView.updateViewPropertiesToTaskTransform(toTransform, duration); 156 157 // Check to see if any properties have changed, and update the task view 158 if (duration > 0) { 159 ViewPropertyAnimator anim = animate(); 160 boolean useLayers = false; 161 162 // Animate to the final state 163 if (toTransform.hasTranslationYChangedFrom(getTranslationY())) { 164 anim.translationY(toTransform.translationY); 165 } 166 if (Constants.DebugFlags.App.EnableShadows && 167 toTransform.hasTranslationZChangedFrom(getTranslationZ())) { 168 anim.translationZ(toTransform.translationZ); 169 } 170 if (toTransform.hasScaleChangedFrom(getScaleX())) { 171 anim.scaleX(toTransform.scale) 172 .scaleY(toTransform.scale) 173 .setUpdateListener(mUpdateDimListener); 174 useLayers = true; 175 } 176 if (toTransform.hasAlphaChangedFrom(getAlpha())) { 177 // Use layers if we animate alpha 178 anim.alpha(toTransform.alpha); 179 useLayers = true; 180 } 181 if (useLayers) { 182 anim.withLayer(); 183 } 184 anim.setStartDelay(toTransform.startDelay) 185 .setDuration(duration) 186 .setInterpolator(mConfig.fastOutSlowInInterpolator) 187 .start(); 188 } else { 189 // Set the changed properties 190 if (toTransform.hasTranslationYChangedFrom(getTranslationY())) { 191 setTranslationY(toTransform.translationY); 192 } 193 if (Constants.DebugFlags.App.EnableShadows && 194 toTransform.hasTranslationZChangedFrom(getTranslationZ())) { 195 setTranslationZ(toTransform.translationZ); 196 } 197 if (toTransform.hasScaleChangedFrom(getScaleX())) { 198 setScaleX(toTransform.scale); 199 setScaleY(toTransform.scale); 200 updateDimOverlayFromScale(); 201 } 202 if (toTransform.hasAlphaChangedFrom(getAlpha())) { 203 setAlpha(toTransform.alpha); 204 } 205 } 206 } 207 208 /** Resets this view's properties */ 209 void resetViewProperties() { 210 setTranslationX(0f); 211 setTranslationY(0f); 212 if (Constants.DebugFlags.App.EnableShadows) { 213 setTranslationZ(0f); 214 } 215 setScaleX(1f); 216 setScaleY(1f); 217 setAlpha(1f); 218 setDim(0); 219 invalidate(); 220 } 221 222 /** 223 * When we are un/filtering, this method will set up the transform that we are animating to, 224 * in order to hide the task. 225 */ 226 void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) { 227 // Fade the view out and slide it away 228 toTransform.alpha = 0f; 229 toTransform.translationY += 200; 230 toTransform.translationZ = 0; 231 } 232 233 /** 234 * When we are un/filtering, this method will setup the transform that we are animating from, 235 * in order to show the task. 236 */ 237 void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) { 238 // Fade the view in 239 fromTransform.alpha = 0f; 240 } 241 242 /** Prepares this task view for the enter-recents animations. This is called earlier in the 243 * first layout because the actual animation into recents may take a long time. */ 244 public void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, int offsetY, 245 int offscreenY) { 246 if (mConfig.launchedFromAppWithScreenshot) { 247 if (isTaskViewLaunchTargetTask) { 248 // Hide the task view as we are going to animate the full screenshot into view 249 // and then replace it with this view once we are done 250 setVisibility(View.INVISIBLE); 251 // Also hide the front most task bar view so we can animate it in 252 mBarView.prepareEnterRecentsAnimation(); 253 } else { 254 // Top align the task views 255 setTranslationY(offsetY); 256 setScaleX(1f); 257 setScaleY(1f); 258 } 259 260 } else if (mConfig.launchedFromAppWithThumbnail) { 261 if (isTaskViewLaunchTargetTask) { 262 // Hide the front most task bar view so we can animate it in 263 mBarView.prepareEnterRecentsAnimation(); 264 // Set the dim to 0 so we can animate it in 265 setDim(0); 266 } 267 268 } else if (mConfig.launchedFromHome) { 269 // Move the task view off screen (below) so we can animate it in 270 setTranslationY(offscreenY); 271 if (Constants.DebugFlags.App.EnableShadows) { 272 setTranslationZ(0); 273 } 274 setScaleX(1f); 275 setScaleY(1f); 276 } 277 } 278 279 /** Animates this task view as it enters recents */ 280 public void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { 281 TaskViewTransform transform = ctx.currentTaskTransform; 282 283 if (mConfig.launchedFromAppWithScreenshot) { 284 if (ctx.isCurrentTaskLaunchTarget) { 285 // Animate the full screenshot down first, before swapping with this task view 286 ctx.fullScreenshotView.animateOnEnterRecents(ctx, new Runnable() { 287 @Override 288 public void run() { 289 // Animate the task bar of the first task view 290 mBarView.startEnterRecentsAnimation(0, mEnableThumbnailClip); 291 setVisibility(View.VISIBLE); 292 // Decrement the post animation trigger 293 ctx.postAnimationTrigger.decrement(); 294 } 295 }); 296 } else { 297 // Animate the tasks down behind the full screenshot 298 animate() 299 .scaleX(transform.scale) 300 .scaleY(transform.scale) 301 .translationY(transform.translationY) 302 .setStartDelay(0) 303 .setUpdateListener(null) 304 .setInterpolator(mConfig.linearOutSlowInInterpolator) 305 .setDuration(475) 306 .withLayer() 307 .withEndAction(new Runnable() { 308 @Override 309 public void run() { 310 mEnableThumbnailClip.run(); 311 // Decrement the post animation trigger 312 ctx.postAnimationTrigger.decrement(); 313 } 314 }) 315 .start(); 316 } 317 ctx.postAnimationTrigger.increment(); 318 319 } else if (mConfig.launchedFromAppWithThumbnail) { 320 if (ctx.isCurrentTaskLaunchTarget) { 321 // Animate the task bar of the first task view 322 mBarView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, mEnableThumbnailClip); 323 324 // Animate the dim into view as well 325 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimOverlayFromScale()); 326 anim.setStartDelay(mConfig.taskBarEnterAnimDelay); 327 anim.setDuration(mConfig.taskBarEnterAnimDuration); 328 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 329 anim.addListener(new AnimatorListenerAdapter() { 330 @Override 331 public void onAnimationEnd(Animator animation) { 332 // Decrement the post animation trigger 333 ctx.postAnimationTrigger.decrement(); 334 } 335 }); 336 anim.start(); 337 ctx.postAnimationTrigger.increment(); 338 } else { 339 mEnableThumbnailClip.run(); 340 } 341 342 } else if (mConfig.launchedFromHome) { 343 // Animate the tasks up 344 int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); 345 int delay = mConfig.taskBarEnterAnimDelay + 346 frontIndex * mConfig.taskViewEnterFromHomeDelay; 347 if (Constants.DebugFlags.App.EnableShadows) { 348 animate().translationZ(transform.translationZ); 349 } 350 animate() 351 .scaleX(transform.scale) 352 .scaleY(transform.scale) 353 .translationY(transform.translationY) 354 .setStartDelay(delay) 355 .setUpdateListener(null) 356 .setInterpolator(mConfig.quintOutInterpolator) 357 .setDuration(mConfig.taskViewEnterFromHomeDuration) 358 .withLayer() 359 .withEndAction(new Runnable() { 360 @Override 361 public void run() { 362 mEnableThumbnailClip.run(); 363 // Decrement the post animation trigger 364 ctx.postAnimationTrigger.decrement(); 365 } 366 }) 367 .start(); 368 ctx.postAnimationTrigger.increment(); 369 } else { 370 // Otherwise, just enable the thumbnail clip 371 mEnableThumbnailClip.run(); 372 } 373 } 374 375 /** Animates this task view as it leaves recents by pressing home. */ 376 public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 377 animate() 378 .translationY(ctx.offscreenTranslationY) 379 .setStartDelay(0) 380 .setUpdateListener(null) 381 .setInterpolator(mConfig.fastOutLinearInInterpolator) 382 .setDuration(mConfig.taskViewExitToHomeDuration) 383 .withLayer() 384 .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) 385 .start(); 386 ctx.postAnimationTrigger.increment(); 387 } 388 389 /** Animates this task view as it exits recents */ 390 public void startLaunchTaskAnimation(final Runnable r, boolean isLaunchingTask) { 391 if (isLaunchingTask) { 392 // Disable the thumbnail clip and animate the bar out 393 mBarView.startLaunchTaskAnimation(mDisableThumbnailClip, r); 394 395 // Animate the dim 396 if (mDim > 0) { 397 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); 398 anim.setDuration(mConfig.taskBarExitAnimDuration); 399 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 400 anim.start(); 401 } 402 } else { 403 // Hide the dismiss button 404 mBarView.startLaunchTaskDismissAnimation(); 405 } 406 } 407 408 /** Animates the deletion of this task view */ 409 public void startDeleteTaskAnimation(final Runnable r) { 410 // Disabling clipping with the stack while the view is animating away 411 setClipViewInStack(false); 412 413 animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) 414 .alpha(0f) 415 .setStartDelay(0) 416 .setUpdateListener(null) 417 .setInterpolator(mConfig.fastOutSlowInInterpolator) 418 .setDuration(mConfig.taskViewRemoveAnimDuration) 419 .withLayer() 420 .withEndAction(new Runnable() { 421 @Override 422 public void run() { 423 // We just throw this into a runnable because starting a view property 424 // animation using layers can cause inconsisten results if we try and 425 // update the layers while the animation is running. In some cases, 426 // the runnabled passed in may start an animation which also uses layers 427 // so we defer all this by posting this. 428 r.run(); 429 430 // Re-enable clipping with the stack (we will reuse this view) 431 setClipViewInStack(true); 432 } 433 }) 434 .start(); 435 } 436 437 /** Animates this task view if the user does not interact with the stack after a certain time. */ 438 public void startNoUserInteractionAnimation() { 439 mBarView.startNoUserInteractionAnimation(); 440 } 441 442 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ 443 public void setNoUserInteractionState() { 444 mBarView.setNoUserInteractionState(); 445 } 446 447 /** Returns the rect we want to clip (it may not be the full rect) */ 448 Rect getClippingRect(Rect outRect) { 449 getHitRect(outRect); 450 // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster 451 outRect.right = outRect.left + mThumbnailView.getRight(); 452 outRect.bottom = outRect.top + mThumbnailView.getBottom(); 453 return outRect; 454 } 455 456 /** Enable the hw layers on this task view */ 457 void enableHwLayers() { 458 mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, mLayerPaint); 459 mBarView.enableHwLayers(); 460 } 461 462 /** Disable the hw layers on this task view */ 463 void disableHwLayers() { 464 mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, mLayerPaint); 465 mBarView.disableHwLayers(); 466 } 467 468 /** Sets the stubbed state of this task view. */ 469 void setStubState(boolean isStub) { 470 if (!mIsStub && isStub) { 471 // This is now a stub task view, so clip to the bar height, hide the thumbnail 472 setClipBounds(new Rect(0, 0, getMeasuredWidth(), mBarView.getMeasuredHeight())); 473 mThumbnailView.setVisibility(View.INVISIBLE); 474 // Temporary 475 mBarView.mActivityDescription.setText("Stub"); 476 } else if (mIsStub && !isStub) { 477 setClipBounds(null); 478 mThumbnailView.setVisibility(View.VISIBLE); 479 } 480 mIsStub = isStub; 481 } 482 483 /** 484 * Returns whether this view should be clipped, or any views below should clip against this 485 * view. 486 */ 487 boolean shouldClipViewInStack() { 488 return mClipViewInStack && (getVisibility() == View.VISIBLE); 489 } 490 491 /** Sets whether this view should be clipped, or clipped against. */ 492 void setClipViewInStack(boolean clip) { 493 if (clip != mClipViewInStack) { 494 mClipViewInStack = clip; 495 if (getParent() instanceof View) { 496 getHitRect(mTmpRect); 497 ((View) getParent()).invalidate(mTmpRect); 498 } 499 } 500 } 501 502 /** Returns the current dim. */ 503 public void setDim(int dim) { 504 mDim = dim; 505 postInvalidateOnAnimation(); 506 } 507 508 /** Returns the current dim. */ 509 public int getDim() { 510 return mDim; 511 } 512 513 /** Compute the dim as a function of the scale of this view. */ 514 int getDimOverlayFromScale() { 515 float minScale = TaskStackViewLayoutAlgorithm.StackPeekMinScale; 516 float scaleRange = 1f - minScale; 517 float dim = (1f - getScaleX()) / scaleRange; 518 dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f)); 519 return Math.max(0, Math.min(mMaxDim, (int) (dim * 255))); 520 } 521 522 /** Update the dim as a function of the scale of this view. */ 523 void updateDimOverlayFromScale() { 524 setDim(getDimOverlayFromScale()); 525 } 526 527 @Override 528 public void draw(Canvas canvas) { 529 super.draw(canvas); 530 531 // Apply the dim if necessary 532 if (mDim > 0) { 533 canvas.drawColor(mDim << 24); 534 } 535 } 536 537 @Override 538 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 539 if (mIsStub && (child == mThumbnailView)) { 540 // Skip the thumbnail view if we are in stub mode 541 return false; 542 } 543 return super.drawChild(canvas, child, drawingTime); 544 } 545 546 /** 547 * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen 548 * if the view is not currently visible, or we are in touch state (where we still want to keep 549 * track of focus). 550 */ 551 public void setFocusedTask() { 552 mIsFocused = true; 553 // Workaround, we don't always want it focusable in touch mode, but we want the first task 554 // to be focused after the enter-recents animation, which can be triggered from either touch 555 // or keyboard 556 setFocusableInTouchMode(true); 557 requestFocus(); 558 setFocusableInTouchMode(false); 559 invalidate(); 560 } 561 562 /** 563 * Updates the explicitly focused state when the view focus changes. 564 */ 565 @Override 566 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 567 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 568 if (!gainFocus) { 569 mIsFocused = false; 570 invalidate(); 571 } 572 } 573 574 /** 575 * Returns whether we have explicitly been focused. 576 */ 577 public boolean isFocusedTask() { 578 return mIsFocused || isFocused(); 579 } 580 581 /**** TaskCallbacks Implementation ****/ 582 583 /** Binds this task view to the task */ 584 public void onTaskBound(Task t) { 585 mTask = t; 586 mTask.setCallbacks(this); 587 } 588 589 @Override 590 public void onTaskDataLoaded() { 591 if (mThumbnailView != null && mBarView != null) { 592 // Bind each of the views to the new task data 593 mThumbnailView.rebindToTask(mTask); 594 mBarView.rebindToTask(mTask); 595 // Rebind any listeners 596 if (Constants.DebugFlags.App.EnableTaskFiltering) { 597 mBarView.mApplicationIcon.setOnClickListener(this); 598 } 599 mBarView.mDismissButton.setOnClickListener(this); 600 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 601 if (mConfig.developerOptionsEnabled) { 602 mBarView.mApplicationIcon.setOnLongClickListener(this); 603 } 604 } 605 } 606 mTaskDataLoaded = true; 607 } 608 609 @Override 610 public void onTaskDataUnloaded() { 611 if (mThumbnailView != null && mBarView != null) { 612 // Unbind each of the views from the task data and remove the task callback 613 mTask.setCallbacks(null); 614 mThumbnailView.unbindFromTask(); 615 mBarView.unbindFromTask(); 616 // Unbind any listeners 617 if (Constants.DebugFlags.App.EnableTaskFiltering) { 618 mBarView.mApplicationIcon.setOnClickListener(null); 619 } 620 mBarView.mDismissButton.setOnClickListener(null); 621 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 622 mBarView.mApplicationIcon.setOnLongClickListener(null); 623 } 624 } 625 mTaskDataLoaded = false; 626 } 627 628 @Override 629 public void onClick(final View v) { 630 // We purposely post the handler delayed to allow for the touch feedback to draw 631 final TaskView tv = this; 632 postDelayed(new Runnable() { 633 @Override 634 public void run() { 635 if (v == mBarView.mApplicationIcon) { 636 mCb.onTaskViewAppIconClicked(tv); 637 } else if (v == mBarView.mDismissButton) { 638 // Animate out the view and call the callback 639 startDeleteTaskAnimation(new Runnable() { 640 @Override 641 public void run() { 642 mCb.onTaskViewDismissed(tv); 643 } 644 }); 645 } 646 } 647 }, 125); 648 } 649 650 @Override 651 public boolean onLongClick(View v) { 652 if (v == mBarView.mApplicationIcon) { 653 mCb.onTaskViewAppInfoClicked(this); 654 return true; 655 } 656 return false; 657 } 658} 659