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 17 package com.android.systemui.recents.views; 18 19 import android.animation.Animator; 20 import android.animation.ObjectAnimator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.*; 24 import android.graphics.drawable.Drawable; 25 import android.graphics.drawable.LayerDrawable; 26 import android.util.AttributeSet; 27 import android.view.View; 28 import android.view.ViewOutlineProvider; 29 import android.view.animation.AccelerateInterpolator; 30 import android.widget.FrameLayout; 31 import com.android.systemui.R; 32 import com.android.systemui.recents.AlternateRecentsComponent; 33 import com.android.systemui.recents.Constants; 34 import com.android.systemui.recents.RecentsConfiguration; 35 import com.android.systemui.recents.model.RecentsTaskLoader; 36 import com.android.systemui.recents.model.Task; 37 38 /* A task view */ 39 public class TaskView extends FrameLayout implements Task.TaskCallbacks, 40 TaskViewFooter.TaskFooterViewCallbacks, View.OnClickListener, View.OnLongClickListener { 41 42 /** The TaskView callbacks */ 43 interface TaskViewCallbacks { onTaskViewAppIconClicked(TaskView tv)44 public void onTaskViewAppIconClicked(TaskView tv); onTaskViewAppInfoClicked(TaskView tv)45 public void onTaskViewAppInfoClicked(TaskView tv); onTaskViewClicked(TaskView tv, Task task, boolean lockToTask)46 public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask); onTaskViewDismissed(TaskView tv)47 public void onTaskViewDismissed(TaskView tv); onTaskViewClipStateChanged(TaskView tv)48 public void onTaskViewClipStateChanged(TaskView tv); onTaskViewFullScreenTransitionCompleted()49 public void onTaskViewFullScreenTransitionCompleted(); onTaskViewFocusChanged(TaskView tv, boolean focused)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 TaskView(Context context)89 public TaskView(Context context) { 90 this(context, null); 91 } 92 TaskView(Context context, AttributeSet attrs)93 public TaskView(Context context, AttributeSet attrs) { 94 this(context, attrs, 0); 95 } 96 TaskView(Context context, AttributeSet attrs, int defStyleAttr)97 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 98 this(context, attrs, defStyleAttr, 0); 99 } 100 TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 */ setCallbacks(TaskViewCallbacks cb)116 void setCallbacks(TaskViewCallbacks cb) { 117 mCb = cb; 118 } 119 120 /** Gets the task */ getTask()121 Task getTask() { 122 return mTask; 123 } 124 125 /** Returns the view bounds. */ getViewBounds()126 AnimateableViewBounds getViewBounds() { 127 return mViewBounds; 128 } 129 130 @Override onFinishInflate()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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 */ updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration)190 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) { 191 updateViewPropertiesToTaskTransform(toTransform, duration, null); 192 } 193 updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration, ValueAnimator.AnimatorUpdateListener updateCallback)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 */ resetViewProperties()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 */ prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform)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 */ prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform)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. */ prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, boolean occludesLaunchTarget, int offscreenY)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 // Hide the action button if it exists 269 mActionButtonView.setAlpha(0f); 270 // Set the dim to 0 so we can animate it in 271 initialDim = 0; 272 } else if (occludesLaunchTarget) { 273 // Move the task view off screen (below) so we can animate it in 274 setTranslationY(offscreenY); 275 } 276 277 } else if (mConfig.launchedFromHome) { 278 // Move the task view off screen (below) so we can animate it in 279 setTranslationY(offscreenY); 280 setTranslationZ(0); 281 setScaleX(1f); 282 setScaleY(1f); 283 } 284 // Apply the current dim 285 setDim(initialDim); 286 // Prepare the thumbnail view alpha 287 mThumbnailView.prepareEnterRecentsAnimation(isTaskViewLaunchTargetTask); 288 } 289 290 /** Animates this task view as it enters recents */ startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx)291 void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { 292 final TaskViewTransform transform = ctx.currentTaskTransform; 293 int startDelay = 0; 294 295 if (mConfig.launchedFromAppWithScreenshot) { 296 if (mTask.isLaunchTarget) { 297 Rect taskRect = ctx.currentTaskRect; 298 int duration = mConfig.taskViewEnterFromHomeDuration * 10; 299 int windowInsetTop = mConfig.systemInsets.top; // XXX: Should be for the window 300 float taskScale = ((float) taskRect.width() / getMeasuredWidth()) * transform.scale; 301 float scaledYOffset = ((1f - taskScale) * getMeasuredHeight()) / 2; 302 float scaledWindowInsetTop = (int) (taskScale * windowInsetTop); 303 float scaledTranslationY = taskRect.top + transform.translationY - 304 (scaledWindowInsetTop + scaledYOffset); 305 startDelay = mConfig.taskViewEnterFromHomeStaggerDelay; 306 307 // Animate the top clip 308 mViewBounds.animateClipTop(windowInsetTop, duration, 309 new ValueAnimator.AnimatorUpdateListener() { 310 @Override 311 public void onAnimationUpdate(ValueAnimator animation) { 312 int y = (Integer) animation.getAnimatedValue(); 313 mHeaderView.setTranslationY(y); 314 } 315 }); 316 // Animate the bottom or right clip 317 int size = Math.round((taskRect.width() / taskScale)); 318 if (mConfig.hasHorizontalLayout()) { 319 mViewBounds.animateClipRight(getMeasuredWidth() - size, duration); 320 } else { 321 mViewBounds.animateClipBottom(getMeasuredHeight() - (windowInsetTop + size), duration); 322 } 323 // Animate the task bar of the first task view 324 animate() 325 .scaleX(taskScale) 326 .scaleY(taskScale) 327 .translationY(scaledTranslationY) 328 .setDuration(duration) 329 .withEndAction(new Runnable() { 330 @Override 331 public void run() { 332 setIsFullScreen(false); 333 requestLayout(); 334 335 // Reset the clip 336 mViewBounds.setClipTop(0); 337 mViewBounds.setClipBottom(0); 338 mViewBounds.setClipRight(0); 339 // Reset the bar translation 340 mHeaderView.setTranslationY(0); 341 // Animate the footer into view (if it is the front most task) 342 animateFooterVisibility(true, mConfig.taskBarEnterAnimDuration); 343 344 // Unbind the thumbnail from the screenshot 345 RecentsTaskLoader.getInstance().loadTaskData(mTask); 346 // Recycle the full screen screenshot 347 AlternateRecentsComponent.consumeLastScreenshot(); 348 349 mCb.onTaskViewFullScreenTransitionCompleted(); 350 351 // Decrement the post animation trigger 352 ctx.postAnimationTrigger.decrement(); 353 } 354 }) 355 .start(); 356 } else { 357 // Animate the footer into view 358 animateFooterVisibility(true, 0); 359 } 360 ctx.postAnimationTrigger.increment(); 361 362 } else if (mConfig.launchedFromAppWithThumbnail) { 363 if (mTask.isLaunchTarget) { 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 // Animate the task up if it was occluding the launch target 396 if (ctx.currentTaskOccludesLaunchTarget) { 397 setTranslationY(transform.translationY + mConfig.taskViewAffiliateGroupEnterOffsetPx); 398 setAlpha(0f); 399 animate().alpha(1f) 400 .translationY(transform.translationY) 401 .setStartDelay(mConfig.taskBarEnterAnimDelay) 402 .setUpdateListener(null) 403 .setInterpolator(mConfig.fastOutSlowInInterpolator) 404 .setDuration(mConfig.taskViewEnterFromHomeDuration) 405 .withEndAction(new Runnable() { 406 @Override 407 public void run() { 408 // Decrement the post animation trigger 409 ctx.postAnimationTrigger.decrement(); 410 } 411 }) 412 .start(); 413 ctx.postAnimationTrigger.increment(); 414 } 415 } 416 startDelay = mConfig.taskBarEnterAnimDelay; 417 418 } else if (mConfig.launchedFromHome) { 419 // Animate the tasks up 420 int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); 421 int delay = mConfig.taskViewEnterFromHomeDelay + 422 frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay; 423 424 setScaleX(transform.scale); 425 setScaleY(transform.scale); 426 if (!mConfig.fakeShadows) { 427 animate().translationZ(transform.translationZ); 428 } 429 animate() 430 .translationY(transform.translationY) 431 .setStartDelay(delay) 432 .setUpdateListener(ctx.updateListener) 433 .setInterpolator(mConfig.quintOutInterpolator) 434 .setDuration(mConfig.taskViewEnterFromHomeDuration + 435 frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay) 436 .withEndAction(new Runnable() { 437 @Override 438 public void run() { 439 // Decrement the post animation trigger 440 ctx.postAnimationTrigger.decrement(); 441 } 442 }) 443 .start(); 444 ctx.postAnimationTrigger.increment(); 445 446 // Animate the footer into view 447 animateFooterVisibility(true, mConfig.taskViewEnterFromHomeDuration); 448 startDelay = delay; 449 450 } else { 451 // Animate the footer into view 452 animateFooterVisibility(true, 0); 453 } 454 455 // Enable the focus animations from this point onwards so that they aren't affected by the 456 // window transitions 457 postDelayed(new Runnable() { 458 @Override 459 public void run() { 460 enableFocusAnimations(); 461 } 462 }, (startDelay / 2)); 463 } 464 465 /** Animates this task view as it leaves recents by pressing home. */ startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx)466 void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 467 animate() 468 .translationY(ctx.offscreenTranslationY) 469 .setStartDelay(0) 470 .setUpdateListener(null) 471 .setInterpolator(mConfig.fastOutLinearInInterpolator) 472 .setDuration(mConfig.taskViewExitToHomeDuration) 473 .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) 474 .start(); 475 ctx.postAnimationTrigger.increment(); 476 } 477 478 /** Animates this task view as it exits recents */ startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, boolean occludesLaunchTarget, boolean lockToTask)479 void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, 480 boolean occludesLaunchTarget, boolean lockToTask) { 481 if (isLaunchingTask) { 482 // Animate the thumbnail alpha back into full opacity for the window animation out 483 mThumbnailView.startLaunchTaskAnimation(postAnimRunnable); 484 485 // Animate the dim 486 if (mDim > 0) { 487 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); 488 anim.setDuration(mConfig.taskBarExitAnimDuration); 489 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 490 anim.start(); 491 } 492 493 // Animate the action button away 494 if (!lockToTask) { 495 float toScale = 0.9f; 496 mActionButtonView.animate() 497 .scaleX(toScale) 498 .scaleY(toScale); 499 } 500 mActionButtonView.animate() 501 .alpha(0f) 502 .setStartDelay(0) 503 .setDuration(mConfig.taskBarExitAnimDuration) 504 .setInterpolator(mConfig.fastOutLinearInInterpolator) 505 .withLayer() 506 .start(); 507 } else { 508 // Hide the dismiss button 509 mHeaderView.startLaunchTaskDismissAnimation(); 510 // If this is another view in the task grouping and is in front of the launch task, 511 // animate it away first 512 if (occludesLaunchTarget) { 513 animate().alpha(0f) 514 .translationY(getTranslationY() + mConfig.taskViewAffiliateGroupEnterOffsetPx) 515 .setStartDelay(0) 516 .setUpdateListener(null) 517 .setInterpolator(mConfig.fastOutLinearInInterpolator) 518 .setDuration(mConfig.taskBarExitAnimDuration) 519 .start(); 520 } 521 } 522 } 523 524 /** Animates the deletion of this task view */ startDeleteTaskAnimation(final Runnable r)525 void startDeleteTaskAnimation(final Runnable r) { 526 // Disabling clipping with the stack while the view is animating away 527 setClipViewInStack(false); 528 529 animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) 530 .alpha(0f) 531 .setStartDelay(0) 532 .setUpdateListener(null) 533 .setInterpolator(mConfig.fastOutSlowInInterpolator) 534 .setDuration(mConfig.taskViewRemoveAnimDuration) 535 .withEndAction(new Runnable() { 536 @Override 537 public void run() { 538 // We just throw this into a runnable because starting a view property 539 // animation using layers can cause inconsisten results if we try and 540 // update the layers while the animation is running. In some cases, 541 // the runnabled passed in may start an animation which also uses layers 542 // so we defer all this by posting this. 543 r.run(); 544 545 // Re-enable clipping with the stack (we will reuse this view) 546 setClipViewInStack(true); 547 } 548 }) 549 .start(); 550 } 551 552 /** Animates this task view if the user does not interact with the stack after a certain time. */ startNoUserInteractionAnimation()553 void startNoUserInteractionAnimation() { 554 mHeaderView.startNoUserInteractionAnimation(); 555 } 556 557 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ setNoUserInteractionState()558 void setNoUserInteractionState() { 559 mHeaderView.setNoUserInteractionState(); 560 } 561 562 /** Dismisses this task. */ dismissTask()563 void dismissTask() { 564 // Animate out the view and call the callback 565 final TaskView tv = this; 566 startDeleteTaskAnimation(new Runnable() { 567 @Override 568 public void run() { 569 mCb.onTaskViewDismissed(tv); 570 } 571 }); 572 // Hide the footer 573 animateFooterVisibility(false, mConfig.taskViewRemoveAnimDuration); 574 } 575 576 /** Sets whether this task view is full screen or not. */ setIsFullScreen(boolean isFullscreen)577 void setIsFullScreen(boolean isFullscreen) { 578 mIsFullScreenView = isFullscreen; 579 mHeaderView.setIsFullscreen(isFullscreen); 580 if (isFullscreen) { 581 // If we are full screen, then disable the bottom outline clip for the footer 582 mViewBounds.setOutlineClipBottom(0); 583 } 584 } 585 586 /** Returns whether this task view should currently be drawn as a full screen view. */ isFullScreenView()587 boolean isFullScreenView() { 588 return mIsFullScreenView; 589 } 590 591 /** 592 * Returns whether this view should be clipped, or any views below should clip against this 593 * view. 594 */ shouldClipViewInStack()595 boolean shouldClipViewInStack() { 596 return mClipViewInStack && !mIsFullScreenView && (getVisibility() == View.VISIBLE); 597 } 598 599 /** Sets whether this view should be clipped, or clipped against. */ setClipViewInStack(boolean clip)600 void setClipViewInStack(boolean clip) { 601 if (clip != mClipViewInStack) { 602 mClipViewInStack = clip; 603 mCb.onTaskViewClipStateChanged(this); 604 } 605 } 606 607 /** Gets the max footer height. */ getMaxFooterHeight()608 public int getMaxFooterHeight() { 609 if (mFooterView != null) { 610 return mFooterView.mMaxFooterHeight; 611 } else { 612 return 0; 613 } 614 } 615 616 /** Animates the footer into and out of view. */ animateFooterVisibility(boolean visible, int duration)617 void animateFooterVisibility(boolean visible, int duration) { 618 // Hide the footer if we are a full screen view 619 if (mIsFullScreenView) return; 620 // Hide the footer if the current task can not be locked to 621 if (!mTask.lockToTaskEnabled || !mTask.lockToThisTask) return; 622 // Otherwise, animate the visibility 623 if (mFooterView != null) { 624 mFooterView.animateFooterVisibility(visible, duration); 625 } 626 } 627 628 /** Sets the current task progress. */ setTaskProgress(float p)629 public void setTaskProgress(float p) { 630 mTaskProgress = p; 631 mViewBounds.setAlpha(p); 632 updateDimFromTaskProgress(); 633 } 634 635 /** Returns the current task progress. */ getTaskProgress()636 public float getTaskProgress() { 637 return mTaskProgress; 638 } 639 640 /** Returns the current dim. */ setDim(int dim)641 public void setDim(int dim) { 642 mDim = dim; 643 if (mDimAnimator != null) { 644 mDimAnimator.removeAllListeners(); 645 mDimAnimator.cancel(); 646 } 647 if (mConfig.useHardwareLayers) { 648 // Defer setting hardware layers if we have not yet measured, or there is no dim to draw 649 if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { 650 if (mDimAnimator != null) { 651 mDimAnimator.removeAllListeners(); 652 mDimAnimator.cancel(); 653 } 654 655 int inverse = 255 - mDim; 656 mDimColorFilter.setColor(Color.argb(0xFF, inverse, inverse, inverse)); 657 mLayerPaint.setColorFilter(mDimColorFilter); 658 mContent.setLayerType(LAYER_TYPE_HARDWARE, mLayerPaint); 659 } 660 } else { 661 float dimAlpha = mDim / 255.0f; 662 if (mThumbnailView != null) { 663 mThumbnailView.setDimAlpha(dimAlpha); 664 } 665 if (mHeaderView != null) { 666 mHeaderView.setDimAlpha(dim); 667 } 668 } 669 } 670 671 /** Returns the current dim. */ getDim()672 public int getDim() { 673 return mDim; 674 } 675 676 /** Animates the dim to the task progress. */ animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable)677 void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) { 678 // Animate the dim into view as well 679 int toDim = getDimFromTaskProgress(); 680 if (toDim != getDim()) { 681 ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim); 682 anim.setStartDelay(delay); 683 anim.setDuration(duration); 684 if (postAnimRunnable != null) { 685 anim.addListener(postAnimRunnable); 686 } 687 anim.start(); 688 } 689 } 690 691 /** Compute the dim as a function of the scale of this view. */ getDimFromTaskProgress()692 int getDimFromTaskProgress() { 693 float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress); 694 return (int) (dim * 255); 695 } 696 697 /** Update the dim as a function of the scale of this view. */ updateDimFromTaskProgress()698 void updateDimFromTaskProgress() { 699 setDim(getDimFromTaskProgress()); 700 } 701 702 /**** View focus state ****/ 703 704 /** 705 * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen 706 * if the view is not currently visible, or we are in touch state (where we still want to keep 707 * track of focus). 708 */ setFocusedTask()709 public void setFocusedTask() { 710 mIsFocused = true; 711 if (mFocusAnimationsEnabled) { 712 // Focus the header bar 713 mHeaderView.onTaskViewFocusChanged(true); 714 } 715 // Update the thumbnail alpha with the focus 716 mThumbnailView.onFocusChanged(true); 717 // Call the callback 718 mCb.onTaskViewFocusChanged(this, true); 719 // Workaround, we don't always want it focusable in touch mode, but we want the first task 720 // to be focused after the enter-recents animation, which can be triggered from either touch 721 // or keyboard 722 setFocusableInTouchMode(true); 723 requestFocus(); 724 setFocusableInTouchMode(false); 725 invalidate(); 726 } 727 728 /** 729 * Unsets the focused task explicitly. 730 */ unsetFocusedTask()731 void unsetFocusedTask() { 732 mIsFocused = false; 733 if (mFocusAnimationsEnabled) { 734 // Un-focus the header bar 735 mHeaderView.onTaskViewFocusChanged(false); 736 } 737 738 // Update the thumbnail alpha with the focus 739 mThumbnailView.onFocusChanged(false); 740 // Call the callback 741 mCb.onTaskViewFocusChanged(this, false); 742 invalidate(); 743 } 744 745 /** 746 * Updates the explicitly focused state when the view focus changes. 747 */ 748 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)749 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 750 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 751 if (!gainFocus) { 752 unsetFocusedTask(); 753 } 754 } 755 756 /** 757 * Returns whether we have explicitly been focused. 758 */ isFocusedTask()759 public boolean isFocusedTask() { 760 return mIsFocused || isFocused(); 761 } 762 763 /** Enables all focus animations. */ enableFocusAnimations()764 void enableFocusAnimations() { 765 boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled; 766 mFocusAnimationsEnabled = true; 767 if (mIsFocused && !wasFocusAnimationsEnabled) { 768 // Re-notify the header if we were focused and animations were not previously enabled 769 mHeaderView.onTaskViewFocusChanged(true); 770 } 771 } 772 773 /**** TaskCallbacks Implementation ****/ 774 775 /** Binds this task view to the task */ onTaskBound(Task t)776 public void onTaskBound(Task t) { 777 mTask = t; 778 mTask.setCallbacks(this); 779 if (getMeasuredWidth() == 0) { 780 // If we haven't yet measured, we should just set the footer height with any animation 781 animateFooterVisibility(t.lockToThisTask, 0); 782 } else { 783 animateFooterVisibility(t.lockToThisTask, mConfig.taskViewLockToAppLongAnimDuration); 784 } 785 // Hide the action button if lock to app is disabled 786 if (!t.lockToTaskEnabled && mActionButtonView.getVisibility() != View.GONE) { 787 mActionButtonView.setVisibility(View.GONE); 788 } 789 } 790 791 @Override onTaskDataLoaded()792 public void onTaskDataLoaded() { 793 if (mThumbnailView != null && mHeaderView != null) { 794 // Bind each of the views to the new task data 795 if (mIsFullScreenView) { 796 mThumbnailView.bindToScreenshot(AlternateRecentsComponent.getLastScreenshot()); 797 } else { 798 mThumbnailView.rebindToTask(mTask); 799 } 800 mHeaderView.rebindToTask(mTask); 801 // Rebind any listeners 802 mHeaderView.mApplicationIcon.setOnClickListener(this); 803 mHeaderView.mDismissButton.setOnClickListener(this); 804 if (mFooterView != null) { 805 mFooterView.setOnClickListener(this); 806 } 807 mActionButtonView.setOnClickListener(this); 808 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 809 if (mConfig.developerOptionsEnabled) { 810 mHeaderView.mApplicationIcon.setOnLongClickListener(this); 811 } 812 } 813 } 814 mTaskDataLoaded = true; 815 } 816 817 @Override onTaskDataUnloaded()818 public void onTaskDataUnloaded() { 819 if (mThumbnailView != null && mHeaderView != null) { 820 // Unbind each of the views from the task data and remove the task callback 821 mTask.setCallbacks(null); 822 mThumbnailView.unbindFromTask(); 823 mHeaderView.unbindFromTask(); 824 // Unbind any listeners 825 mHeaderView.mApplicationIcon.setOnClickListener(null); 826 mHeaderView.mDismissButton.setOnClickListener(null); 827 if (mFooterView != null) { 828 mFooterView.setOnClickListener(null); 829 } 830 mActionButtonView.setOnClickListener(null); 831 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 832 mHeaderView.mApplicationIcon.setOnLongClickListener(null); 833 } 834 } 835 mTaskDataLoaded = false; 836 } 837 838 /** Enables/disables handling touch on this task view. */ setTouchEnabled(boolean enabled)839 void setTouchEnabled(boolean enabled) { 840 setOnClickListener(enabled ? this : null); 841 } 842 843 /**** TaskViewFooter.TaskFooterViewCallbacks ****/ 844 845 @Override onTaskFooterHeightChanged(int height, int maxHeight)846 public void onTaskFooterHeightChanged(int height, int maxHeight) { 847 if (mIsFullScreenView) { 848 // Disable the bottom outline clip when fullscreen 849 mViewBounds.setOutlineClipBottom(0); 850 } else { 851 // Update the bottom clip in our outline provider 852 mViewBounds.setOutlineClipBottom(maxHeight - height); 853 } 854 } 855 856 /**** View.OnClickListener Implementation ****/ 857 858 @Override onClick(final View v)859 public void onClick(final View v) { 860 final TaskView tv = this; 861 final boolean delayViewClick = (v != this) && (v != mActionButtonView); 862 if (delayViewClick) { 863 // We purposely post the handler delayed to allow for the touch feedback to draw 864 postDelayed(new Runnable() { 865 @Override 866 public void run() { 867 if (Constants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { 868 mCb.onTaskViewAppIconClicked(tv); 869 } else if (v == mHeaderView.mDismissButton) { 870 dismissTask(); 871 } 872 } 873 }, 125); 874 } else { 875 if (v == mActionButtonView) { 876 // Reset the translation of the action button before we animate it out 877 mActionButtonView.setTranslationZ(0f); 878 } 879 mCb.onTaskViewClicked(tv, tv.getTask(), 880 (v == mFooterView || v == mActionButtonView)); 881 } 882 } 883 884 /**** View.OnLongClickListener Implementation ****/ 885 886 @Override onLongClick(View v)887 public boolean onLongClick(View v) { 888 if (v == mHeaderView.mApplicationIcon) { 889 mCb.onTaskViewAppInfoClicked(this); 890 return true; 891 } 892 return false; 893 } 894 } 895