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