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