TaskView.java revision cb5570316d55c6fe2ff717fa6b94b14d13980263
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.ObjectAnimator; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.graphics.Color; 24import android.graphics.Outline; 25import android.graphics.Paint; 26import android.graphics.PorterDuff; 27import android.graphics.PorterDuffColorFilter; 28import android.graphics.Rect; 29import android.graphics.drawable.Drawable; 30import android.graphics.drawable.LayerDrawable; 31import android.util.AttributeSet; 32import android.view.View; 33import android.view.ViewOutlineProvider; 34import android.view.animation.AccelerateInterpolator; 35import android.widget.FrameLayout; 36import com.android.systemui.R; 37import com.android.systemui.recents.AlternateRecentsComponent; 38import com.android.systemui.recents.Constants; 39import com.android.systemui.recents.RecentsConfiguration; 40import com.android.systemui.recents.model.RecentsTaskLoader; 41import com.android.systemui.recents.model.Task; 42 43/* A task view */ 44public class TaskView extends FrameLayout implements Task.TaskCallbacks, 45 TaskViewFooter.TaskFooterViewCallbacks, View.OnClickListener, View.OnLongClickListener { 46 47 /** The TaskView callbacks */ 48 interface TaskViewCallbacks { 49 public void onTaskViewAppIconClicked(TaskView tv); 50 public void onTaskViewAppInfoClicked(TaskView tv); 51 public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask); 52 public void onTaskViewDismissed(TaskView tv); 53 public void onTaskViewClipStateChanged(TaskView tv); 54 public void onTaskViewFullScreenTransitionCompleted(); 55 public void onTaskViewFocusChanged(TaskView tv, boolean focused); 56 } 57 58 RecentsConfiguration mConfig; 59 60 float mTaskProgress; 61 ObjectAnimator mTaskProgressAnimator; 62 ObjectAnimator mDimAnimator; 63 float mMaxDimScale; 64 int mDim; 65 AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f); 66 PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.MULTIPLY); 67 68 Task mTask; 69 boolean mTaskDataLoaded; 70 boolean mIsFocused; 71 boolean mFocusAnimationsEnabled; 72 boolean mIsFullScreenView; 73 boolean mClipViewInStack; 74 AnimateableViewBounds mViewBounds; 75 Paint mLayerPaint = new Paint(); 76 77 TaskViewThumbnail mThumbnailView; 78 TaskViewHeader mHeaderView; 79 TaskViewFooter mFooterView; 80 View mActionButtonView; 81 TaskViewCallbacks mCb; 82 83 // Optimizations 84 ValueAnimator.AnimatorUpdateListener mUpdateDimListener = 85 new ValueAnimator.AnimatorUpdateListener() { 86 @Override 87 public void onAnimationUpdate(ValueAnimator animation) { 88 setTaskProgress((Float) animation.getAnimatedValue()); 89 } 90 }; 91 92 93 public TaskView(Context context) { 94 this(context, null); 95 } 96 97 public TaskView(Context context, AttributeSet attrs) { 98 this(context, attrs, 0); 99 } 100 101 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 102 this(context, attrs, defStyleAttr, 0); 103 } 104 105 public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 106 super(context, attrs, defStyleAttr, defStyleRes); 107 mConfig = RecentsConfiguration.getInstance(); 108 mMaxDimScale = mConfig.taskStackMaxDim / 255f; 109 mClipViewInStack = true; 110 mViewBounds = new AnimateableViewBounds(this, mConfig.taskViewRoundedCornerRadiusPx); 111 setTaskProgress(getTaskProgress()); 112 setDim(getDim()); 113 if (mConfig.fakeShadows) { 114 setBackground(new FakeShadowDrawable(context.getResources(), mConfig)); 115 } 116 setOutlineProvider(mViewBounds); 117 } 118 119 /** Set callback */ 120 void setCallbacks(TaskViewCallbacks cb) { 121 mCb = cb; 122 } 123 124 /** Gets the task */ 125 Task getTask() { 126 return mTask; 127 } 128 129 /** Returns the view bounds. */ 130 AnimateableViewBounds getViewBounds() { 131 return mViewBounds; 132 } 133 134 @Override 135 protected void onFinishInflate() { 136 // Bind the views 137 mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); 138 mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); 139 mActionButtonView = findViewById(R.id.lock_to_app_fab); 140 mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { 141 @Override 142 public void getOutline(View view, Outline outline) { 143 // Set the outline to match the FAB background 144 outline.setOval(0, 0, mActionButtonView.getWidth(), 145 mActionButtonView.getHeight()); 146 } 147 }); 148 if (mFooterView != null) { 149 mFooterView.setCallbacks(this); 150 } 151 } 152 153 @Override 154 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 155 int width = MeasureSpec.getSize(widthMeasureSpec); 156 int height = MeasureSpec.getSize(heightMeasureSpec); 157 158 int widthWithoutPadding = width - mPaddingLeft - mPaddingRight; 159 int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; 160 // Measure the bar view, thumbnail, and footer 161 mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 162 MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY)); 163 if (mFooterView != null) { 164 mFooterView.measure( 165 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 166 MeasureSpec.makeMeasureSpec(mConfig.taskViewLockToAppButtonHeight, 167 MeasureSpec.EXACTLY)); 168 } 169 mActionButtonView.measure( 170 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.AT_MOST), 171 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.AT_MOST)); 172 if (mIsFullScreenView) { 173 // Measure the thumbnail height to be the full dimensions 174 mThumbnailView.measure( 175 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 176 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY)); 177 } else { 178 // Measure the thumbnail to be square 179 mThumbnailView.measure( 180 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 181 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); 182 } 183 setMeasuredDimension(width, height); 184 invalidateOutline(); 185 } 186 187 /** Synchronizes this view's properties with the task's transform */ 188 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) { 189 // If we are a full screen view, then only update the Z to keep it in order 190 // XXX: Also update/animate the dim as well 191 if (mIsFullScreenView) { 192 if (!mConfig.fakeShadows && 193 toTransform.hasTranslationZChangedFrom(getTranslationZ())) { 194 setTranslationZ(toTransform.translationZ); 195 } 196 return; 197 } 198 199 // Apply the transform 200 toTransform.applyToTaskView(this, duration, mConfig.fastOutSlowInInterpolator, false, 201 !mConfig.fakeShadows); 202 203 // Update the task progress 204 if (mTaskProgressAnimator != null) { 205 mTaskProgressAnimator.removeAllListeners(); 206 mTaskProgressAnimator.cancel(); 207 } 208 if (duration <= 0) { 209 setTaskProgress(toTransform.p); 210 } else { 211 mTaskProgressAnimator = ObjectAnimator.ofFloat(this, "taskProgress", toTransform.p); 212 mTaskProgressAnimator.setDuration(duration); 213 mTaskProgressAnimator.addUpdateListener(mUpdateDimListener); 214 mTaskProgressAnimator.start(); 215 } 216 } 217 218 /** Resets this view's properties */ 219 void resetViewProperties() { 220 setDim(0); 221 TaskViewTransform.reset(this); 222 } 223 224 /** 225 * When we are un/filtering, this method will set up the transform that we are animating to, 226 * in order to hide the task. 227 */ 228 void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) { 229 // Fade the view out and slide it away 230 toTransform.alpha = 0f; 231 toTransform.translationY += 200; 232 toTransform.translationZ = 0; 233 } 234 235 /** 236 * When we are un/filtering, this method will setup the transform that we are animating from, 237 * in order to show the task. 238 */ 239 void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) { 240 // Fade the view in 241 fromTransform.alpha = 0f; 242 } 243 244 /** Prepares this task view for the enter-recents animations. This is called earlier in the 245 * first layout because the actual animation into recents may take a long time. */ 246 void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, 247 boolean occludesLaunchTarget, int offscreenY) { 248 int initialDim = getDim(); 249 if (mConfig.launchedFromAppWithScreenshot) { 250 if (isTaskViewLaunchTargetTask) { 251 // Hide the footer during the transition in, and animate it out afterwards? 252 if (mFooterView != null) { 253 mFooterView.animateFooterVisibility(false, 0); 254 } 255 } else { 256 // Don't do anything for the side views when animating in 257 } 258 259 } else if (mConfig.launchedFromAppWithThumbnail) { 260 if (isTaskViewLaunchTargetTask) { 261 // Hide the action button if it exists 262 mActionButtonView.setAlpha(0f); 263 // Set the dim to 0 so we can animate it in 264 initialDim = 0; 265 } else if (occludesLaunchTarget) { 266 // Move the task view off screen (below) so we can animate it in 267 setTranslationY(offscreenY); 268 } 269 270 } else if (mConfig.launchedFromHome) { 271 // Move the task view off screen (below) so we can animate it in 272 setTranslationY(offscreenY); 273 setTranslationZ(0); 274 setScaleX(1f); 275 setScaleY(1f); 276 } 277 // Apply the current dim 278 setDim(initialDim); 279 // Prepare the thumbnail view alpha 280 mThumbnailView.prepareEnterRecentsAnimation(isTaskViewLaunchTargetTask); 281 } 282 283 /** Animates this task view as it enters recents */ 284 void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { 285 final TaskViewTransform transform = ctx.currentTaskTransform; 286 int startDelay = 0; 287 288 if (mConfig.launchedFromAppWithScreenshot) { 289 if (mTask.isLaunchTarget) { 290 Rect taskRect = ctx.currentTaskRect; 291 int duration = mConfig.taskViewEnterFromHomeDuration * 10; 292 int windowInsetTop = mConfig.systemInsets.top; // XXX: Should be for the window 293 float taskScale = ((float) taskRect.width() / getMeasuredWidth()) * transform.scale; 294 float scaledYOffset = ((1f - taskScale) * getMeasuredHeight()) / 2; 295 float scaledWindowInsetTop = (int) (taskScale * windowInsetTop); 296 float scaledTranslationY = taskRect.top + transform.translationY - 297 (scaledWindowInsetTop + scaledYOffset); 298 startDelay = mConfig.taskViewEnterFromHomeStaggerDelay; 299 300 // Animate the top clip 301 mViewBounds.animateClipTop(windowInsetTop, duration, 302 new ValueAnimator.AnimatorUpdateListener() { 303 @Override 304 public void onAnimationUpdate(ValueAnimator animation) { 305 int y = (Integer) animation.getAnimatedValue(); 306 mHeaderView.setTranslationY(y); 307 } 308 }); 309 // Animate the bottom or right clip 310 int size = Math.round((taskRect.width() / taskScale)); 311 if (mConfig.hasHorizontalLayout()) { 312 mViewBounds.animateClipRight(getMeasuredWidth() - size, duration); 313 } else { 314 mViewBounds.animateClipBottom(getMeasuredHeight() - (windowInsetTop + size), duration); 315 } 316 // Animate the task bar of the first task view 317 animate() 318 .scaleX(taskScale) 319 .scaleY(taskScale) 320 .translationY(scaledTranslationY) 321 .setDuration(duration) 322 .withEndAction(new Runnable() { 323 @Override 324 public void run() { 325 setIsFullScreen(false); 326 requestLayout(); 327 328 // Reset the clip 329 mViewBounds.setClipTop(0); 330 mViewBounds.setClipBottom(0); 331 mViewBounds.setClipRight(0); 332 // Reset the bar translation 333 mHeaderView.setTranslationY(0); 334 // Enable the thumbnail clip 335 mThumbnailView.enableTaskBarClip(mHeaderView); 336 // Animate the footer into view (if it is the front most task) 337 animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); 338 339 // Unbind the thumbnail from the screenshot 340 RecentsTaskLoader.getInstance().loadTaskData(mTask); 341 // Recycle the full screen screenshot 342 AlternateRecentsComponent.consumeLastScreenshot(); 343 344 mCb.onTaskViewFullScreenTransitionCompleted(); 345 346 // Decrement the post animation trigger 347 ctx.postAnimationTrigger.decrement(); 348 } 349 }) 350 .start(); 351 } else { 352 // Otherwise, just enable the thumbnail clip 353 mThumbnailView.enableTaskBarClip(mHeaderView); 354 355 // Animate the footer into view 356 animateFooterVisibility(true, 0); 357 } 358 ctx.postAnimationTrigger.increment(); 359 360 } else if (mConfig.launchedFromAppWithThumbnail) { 361 if (mTask.isLaunchTarget) { 362 // Enable the task bar clip 363 mThumbnailView.enableTaskBarClip(mHeaderView); 364 // Animate the dim/overlay 365 if (Constants.DebugFlags.App.EnableThumbnailAlphaOnFrontmost) { 366 // Animate the thumbnail alpha before the dim animation (to prevent updating the 367 // hardware layer) 368 mThumbnailView.startEnterRecentsAnimation(mConfig.taskBarEnterAnimDelay, 369 new Runnable() { 370 @Override 371 public void run() { 372 animateDimToProgress(0, mConfig.taskBarEnterAnimDuration, 373 ctx.postAnimationTrigger.decrementOnAnimationEnd()); 374 } 375 }); 376 } else { 377 // Immediately start the dim animation 378 animateDimToProgress(mConfig.taskBarEnterAnimDelay, 379 mConfig.taskBarEnterAnimDuration, 380 ctx.postAnimationTrigger.decrementOnAnimationEnd()); 381 } 382 ctx.postAnimationTrigger.increment(); 383 384 // Animate the footer into view 385 animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); 386 387 // Animate the action button in 388 mActionButtonView.animate().alpha(1f) 389 .setStartDelay(mConfig.taskBarEnterAnimDelay) 390 .setDuration(mConfig.taskBarEnterAnimDuration) 391 .setInterpolator(mConfig.fastOutLinearInInterpolator) 392 .withLayer() 393 .start(); 394 } else { 395 // Enable the task bar clip 396 mThumbnailView.enableTaskBarClip(mHeaderView); 397 // Animate the task up if it was occluding the launch target 398 if (ctx.currentTaskOccludesLaunchTarget) { 399 setTranslationY(transform.translationY + mConfig.taskViewAffiliateGroupEnterOffsetPx); 400 setAlpha(0f); 401 animate().alpha(1f) 402 .translationY(transform.translationY) 403 .setStartDelay(mConfig.taskBarEnterAnimDelay) 404 .setUpdateListener(null) 405 .setInterpolator(mConfig.fastOutSlowInInterpolator) 406 .setDuration(mConfig.taskViewEnterFromHomeDuration) 407 .withEndAction(new Runnable() { 408 @Override 409 public void run() { 410 mThumbnailView.enableTaskBarClip(mHeaderView); 411 // Decrement the post animation trigger 412 ctx.postAnimationTrigger.decrement(); 413 } 414 }) 415 .start(); 416 ctx.postAnimationTrigger.increment(); 417 } 418 } 419 startDelay = mConfig.taskBarEnterAnimDelay; 420 421 } else if (mConfig.launchedFromHome) { 422 // Animate the tasks up 423 int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); 424 int delay = mConfig.taskViewEnterFromHomeDelay + 425 frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay; 426 if (!mConfig.fakeShadows) { 427 animate().translationZ(transform.translationZ); 428 } 429 animate() 430 .scaleX(transform.scale) 431 .scaleY(transform.scale) 432 .translationY(transform.translationY) 433 .setStartDelay(delay) 434 .setUpdateListener(null) 435 .setInterpolator(mConfig.quintOutInterpolator) 436 .setDuration(mConfig.taskViewEnterFromHomeDuration) 437 .withEndAction(new Runnable() { 438 @Override 439 public void run() { 440 mThumbnailView.enableTaskBarClip(mHeaderView); 441 // Decrement the post animation trigger 442 ctx.postAnimationTrigger.decrement(); 443 } 444 }) 445 .start(); 446 ctx.postAnimationTrigger.increment(); 447 448 // Animate the footer into view 449 animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration); 450 startDelay = delay; 451 452 } else { 453 // Otherwise, just enable the thumbnail clip 454 mThumbnailView.enableTaskBarClip(mHeaderView); 455 456 // Animate the footer into view 457 animateFooterVisibility(true, 0); 458 } 459 460 // Enable the focus animations from this point onwards so that they aren't affected by the 461 // window transitions 462 postDelayed(new Runnable() { 463 @Override 464 public void run() { 465 enableFocusAnimations(); 466 } 467 }, (startDelay / 2)); 468 } 469 470 /** Animates this task view as it leaves recents by pressing home. */ 471 void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 472 animate() 473 .translationY(ctx.offscreenTranslationY) 474 .setStartDelay(0) 475 .setUpdateListener(null) 476 .setInterpolator(mConfig.fastOutLinearInInterpolator) 477 .setDuration(mConfig.taskViewExitToHomeDuration) 478 .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) 479 .start(); 480 ctx.postAnimationTrigger.increment(); 481 } 482 483 /** Animates this task view as it exits recents */ 484 void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, 485 boolean occludesLaunchTarget, boolean lockToTask) { 486 if (isLaunchingTask) { 487 // Disable the thumbnail clip 488 mThumbnailView.disableTaskBarClip(); 489 // Animate the thumbnail alpha back into full opacity for the window animation out 490 mThumbnailView.startLaunchTaskAnimation(postAnimRunnable); 491 492 // Animate the dim 493 if (mDim > 0) { 494 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); 495 anim.setDuration(mConfig.taskBarExitAnimDuration); 496 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 497 anim.start(); 498 } 499 500 // Animate the action button away 501 if (!lockToTask) { 502 float toScale = 0.9f; 503 mActionButtonView.animate() 504 .scaleX(toScale) 505 .scaleY(toScale); 506 } 507 mActionButtonView.animate() 508 .alpha(0f) 509 .setStartDelay(0) 510 .setDuration(mConfig.taskBarExitAnimDuration) 511 .setInterpolator(mConfig.fastOutLinearInInterpolator) 512 .withLayer() 513 .start(); 514 } else { 515 // Hide the dismiss button 516 mHeaderView.startLaunchTaskDismissAnimation(); 517 // If this is another view in the task grouping and is in front of the launch task, 518 // animate it away first 519 if (occludesLaunchTarget) { 520 animate().alpha(0f) 521 .translationY(getTranslationY() + mConfig.taskViewAffiliateGroupEnterOffsetPx) 522 .setStartDelay(0) 523 .setUpdateListener(null) 524 .setInterpolator(mConfig.fastOutLinearInInterpolator) 525 .setDuration(mConfig.taskBarExitAnimDuration) 526 .start(); 527 } 528 } 529 } 530 531 /** Animates the deletion of this task view */ 532 void startDeleteTaskAnimation(final Runnable r) { 533 // Disabling clipping with the stack while the view is animating away 534 setClipViewInStack(false); 535 536 animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) 537 .alpha(0f) 538 .setStartDelay(0) 539 .setUpdateListener(null) 540 .setInterpolator(mConfig.fastOutSlowInInterpolator) 541 .setDuration(mConfig.taskViewRemoveAnimDuration) 542 .withEndAction(new Runnable() { 543 @Override 544 public void run() { 545 // We just throw this into a runnable because starting a view property 546 // animation using layers can cause inconsisten results if we try and 547 // update the layers while the animation is running. In some cases, 548 // the runnabled passed in may start an animation which also uses layers 549 // so we defer all this by posting this. 550 r.run(); 551 552 // Re-enable clipping with the stack (we will reuse this view) 553 setClipViewInStack(true); 554 } 555 }) 556 .start(); 557 } 558 559 /** Animates this task view if the user does not interact with the stack after a certain time. */ 560 void startNoUserInteractionAnimation() { 561 mHeaderView.startNoUserInteractionAnimation(); 562 } 563 564 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ 565 void setNoUserInteractionState() { 566 mHeaderView.setNoUserInteractionState(); 567 } 568 569 /** Dismisses this task. */ 570 void dismissTask() { 571 // Animate out the view and call the callback 572 final TaskView tv = this; 573 startDeleteTaskAnimation(new Runnable() { 574 @Override 575 public void run() { 576 mCb.onTaskViewDismissed(tv); 577 } 578 }); 579 // Hide the footer 580 animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration); 581 } 582 583 /** Sets whether this task view is full screen or not. */ 584 void setIsFullScreen(boolean isFullscreen) { 585 mIsFullScreenView = isFullscreen; 586 mHeaderView.setIsFullscreen(isFullscreen); 587 if (isFullscreen) { 588 // If we are full screen, then disable the bottom outline clip for the footer 589 mViewBounds.setOutlineClipBottom(0); 590 } 591 } 592 593 /** Returns whether this task view should currently be drawn as a full screen view. */ 594 boolean isFullScreenView() { 595 return mIsFullScreenView; 596 } 597 598 /** 599 * Returns whether this view should be clipped, or any views below should clip against this 600 * view. 601 */ 602 boolean shouldClipViewInStack() { 603 return mClipViewInStack && !mIsFullScreenView && (getVisibility() == View.VISIBLE); 604 } 605 606 /** Sets whether this view should be clipped, or clipped against. */ 607 void setClipViewInStack(boolean clip) { 608 if (clip != mClipViewInStack) { 609 mClipViewInStack = clip; 610 mCb.onTaskViewClipStateChanged(this); 611 } 612 } 613 614 /** Gets the max footer height. */ 615 public int getMaxFooterHeight() { 616 if (mFooterView != null) { 617 return mFooterView.mMaxFooterHeight; 618 } else { 619 return 0; 620 } 621 } 622 623 /** Animates the footer into and out of view. */ 624 void animateFooterVisibility(boolean visible, int duration) { 625 // Hide the footer if we are a full screen view 626 if (mIsFullScreenView) return; 627 // Hide the footer if the current task can not be locked to 628 if (!mTask.lockToTaskEnabled || !mTask.lockToThisTask) return; 629 // Otherwise, animate the visibility 630 if (mFooterView != null) { 631 mFooterView.animateFooterVisibility(visible, duration); 632 } 633 } 634 635 /** Sets the current task progress. */ 636 public void setTaskProgress(float p) { 637 mTaskProgress = p; 638 mViewBounds.setAlpha(p); 639 updateDimFromTaskProgress(); 640 } 641 642 /** Returns the current task progress. */ 643 public float getTaskProgress() { 644 return mTaskProgress; 645 } 646 647 /** Returns the current dim. */ 648 public void setDim(int dim) { 649 mDim = dim; 650 if (mDimAnimator != null) { 651 mDimAnimator.removeAllListeners(); 652 mDimAnimator.cancel(); 653 } 654 if (mConfig.useHardwareLayers) { 655 // Defer setting hardware layers if we have not yet measured, or there is no dim to draw 656 if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { 657 if (mDimAnimator != null) { 658 mDimAnimator.removeAllListeners(); 659 mDimAnimator.cancel(); 660 } 661 662 int inverse = 255 - mDim; 663 mDimColorFilter.setColor(Color.argb(0xFF, inverse, inverse, inverse)); 664 mLayerPaint.setColorFilter(mDimColorFilter); 665 setLayerType(LAYER_TYPE_HARDWARE, mLayerPaint); 666 } 667 } else { 668 float dimAlpha = mDim / 255.0f; 669 if (mThumbnailView != null) { 670 mThumbnailView.setDimAlpha(dimAlpha); 671 } 672 if (mHeaderView != null) { 673 mHeaderView.setDimAlpha(dim); 674 } 675 } 676 } 677 678 /** Returns the current dim. */ 679 public int getDim() { 680 return mDim; 681 } 682 683 /** Animates the dim to the task progress. */ 684 void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) { 685 // Animate the dim into view as well 686 int toDim = getDimFromTaskProgress(); 687 if (toDim != getDim()) { 688 ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim); 689 anim.setStartDelay(delay); 690 anim.setDuration(duration); 691 if (postAnimRunnable != null) { 692 anim.addListener(postAnimRunnable); 693 } 694 anim.start(); 695 } 696 } 697 698 /** Compute the dim as a function of the scale of this view. */ 699 int getDimFromTaskProgress() { 700 float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress); 701 return (int) (dim * 255); 702 } 703 704 /** Update the dim as a function of the scale of this view. */ 705 void updateDimFromTaskProgress() { 706 setDim(getDimFromTaskProgress()); 707 } 708 709 /**** View focus state ****/ 710 711 /** 712 * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen 713 * if the view is not currently visible, or we are in touch state (where we still want to keep 714 * track of focus). 715 */ 716 public void setFocusedTask() { 717 mIsFocused = true; 718 if (mFocusAnimationsEnabled) { 719 // Focus the header bar 720 mHeaderView.onTaskViewFocusChanged(true); 721 } 722 // Update the thumbnail alpha with the focus 723 mThumbnailView.onFocusChanged(true); 724 // Call the callback 725 mCb.onTaskViewFocusChanged(this, true); 726 // Workaround, we don't always want it focusable in touch mode, but we want the first task 727 // to be focused after the enter-recents animation, which can be triggered from either touch 728 // or keyboard 729 setFocusableInTouchMode(true); 730 requestFocus(); 731 setFocusableInTouchMode(false); 732 invalidate(); 733 } 734 735 /** 736 * Unsets the focused task explicitly. 737 */ 738 void unsetFocusedTask() { 739 mIsFocused = false; 740 if (mFocusAnimationsEnabled) { 741 // Un-focus the header bar 742 mHeaderView.onTaskViewFocusChanged(false); 743 } 744 745 // Update the thumbnail alpha with the focus 746 mThumbnailView.onFocusChanged(false); 747 // Call the callback 748 mCb.onTaskViewFocusChanged(this, false); 749 invalidate(); 750 } 751 752 /** 753 * Updates the explicitly focused state when the view focus changes. 754 */ 755 @Override 756 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 757 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 758 if (!gainFocus) { 759 unsetFocusedTask(); 760 } 761 } 762 763 /** 764 * Returns whether we have explicitly been focused. 765 */ 766 public boolean isFocusedTask() { 767 return mIsFocused || isFocused(); 768 } 769 770 /** Enables all focus animations. */ 771 void enableFocusAnimations() { 772 boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled; 773 mFocusAnimationsEnabled = true; 774 if (mIsFocused && !wasFocusAnimationsEnabled) { 775 // Re-notify the header if we were focused and animations were not previously enabled 776 mHeaderView.onTaskViewFocusChanged(true); 777 } 778 } 779 780 /**** TaskCallbacks Implementation ****/ 781 782 /** Binds this task view to the task */ 783 public void onTaskBound(Task t) { 784 mTask = t; 785 mTask.setCallbacks(this); 786 if (getMeasuredWidth() == 0) { 787 // If we haven't yet measured, we should just set the footer height with any animation 788 animateFooterVisibility(t.lockToThisTask, 0); 789 } else { 790 animateFooterVisibility(t.lockToThisTask, mConfig.taskViewLockToAppLongAnimDuration); 791 } 792 // Hide the action button if lock to app is disabled 793 if (!t.lockToTaskEnabled && mActionButtonView.getVisibility() != View.GONE) { 794 mActionButtonView.setVisibility(View.GONE); 795 } 796 } 797 798 @Override 799 public void onTaskDataLoaded() { 800 if (mThumbnailView != null && mHeaderView != null) { 801 // Bind each of the views to the new task data 802 if (mIsFullScreenView) { 803 mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot()); 804 } else { 805 mThumbnailView.rebindToTask(mTask); 806 } 807 mHeaderView.rebindToTask(mTask); 808 // Rebind any listeners 809 mHeaderView.mApplicationIcon.setOnClickListener(this); 810 mHeaderView.mDismissButton.setOnClickListener(this); 811 if (mFooterView != null) { 812 mFooterView.setOnClickListener(this); 813 } 814 mActionButtonView.setOnClickListener(this); 815 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 816 if (mConfig.developerOptionsEnabled) { 817 mHeaderView.mApplicationIcon.setOnLongClickListener(this); 818 } 819 } 820 } 821 mTaskDataLoaded = true; 822 } 823 824 @Override 825 public void onTaskDataUnloaded() { 826 if (mThumbnailView != null && mHeaderView != null) { 827 // Unbind each of the views from the task data and remove the task callback 828 mTask.setCallbacks(null); 829 mThumbnailView.unbindFromTask(); 830 mHeaderView.unbindFromTask(); 831 // Unbind any listeners 832 mHeaderView.mApplicationIcon.setOnClickListener(null); 833 mHeaderView.mDismissButton.setOnClickListener(null); 834 if (mFooterView != null) { 835 mFooterView.setOnClickListener(null); 836 } 837 mActionButtonView.setOnClickListener(null); 838 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 839 mHeaderView.mApplicationIcon.setOnLongClickListener(null); 840 } 841 } 842 mTaskDataLoaded = false; 843 } 844 845 /** Enables/disables handling touch on this task view. */ 846 void setTouchEnabled(boolean enabled) { 847 setOnClickListener(enabled ? this : null); 848 } 849 850 /**** TaskViewFooter.TaskFooterViewCallbacks ****/ 851 852 @Override 853 public void onTaskFooterHeightChanged(int height, int maxHeight) { 854 if (mIsFullScreenView) { 855 // Disable the bottom outline clip when fullscreen 856 mViewBounds.setOutlineClipBottom(0); 857 } else { 858 // Update the bottom clip in our outline provider 859 mViewBounds.setOutlineClipBottom(maxHeight - height); 860 } 861 } 862 863 /**** View.OnClickListener Implementation ****/ 864 865 @Override 866 public void onClick(final View v) { 867 final TaskView tv = this; 868 final boolean delayViewClick = (v != this) && (v != mActionButtonView); 869 if (delayViewClick) { 870 // We purposely post the handler delayed to allow for the touch feedback to draw 871 postDelayed(new Runnable() { 872 @Override 873 public void run() { 874 if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { 875 mCb.onTaskViewAppIconClicked(tv); 876 } else if (v == mHeaderView.mDismissButton) { 877 dismissTask(); 878 } 879 } 880 }, 125); 881 } else { 882 if (v == mActionButtonView) { 883 // Reset the translation of the action button before we animate it out 884 mActionButtonView.setTranslationZ(0f); 885 } 886 mCb.onTaskViewClicked(tv, tv.getTask(), 887 (v == mFooterView || v == mActionButtonView)); 888 } 889 } 890 891 /**** View.OnLongClickListener Implementation ****/ 892 893 @Override 894 public boolean onLongClick(View v) { 895 if (v == mHeaderView.mApplicationIcon) { 896 mCb.onTaskViewAppInfoClicked(this); 897 return true; 898 } 899 return false; 900 } 901} 902