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