TaskView.java revision 65c851e6e9e08656744b6f66d3da188e3283b17d
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.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.animation.ValueAnimator; 24import android.content.Context; 25import android.content.res.Resources; 26import android.graphics.Color; 27import android.graphics.Outline; 28import android.graphics.Paint; 29import android.graphics.Point; 30import android.graphics.PorterDuff; 31import android.graphics.PorterDuffColorFilter; 32import android.graphics.Rect; 33import android.util.AttributeSet; 34import android.util.FloatProperty; 35import android.util.IntProperty; 36import android.util.Property; 37import android.view.MotionEvent; 38import android.view.View; 39import android.view.ViewOutlineProvider; 40import android.view.animation.AccelerateInterpolator; 41import android.view.animation.AnimationUtils; 42import android.view.animation.Interpolator; 43import android.widget.FrameLayout; 44import com.android.systemui.R; 45import com.android.systemui.recents.Recents; 46import com.android.systemui.recents.RecentsActivity; 47import com.android.systemui.recents.RecentsConfiguration; 48import com.android.systemui.recents.events.EventBus; 49import com.android.systemui.recents.events.activity.LaunchTaskEvent; 50import com.android.systemui.recents.events.ui.DismissTaskViewEvent; 51import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; 52import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; 53import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; 54import com.android.systemui.recents.misc.ReferenceCountedTrigger; 55import com.android.systemui.recents.misc.SystemServicesProxy; 56import com.android.systemui.recents.misc.Utilities; 57import com.android.systemui.recents.model.Task; 58import com.android.systemui.recents.model.TaskStack; 59import com.android.systemui.statusbar.phone.PhoneStatusBar; 60 61import java.util.ArrayList; 62 63import static android.app.ActivityManager.StackId.INVALID_STACK_ID; 64 65/* A task view */ 66public class TaskView extends FrameLayout implements Task.TaskCallbacks, 67 TaskStackAnimationHelper.Callbacks, View.OnClickListener, View.OnLongClickListener { 68 69 /** The TaskView callbacks */ 70 interface TaskViewCallbacks { 71 void onTaskViewClipStateChanged(TaskView tv); 72 } 73 74 /** 75 * The dim overlay is generally calculated from the task progress, but occasionally (like when 76 * launching) needs to be animated independently of the task progress. 77 */ 78 public static final Property<TaskView, Integer> DIM = 79 new IntProperty<TaskView>("dim") { 80 @Override 81 public void setValue(TaskView tv, int dim) { 82 tv.setDim(dim); 83 } 84 85 @Override 86 public Integer get(TaskView tv) { 87 return tv.getDim(); 88 } 89 }; 90 91 public static final Property<TaskView, Float> TASK_PROGRESS = 92 new FloatProperty<TaskView>("taskProgress") { 93 @Override 94 public void setValue(TaskView tv, float p) { 95 tv.setTaskProgress(p); 96 } 97 98 @Override 99 public Float get(TaskView tv) { 100 return tv.getTaskProgress(); 101 } 102 }; 103 104 float mTaskProgress; 105 float mMaxDimScale; 106 int mDimAlpha; 107 AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(3f); 108 PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); 109 Paint mDimLayerPaint = new Paint(); 110 float mActionButtonTranslationZ; 111 112 Task mTask; 113 boolean mTaskDataLoaded; 114 boolean mClipViewInStack = true; 115 AnimateableViewBounds mViewBounds; 116 117 private AnimatorSet mTransformAnimation; 118 private ArrayList<Animator> mTmpAnimators = new ArrayList<>(); 119 120 View mContent; 121 TaskViewThumbnail mThumbnailView; 122 TaskViewHeader mHeaderView; 123 View mActionButtonView; 124 TaskViewCallbacks mCb; 125 126 Point mDownTouchPos = new Point(); 127 128 Interpolator mFastOutSlowInInterpolator; 129 130 public TaskView(Context context) { 131 this(context, null); 132 } 133 134 public TaskView(Context context, AttributeSet attrs) { 135 this(context, attrs, 0); 136 } 137 138 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 139 this(context, attrs, defStyleAttr, 0); 140 } 141 142 public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 143 super(context, attrs, defStyleAttr, defStyleRes); 144 RecentsConfiguration config = Recents.getConfiguration(); 145 Resources res = context.getResources(); 146 mMaxDimScale = res.getInteger(R.integer.recents_max_task_stack_view_dim) / 255f; 147 mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize( 148 R.dimen.recents_task_view_rounded_corners_radius)); 149 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, 150 com.android.internal.R.interpolator.fast_out_slow_in); 151 if (config.fakeShadows) { 152 setBackground(new FakeShadowDrawable(res, config)); 153 } 154 setOutlineProvider(mViewBounds); 155 setOnLongClickListener(this); 156 } 157 158 /** Set callback */ 159 void setCallbacks(TaskViewCallbacks cb) { 160 mCb = cb; 161 } 162 163 /** Resets this TaskView for reuse. */ 164 void reset() { 165 resetViewProperties(); 166 resetNoUserInteractionState(); 167 setClipViewInStack(false); 168 setCallbacks(null); 169 } 170 171 /** Gets the task */ 172 public Task getTask() { 173 return mTask; 174 } 175 176 /** Returns the view bounds. */ 177 AnimateableViewBounds getViewBounds() { 178 return mViewBounds; 179 } 180 181 @Override 182 protected void onFinishInflate() { 183 // Bind the views 184 mContent = findViewById(R.id.task_view_content); 185 mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); 186 mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); 187 mActionButtonView = findViewById(R.id.lock_to_app_fab); 188 mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { 189 @Override 190 public void getOutline(View view, Outline outline) { 191 // Set the outline to match the FAB background 192 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight()); 193 outline.setAlpha(0.35f); 194 } 195 }); 196 mActionButtonView.setOnClickListener(this); 197 mActionButtonTranslationZ = mActionButtonView.getTranslationZ(); 198 } 199 200 @Override 201 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 202 super.onSizeChanged(w, h, oldw, oldh); 203 if (w > 0 && h > 0) { 204 mHeaderView.onTaskViewSizeChanged(w, h); 205 mThumbnailView.onTaskViewSizeChanged(w, h); 206 } 207 } 208 209 @Override 210 public boolean hasOverlappingRendering() { 211 return false; 212 } 213 214 @Override 215 public boolean onInterceptTouchEvent(MotionEvent ev) { 216 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 217 mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY())); 218 } 219 return super.onInterceptTouchEvent(ev); 220 } 221 222 @Override 223 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 224 int width = MeasureSpec.getSize(widthMeasureSpec); 225 int height = MeasureSpec.getSize(heightMeasureSpec); 226 227 int widthWithoutPadding = width - mPaddingLeft - mPaddingRight; 228 int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; 229 int taskBarHeight = getResources().getDimensionPixelSize(R.dimen.recents_task_bar_height); 230 231 // Measure the content 232 mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 233 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY)); 234 235 // Measure the bar view, and action button 236 mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 237 MeasureSpec.makeMeasureSpec(taskBarHeight, MeasureSpec.EXACTLY)); 238 mActionButtonView.measure( 239 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.AT_MOST), 240 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.AT_MOST)); 241 // Measure the thumbnail to be square 242 mThumbnailView.measure( 243 MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), 244 MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY)); 245 mThumbnailView.updateClipToTaskBar(mHeaderView); 246 247 setMeasuredDimension(width, height); 248 invalidateOutline(); 249 } 250 251 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, 252 TaskViewAnimation toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) { 253 RecentsConfiguration config = Recents.getConfiguration(); 254 Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); 255 256 // Compose the animations for the transform 257 mTmpAnimators.clear(); 258 toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows); 259 if (toAnimation.isImmediate()) { 260 if (Float.compare(getTaskProgress(), toTransform.p) != 0) { 261 setTaskProgress(toTransform.p); 262 } 263 // Manually call back to the animator listener and update callback 264 if (toAnimation.listener != null) { 265 toAnimation.listener.onAnimationEnd(null); 266 } 267 if (updateCallback != null) { 268 updateCallback.onAnimationUpdate(null); 269 } 270 } else { 271 if (Float.compare(getTaskProgress(), toTransform.p) != 0) { 272 mTmpAnimators.add(ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(), 273 toTransform.p)); 274 } 275 if (updateCallback != null) { 276 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1); 277 updateCallbackAnim.addUpdateListener(updateCallback); 278 mTmpAnimators.add(updateCallbackAnim); 279 } 280 281 // Create the animator 282 mTransformAnimation = toAnimation.createAnimator(mTmpAnimators); 283 mTransformAnimation.start(); 284 } 285 } 286 287 /** Resets this view's properties */ 288 void resetViewProperties() { 289 Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); 290 setDim(0); 291 setVisibility(View.VISIBLE); 292 getViewBounds().reset(); 293 TaskViewTransform.reset(this); 294 295 mActionButtonView.setScaleX(1f); 296 mActionButtonView.setScaleY(1f); 297 mActionButtonView.setAlpha(0f); 298 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 299 } 300 301 /** 302 * Cancels any current transform animations. 303 */ 304 public void cancelTransformAnimation() { 305 Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); 306 } 307 308 /** Enables/disables handling touch on this task view. */ 309 void setTouchEnabled(boolean enabled) { 310 setOnClickListener(enabled ? this : null); 311 } 312 313 /** Animates this task view if the user does not interact with the stack after a certain time. */ 314 void startNoUserInteractionAnimation() { 315 mHeaderView.startNoUserInteractionAnimation(); 316 } 317 318 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ 319 void setNoUserInteractionState() { 320 mHeaderView.setNoUserInteractionState(); 321 } 322 323 /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ 324 void resetNoUserInteractionState() { 325 mHeaderView.resetNoUserInteractionState(); 326 } 327 328 /** Dismisses this task. */ 329 void dismissTask() { 330 // Animate out the view and call the callback 331 final TaskView tv = this; 332 DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv, mTask); 333 dismissEvent.addPostAnimationCallback(new Runnable() { 334 @Override 335 public void run() { 336 EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv)); 337 } 338 }); 339 EventBus.getDefault().send(dismissEvent); 340 } 341 342 /** 343 * Returns whether this view should be clipped, or any views below should clip against this 344 * view. 345 */ 346 boolean shouldClipViewInStack() { 347 // Never clip for freeform tasks or if invisible 348 if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) { 349 return false; 350 } 351 return mClipViewInStack; 352 } 353 354 /** Sets whether this view should be clipped, or clipped against. */ 355 void setClipViewInStack(boolean clip) { 356 if (clip != mClipViewInStack) { 357 mClipViewInStack = clip; 358 if (mCb != null) { 359 mCb.onTaskViewClipStateChanged(this); 360 } 361 } 362 } 363 364 /** Sets the current task progress. */ 365 public void setTaskProgress(float p) { 366 mTaskProgress = p; 367 mViewBounds.setAlpha(p); 368 updateDimFromTaskProgress(); 369 } 370 371 public TaskViewHeader getHeaderView() { 372 return mHeaderView; 373 } 374 375 /** Returns the current task progress. */ 376 public float getTaskProgress() { 377 return mTaskProgress; 378 } 379 380 /** Returns the current dim. */ 381 public void setDim(int dim) { 382 RecentsConfiguration config = Recents.getConfiguration(); 383 384 mDimAlpha = dim; 385 if (config.useHardwareLayers) { 386 // Defer setting hardware layers if we have not yet measured, or there is no dim to draw 387 if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { 388 mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0)); 389 mDimLayerPaint.setColorFilter(mDimColorFilter); 390 mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); 391 } 392 } else { 393 float dimAlpha = mDimAlpha / 255.0f; 394 mThumbnailView.setDimAlpha(dimAlpha); 395 mHeaderView.setDimAlpha(dimAlpha); 396 } 397 } 398 399 /** Returns the current dim. */ 400 public int getDim() { 401 return mDimAlpha; 402 } 403 404 /** Animates the dim to the task progress. */ 405 void animateDimToProgress(int duration, Animator.AnimatorListener animListener) { 406 // Animate the dim into view as well 407 int toDim = getDimFromTaskProgress(); 408 if (toDim != getDim()) { 409 ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), toDim); 410 anim.setDuration(duration); 411 if (animListener != null) { 412 anim.addListener(animListener); 413 } 414 anim.start(); 415 } else { 416 animListener.onAnimationEnd(null); 417 } 418 } 419 420 /** Compute the dim as a function of the scale of this view. */ 421 int getDimFromTaskProgress() { 422 float x = mTaskProgress < 0 423 ? 1f 424 : mDimInterpolator.getInterpolation(1f - mTaskProgress); 425 float dim = mMaxDimScale * x; 426 return (int) (dim * 255); 427 } 428 429 /** Update the dim as a function of the scale of this view. */ 430 void updateDimFromTaskProgress() { 431 setDim(getDimFromTaskProgress()); 432 } 433 434 /** 435 * Explicitly sets the focused state of this task. 436 */ 437 public void setFocusedState(boolean isFocused, boolean requestViewFocus) { 438 SystemServicesProxy ssp = Recents.getSystemServices(); 439 if (isFocused) { 440 if (requestViewFocus && !isFocused()) { 441 requestFocus(); 442 } 443 if (requestViewFocus && !isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) { 444 requestAccessibilityFocus(); 445 } 446 } else { 447 if (isAccessibilityFocused() && ssp.isTouchExplorationEnabled()) { 448 clearAccessibilityFocus(); 449 } 450 } 451 } 452 453 /** 454 * Shows the action button. 455 * @param fadeIn whether or not to animate the action button in. 456 * @param fadeInDuration the duration of the action button animation, only used if 457 * {@param fadeIn} is true. 458 */ 459 public void showActionButton(boolean fadeIn, int fadeInDuration) { 460 mActionButtonView.setVisibility(View.VISIBLE); 461 462 if (fadeIn && mActionButtonView.getAlpha() < 1f) { 463 mActionButtonView.animate() 464 .alpha(1f) 465 .scaleX(1f) 466 .scaleY(1f) 467 .setDuration(fadeInDuration) 468 .setInterpolator(PhoneStatusBar.ALPHA_IN) 469 .start(); 470 } else { 471 mActionButtonView.setScaleX(1f); 472 mActionButtonView.setScaleY(1f); 473 mActionButtonView.setAlpha(1f); 474 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 475 } 476 } 477 478 /** 479 * Immediately hides the action button. 480 * 481 * @param fadeOut whether or not to animate the action button out. 482 */ 483 public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, 484 final Animator.AnimatorListener animListener) { 485 if (fadeOut && mActionButtonView.getAlpha() > 0f) { 486 if (scaleDown) { 487 float toScale = 0.9f; 488 mActionButtonView.animate() 489 .scaleX(toScale) 490 .scaleY(toScale); 491 } 492 mActionButtonView.animate() 493 .alpha(0f) 494 .setDuration(fadeOutDuration) 495 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 496 .withEndAction(new Runnable() { 497 @Override 498 public void run() { 499 if (animListener != null) { 500 animListener.onAnimationEnd(null); 501 } 502 mActionButtonView.setVisibility(View.INVISIBLE); 503 } 504 }) 505 .start(); 506 } else { 507 mActionButtonView.setAlpha(0f); 508 mActionButtonView.setVisibility(View.INVISIBLE); 509 if (animListener != null) { 510 animListener.onAnimationEnd(null); 511 } 512 } 513 } 514 515 /**** TaskStackAnimationHelper.Callbacks Implementation ****/ 516 517 @Override 518 public void onPrepareLaunchTargetForEnterAnimation() { 519 // These values will be animated in when onStartLaunchTargetEnterAnimation() is called 520 setDim(0); 521 mActionButtonView.setAlpha(0f); 522 } 523 524 @Override 525 public void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled, 526 ReferenceCountedTrigger postAnimationTrigger) { 527 postAnimationTrigger.increment(); 528 animateDimToProgress(duration, postAnimationTrigger.decrementOnAnimationEnd()); 529 530 if (screenPinningEnabled) { 531 showActionButton(true /* fadeIn */, duration /* fadeInDuration */); 532 } 533 } 534 535 @Override 536 public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, 537 ReferenceCountedTrigger postAnimationTrigger) { 538 if (mDimAlpha > 0) { 539 ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), 0); 540 anim.setDuration(duration); 541 anim.setInterpolator(PhoneStatusBar.ALPHA_OUT); 542 anim.start(); 543 } 544 545 postAnimationTrigger.increment(); 546 hideActionButton(true /* fadeOut */, duration, 547 !screenPinningRequested /* scaleDown */, 548 postAnimationTrigger.decrementOnAnimationEnd()); 549 } 550 551 /**** TaskCallbacks Implementation ****/ 552 553 public void onTaskBound(Task t) { 554 mTask = t; 555 mTask.addCallback(this); 556 } 557 558 @Override 559 public void onTaskDataLoaded(Task task) { 560 // Bind each of the views to the new task data 561 mThumbnailView.rebindToTask(mTask); 562 mHeaderView.rebindToTask(mTask); 563 mTaskDataLoaded = true; 564 } 565 566 @Override 567 public void onTaskDataUnloaded() { 568 // Unbind each of the views from the task data and remove the task callback 569 mTask.removeCallback(this); 570 mThumbnailView.unbindFromTask(); 571 mHeaderView.unbindFromTask(); 572 mTaskDataLoaded = false; 573 } 574 575 @Override 576 public void onTaskStackIdChanged() { 577 mHeaderView.rebindToTask(mTask); 578 } 579 580 /**** View.OnClickListener Implementation ****/ 581 582 @Override 583 public void onClick(final View v) { 584 boolean screenPinningRequested = false; 585 if (v == mActionButtonView) { 586 // Reset the translation of the action button before we animate it out 587 mActionButtonView.setTranslationZ(0f); 588 screenPinningRequested = true; 589 } 590 EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID, 591 screenPinningRequested)); 592 } 593 594 /**** View.OnLongClickListener Implementation ****/ 595 596 @Override 597 public boolean onLongClick(View v) { 598 SystemServicesProxy ssp = Recents.getSystemServices(); 599 // Since we are clipping the view to the bounds, manually do the hit test 600 Rect clipBounds = new Rect(mViewBounds.mClipBounds); 601 clipBounds.scale(getScaleX()); 602 boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y); 603 if (v == this && inBounds && !ssp.hasDockedTask()) { 604 // Start listening for drag events 605 setClipViewInStack(false); 606 607 mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2; 608 mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2; 609 610 EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1); 611 EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos)); 612 return true; 613 } 614 return false; 615 } 616 617 /**** Events ****/ 618 619 public final void onBusEvent(DragEndEvent event) { 620 if (!(event.dropTarget instanceof TaskStack.DockState)) { 621 event.addPostAnimationCallback(new Runnable() { 622 @Override 623 public void run() { 624 // Animate the drag view back from where it is, to the view location, then after 625 // it returns, update the clip state 626 setClipViewInStack(true); 627 } 628 }); 629 } 630 EventBus.getDefault().unregister(this); 631 } 632} 633