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