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