TaskView.java revision b5ddfc375616bf7dbe9f4ff85ad124f02cc5990f
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 prepareAnimateEnterRecents(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.prepareAnimateEnterRecents(); 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.prepareAnimateEnterRecents(); 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 setScaleX(1f); 290 setScaleY(1f); 291 } 292 } 293 294 /** Animates this task view as it enters recents */ 295 public void animateOnEnterRecents(ViewAnimation.TaskViewEnterContext ctx) { 296 TaskViewTransform transform = ctx.transform; 297 298 if (mConfig.launchedFromAppWithScreenshot) { 299 if (ctx.isFrontMost) { 300 // Animate the full screenshot down first, before swapping with this task view 301 ctx.fullScreenshot.animateOnEnterRecents(ctx, new Runnable() { 302 @Override 303 public void run() { 304 // Animate the task bar of the first task view 305 mBarView.animateOnEnterRecents(0, mEnableThumbnailClip); 306 setVisibility(View.VISIBLE); 307 } 308 }); 309 } else { 310 // Animate the tasks down behind the full screenshot 311 animate() 312 .scaleX(transform.scale) 313 .scaleY(transform.scale) 314 .translationY(transform.translationY) 315 .setStartDelay(0) 316 .setUpdateListener(null) 317 .setInterpolator(mConfig.linearOutSlowInInterpolator) 318 .setDuration(475) 319 .withLayer() 320 .withEndAction(mEnableThumbnailClip) 321 .start(); 322 } 323 324 } else if (mConfig.launchedFromAppWithThumbnail) { 325 if (ctx.isFrontMost) { 326 // Animate the task bar of the first task view 327 mBarView.animateOnEnterRecents(mConfig.taskBarEnterAnimDelay, mEnableThumbnailClip); 328 329 // Animate the dim into view as well 330 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", getDimOverlayFromScale()); 331 anim.setStartDelay(mConfig.taskBarEnterAnimDelay); 332 anim.setDuration(mConfig.taskBarEnterAnimDuration); 333 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 334 anim.start(); 335 } else { 336 mEnableThumbnailClip.run(); 337 } 338 339 } else if (mConfig.launchedFromHome) { 340 // Animate the tasks up 341 int frontIndex = (ctx.stackViewCount - ctx.stackViewIndex - 1); 342 int delay = mConfig.taskBarEnterAnimDelay + 343 frontIndex * mConfig.taskViewEnterFromHomeDelay; 344 animate() 345 .scaleX(transform.scale) 346 .scaleY(transform.scale) 347 .translationY(transform.translationY) 348 .setStartDelay(delay) 349 .setUpdateListener(null) 350 .setInterpolator(mConfig.quintOutInterpolator) 351 .setDuration(mConfig.taskViewEnterFromHomeDuration) 352 .withLayer() 353 .withEndAction(mEnableThumbnailClip) 354 .start(); 355 } 356 } 357 358 /** Animates this task view as it leaves recents */ 359 public void animateOnExitRecents(ViewAnimation.TaskViewExitContext ctx) { 360 animate() 361 .translationY(ctx.offscreenTranslationY) 362 .setStartDelay(0) 363 .setUpdateListener(null) 364 .setInterpolator(mConfig.fastOutLinearInInterpolator) 365 .setDuration(mConfig.taskViewExitToHomeDuration) 366 .withLayer() 367 .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) 368 .start(); 369 ctx.postAnimationTrigger.increment(); 370 } 371 372 /** Animates this task view if the user does not interact with the stack after a certain time. */ 373 public void animateOnNoUserInteraction() { 374 mBarView.animateOnNoUserInteraction(); 375 } 376 377 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ 378 public void setOnNoUserInteraction() { 379 mBarView.setOnNoUserInteraction(); 380 } 381 382 /** Animates this task view as it exits recents */ 383 public void animateOnLaunchingTask(final Runnable r) { 384 // Disable the thumbnail clip and animate the bar out 385 mBarView.animateOnLaunchingTask(mDisableThumbnailClip, r); 386 387 // Animate the dim 388 if (mDim > 0) { 389 ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); 390 anim.setDuration(mConfig.taskBarExitAnimDuration); 391 anim.setInterpolator(mConfig.fastOutLinearInInterpolator); 392 anim.start(); 393 } 394 } 395 396 /** Animates the deletion of this task view */ 397 public void animateRemoval(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 /** Returns the rect we want to clip (it may not be the full rect) */ 426 Rect getClippingRect(Rect outRect) { 427 getHitRect(outRect); 428 // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster 429 outRect.right = outRect.left + mThumbnailView.getRight(); 430 outRect.bottom = outRect.top + mThumbnailView.getBottom(); 431 return outRect; 432 } 433 434 /** Enable the hw layers on this task view */ 435 void enableHwLayers() { 436 mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 437 mBarView.enableHwLayers(); 438 } 439 440 /** Disable the hw layers on this task view */ 441 void disableHwLayers() { 442 mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null); 443 mBarView.disableHwLayers(); 444 } 445 446 /** 447 * Returns whether this view should be clipped, or any views below should clip against this 448 * view. 449 */ 450 boolean shouldClipViewInStack() { 451 return mClipViewInStack && (getVisibility() == View.VISIBLE); 452 } 453 454 /** Sets whether this view should be clipped, or clipped against. */ 455 void setClipViewInStack(boolean clip) { 456 if (clip != mClipViewInStack) { 457 mClipViewInStack = clip; 458 if (getParent() instanceof View) { 459 getHitRect(mTmpRect); 460 ((View) getParent()).invalidate(mTmpRect); 461 } 462 } 463 } 464 465 /** Returns the current dim. */ 466 public void setDim(int dim) { 467 mDim = dim; 468 postInvalidateOnAnimation(); 469 } 470 471 /** Returns the current dim. */ 472 public int getDim() { 473 return mDim; 474 } 475 476 /** Compute the dim as a function of the scale of this view. */ 477 int getDimOverlayFromScale() { 478 float minScale = Constants.Values.TaskStackView.StackPeekMinScale; 479 float scaleRange = 1f - minScale; 480 float dim = (1f - getScaleX()) / scaleRange; 481 dim = mDimInterpolator.getInterpolation(Math.min(dim, 1f)); 482 return Math.max(0, Math.min(mMaxDim, (int) (dim * 255))); 483 } 484 485 /** Update the dim as a function of the scale of this view. */ 486 void updateDimOverlayFromScale() { 487 setDim(getDimOverlayFromScale()); 488 } 489 490 @Override 491 public void draw(Canvas canvas) { 492 // Apply the rounded rect clip path on the whole view 493 canvas.clipPath(mRoundedRectClipPath); 494 495 super.draw(canvas); 496 497 // Apply the dim if necessary 498 if (mDim > 0) { 499 canvas.drawColor(mDim << 24); 500 } 501 } 502 503 /** 504 * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen 505 * if the view is not currently visible, or we are in touch state (where we still want to keep 506 * track of focus). 507 */ 508 public void setFocusedTask() { 509 mIsFocused = true; 510 requestFocus(); 511 invalidate(); 512 mCb.onTaskFocused(this); 513 } 514 515 /** 516 * Updates the explicitly focused state when the view focus changes. 517 */ 518 @Override 519 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 520 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 521 if (!gainFocus) { 522 mIsFocused = false; 523 invalidate(); 524 } 525 } 526 527 /** 528 * Returns whether we have explicitly been focused. 529 */ 530 public boolean isFocusedTask() { 531 return mIsFocused || isFocused(); 532 } 533 534 /**** TaskCallbacks Implementation ****/ 535 536 /** Binds this task view to the task */ 537 public void onTaskBound(Task t) { 538 mTask = t; 539 mTask.setCallbacks(this); 540 } 541 542 @Override 543 public void onTaskDataLoaded(boolean reloadingTaskData) { 544 if (mThumbnailView != null && mBarView != null) { 545 // Bind each of the views to the new task data 546 mThumbnailView.rebindToTask(mTask, reloadingTaskData); 547 mBarView.rebindToTask(mTask, reloadingTaskData); 548 // Rebind any listeners 549 mBarView.mApplicationIcon.setOnClickListener(this); 550 mBarView.mDismissButton.setOnClickListener(this); 551 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 552 if (mConfig.developerOptionsEnabled) { 553 mBarView.mApplicationIcon.setOnLongClickListener(this); 554 } 555 } 556 } 557 mTaskDataLoaded = true; 558 } 559 560 @Override 561 public void onTaskDataUnloaded() { 562 if (mThumbnailView != null && mBarView != null) { 563 // Unbind each of the views from the task data and remove the task callback 564 mTask.setCallbacks(null); 565 mThumbnailView.unbindFromTask(); 566 mBarView.unbindFromTask(); 567 // Unbind any listeners 568 mBarView.mApplicationIcon.setOnClickListener(null); 569 mBarView.mDismissButton.setOnClickListener(null); 570 if (Constants.DebugFlags.App.EnableDevAppInfoOnLongPress) { 571 mBarView.mApplicationIcon.setOnLongClickListener(null); 572 } 573 } 574 mTaskDataLoaded = false; 575 } 576 577 @Override 578 public void onClick(View v) { 579 if (v == mBarView.mApplicationIcon) { 580 mCb.onTaskIconClicked(this); 581 } else if (v == mBarView.mDismissButton) { 582 // Animate out the view and call the callback 583 final TaskView tv = this; 584 animateRemoval(new Runnable() { 585 @Override 586 public void run() { 587 mCb.onTaskDismissed(tv); 588 } 589 }); 590 } 591 } 592 593 @Override 594 public boolean onLongClick(View v) { 595 if (v == mBarView.mApplicationIcon) { 596 mCb.onTaskAppInfoClicked(this); 597 return true; 598 } 599 return false; 600 } 601} 602