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