1 /* 2 * Copyright (C) 2019 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 package com.android.quickstep.views; 17 18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 19 20 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 21 22 import static com.android.launcher3.anim.Interpolators.ACCEL_2; 23 import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT; 24 import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL; 25 import static com.android.quickstep.TaskAdapter.ITEM_TYPE_TASK; 26 import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY; 27 import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION; 28 import static com.android.quickstep.util.RemoteAnimationProvider.getLayer; 29 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; 30 31 import android.animation.Animator; 32 import android.animation.AnimatorListenerAdapter; 33 import android.animation.AnimatorSet; 34 import android.animation.ObjectAnimator; 35 import android.animation.PropertyValuesHolder; 36 import android.animation.ValueAnimator; 37 import android.content.Context; 38 import android.content.res.Resources; 39 import android.graphics.Matrix; 40 import android.graphics.Rect; 41 import android.graphics.RectF; 42 import android.graphics.drawable.Drawable; 43 import android.util.ArraySet; 44 import android.util.AttributeSet; 45 import android.util.FloatProperty; 46 import android.view.View; 47 import android.view.ViewDebug; 48 import android.view.ViewTreeObserver; 49 import android.view.animation.PathInterpolator; 50 import android.widget.FrameLayout; 51 52 import androidx.annotation.NonNull; 53 import androidx.annotation.Nullable; 54 import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; 55 import androidx.recyclerview.widget.DefaultItemAnimator; 56 import androidx.recyclerview.widget.ItemTouchHelper; 57 import androidx.recyclerview.widget.LinearLayoutManager; 58 import androidx.recyclerview.widget.RecyclerView; 59 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; 60 import androidx.recyclerview.widget.RecyclerView.ItemDecoration; 61 import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener; 62 63 import com.android.launcher3.BaseActivity; 64 import com.android.launcher3.Insettable; 65 import com.android.launcher3.R; 66 import com.android.launcher3.util.Themes; 67 import com.android.quickstep.ContentFillItemAnimator; 68 import com.android.quickstep.RecentsModel; 69 import com.android.quickstep.RecentsToActivityHelper; 70 import com.android.quickstep.TaskActionController; 71 import com.android.quickstep.TaskAdapter; 72 import com.android.quickstep.TaskHolder; 73 import com.android.quickstep.TaskListLoader; 74 import com.android.quickstep.TaskSwipeCallback; 75 import com.android.quickstep.util.MultiValueUpdateListener; 76 import com.android.systemui.shared.recents.model.Task; 77 import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 78 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; 79 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; 80 81 import java.util.ArrayList; 82 import java.util.List; 83 import java.util.Objects; 84 import java.util.Optional; 85 86 /** 87 * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code 88 * base. 89 */ 90 public final class IconRecentsView extends FrameLayout implements Insettable { 91 92 public static final FloatProperty<IconRecentsView> CONTENT_ALPHA = 93 new FloatProperty<IconRecentsView>("contentAlpha") { 94 @Override 95 public void setValue(IconRecentsView view, float v) { 96 ALPHA.set(view, v); 97 if (view.getVisibility() != VISIBLE && v > 0) { 98 view.setVisibility(VISIBLE); 99 } else if (view.getVisibility() != GONE && v == 0){ 100 view.setVisibility(GONE); 101 } 102 } 103 104 @Override 105 public Float get(IconRecentsView view) { 106 return ALPHA.get(view); 107 } 108 }; 109 private static final long CROSSFADE_DURATION = 300; 110 private static final long LAYOUT_ITEM_ANIMATE_IN_DURATION = 150; 111 private static final long LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN = 40; 112 private static final long ITEM_ANIMATE_OUT_DURATION = 150; 113 private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40; 114 private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f; 115 private static final long CLEAR_ALL_FADE_DELAY = 120; 116 117 private static final long REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION = 300; 118 private static final long REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION = 400; 119 private static final long REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY = 200; 120 private static final long REMOTE_TO_RECENTS_ITEM_FADE_DURATION = 217; 121 private static final long REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY = 33; 122 123 private static final PathInterpolator FAST_OUT_SLOW_IN_1 = 124 new PathInterpolator(.4f, 0f, 0f, 1f); 125 private static final PathInterpolator FAST_OUT_SLOW_IN_2 = 126 new PathInterpolator(.5f, 0f, 0f, 1f); 127 private static final LinearOutSlowInInterpolator OUT_SLOW_IN = 128 new LinearOutSlowInInterpolator(); 129 130 public static final long REMOTE_APP_TO_OVERVIEW_DURATION = 131 REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION; 132 133 /** 134 * A ratio representing the view's relative placement within its padded space. For example, 0 135 * is top aligned and 0.5 is centered vertically. 136 */ 137 @ViewDebug.ExportedProperty(category = "launcher") 138 139 private final Context mContext; 140 private final TaskListLoader mTaskLoader; 141 private final TaskAdapter mTaskAdapter; 142 private final LinearLayoutManager mTaskLayoutManager; 143 private final TaskActionController mTaskActionController; 144 private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator(); 145 private final ContentFillItemAnimator mLoadingContentItemAnimator = 146 new ContentFillItemAnimator(); 147 private final BaseActivity mActivity; 148 private final Drawable mStatusBarForegroundScrim; 149 150 private RecentsToActivityHelper mActivityHelper; 151 private RecyclerView mTaskRecyclerView; 152 private View mShowingContentView; 153 private View mEmptyView; 154 private View mContentView; 155 private boolean mTransitionedFromApp; 156 private boolean mUsingRemoteAnimation; 157 private boolean mStartedEnterAnimation; 158 private boolean mShowStatusBarForegroundScrim; 159 private AnimatorSet mLayoutAnimation; 160 private final ArraySet<View> mLayingOutViews = new ArraySet<>(); 161 private Rect mInsets; 162 private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> { 163 ArrayList<TaskItemView> itemViews = getTaskViews(); 164 for (int i = 0, size = itemViews.size(); i < size; i++) { 165 TaskItemView taskView = itemViews.get(i); 166 TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView); 167 Optional<Task> optTask = taskHolder.getTask(); 168 if (optTask.filter(task -> task.key.id == taskId).isPresent()) { 169 Task task = optTask.get(); 170 // Update thumbnail on the task. 171 task.thumbnail = thumbnailData; 172 taskView.setThumbnail(thumbnailData); 173 return task; 174 } 175 } 176 return null; 177 }; 178 IconRecentsView(Context context, AttributeSet attrs)179 public IconRecentsView(Context context, AttributeSet attrs) { 180 super(context, attrs); 181 mActivity = BaseActivity.fromContext(context); 182 mContext = context; 183 mStatusBarForegroundScrim = 184 Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim); 185 mTaskLoader = new TaskListLoader(mContext); 186 mTaskAdapter = new TaskAdapter(mTaskLoader); 187 mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks()); 188 mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter, 189 mActivity.getStatsLogManager()); 190 mTaskAdapter.setActionController(mTaskActionController); 191 mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */); 192 RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener); 193 } 194 195 @Override onFinishInflate()196 protected void onFinishInflate() { 197 super.onFinishInflate(); 198 if (mTaskRecyclerView == null) { 199 mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view); 200 mTaskRecyclerView.setAdapter(mTaskAdapter); 201 mTaskRecyclerView.setLayoutManager(mTaskLayoutManager); 202 ItemTouchHelper helper = new ItemTouchHelper( 203 new TaskSwipeCallback(holder -> { 204 mTaskActionController.removeTask(holder); 205 if (mTaskLoader.getCurrentTaskList().isEmpty()) { 206 mActivityHelper.leaveRecents(); 207 } 208 })); 209 helper.attachToRecyclerView(mTaskRecyclerView); 210 mTaskRecyclerView.addOnChildAttachStateChangeListener( 211 new OnChildAttachStateChangeListener() { 212 @Override 213 public void onChildViewAttachedToWindow(@NonNull View view) { 214 if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) { 215 // Child view was added that is not part of current layout animation 216 // so restart the animation. 217 animateFadeInLayoutAnimation(); 218 } 219 } 220 221 @Override 222 public void onChildViewDetachedFromWindow(@NonNull View view) { } 223 }); 224 mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator); 225 mLoadingContentItemAnimator.setOnAnimationFinishedRunnable( 226 () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator())); 227 ItemDecoration marginDecorator = new ItemDecoration() { 228 @Override 229 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, 230 @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 231 // TODO: Determine if current margins cause off screen item to be fully off 232 // screen and if so, modify them so that it is partially off screen. 233 int itemType = parent.getChildViewHolder(view).getItemViewType(); 234 Resources res = getResources(); 235 switch (itemType) { 236 case ITEM_TYPE_CLEAR_ALL: 237 outRect.top = (int) res.getDimension( 238 R.dimen.clear_all_item_view_top_margin); 239 outRect.bottom = (int) res.getDimension( 240 R.dimen.clear_all_item_view_bottom_margin); 241 break; 242 case ITEM_TYPE_TASK: 243 int desiredTopMargin = (int) res.getDimension( 244 R.dimen.task_item_top_margin); 245 if (mTaskRecyclerView.getChildAdapterPosition(view) == 246 state.getItemCount() - 1) { 247 // Only add top margin to top task view if insets aren't enough. 248 if (mInsets.top < desiredTopMargin) { 249 outRect.top = desiredTopMargin - mInsets.bottom; 250 } 251 return; 252 } 253 outRect.top = desiredTopMargin; 254 break; 255 default: 256 } 257 } 258 }; 259 mTaskRecyclerView.addItemDecoration(marginDecorator); 260 261 mEmptyView = findViewById(R.id.recent_task_empty_view); 262 mContentView = mTaskRecyclerView; 263 mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() { 264 @Override 265 public void onChanged() { 266 updateContentViewVisibility(); 267 } 268 269 @Override 270 public void onItemRangeRemoved(int positionStart, int itemCount) { 271 updateContentViewVisibility(); 272 } 273 }); 274 } 275 } 276 277 @Override setEnabled(boolean enabled)278 public void setEnabled(boolean enabled) { 279 super.setEnabled(enabled); 280 int childCount = mTaskRecyclerView.getChildCount(); 281 for (int i = 0; i < childCount; i++) { 282 mTaskRecyclerView.getChildAt(i).setEnabled(enabled); 283 } 284 } 285 286 /** 287 * Set activity helper for the view to callback to. 288 * 289 * @param helper the activity helper 290 */ setRecentsToActivityHelper(@onNull RecentsToActivityHelper helper)291 public void setRecentsToActivityHelper(@NonNull RecentsToActivityHelper helper) { 292 mActivityHelper = helper; 293 } 294 295 /** 296 * Logic for when we know we are going to overview/recents and will be putting up the recents 297 * view. This should be used to prepare recents (e.g. load any task data, etc.) before it 298 * becomes visible. 299 */ onBeginTransitionToOverview()300 public void onBeginTransitionToOverview() { 301 mStartedEnterAnimation = false; 302 if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { 303 // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as 304 // all tasks should be visible to fill up the screen in portrait mode and the view will 305 // not be scrollable. 306 mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */); 307 } 308 if (!mUsingRemoteAnimation) { 309 scheduleFadeInLayoutAnimation(); 310 } 311 // Load any task changes 312 if (!mTaskLoader.needsToLoad()) { 313 return; 314 } 315 mTaskAdapter.setIsShowingLoadingUi(true); 316 mTaskAdapter.notifyDataSetChanged(); 317 mTaskLoader.loadTaskList(tasks -> { 318 int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION; 319 mTaskAdapter.setIsShowingLoadingUi(false); 320 int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION; 321 if (numEmptyItems < numActualItems) { 322 throw new IllegalStateException("There are less empty item views than the number " 323 + "of items to animate to."); 324 } 325 // Possible that task list loads faster than adapter changes propagate to layout so 326 // only start content fill animation if there aren't any pending adapter changes and 327 // we've started the on enter layout animation. 328 boolean needsContentFillAnimation = 329 !mTaskRecyclerView.hasPendingAdapterUpdates() && mStartedEnterAnimation; 330 if (needsContentFillAnimation) { 331 // Set item animator for content filling animation. The item animator will switch 332 // back to the default on completion 333 mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator); 334 mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems, 335 numEmptyItems - numActualItems); 336 mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems, 337 CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT); 338 } else { 339 // Notify change without animating. 340 mTaskAdapter.notifyDataSetChanged(); 341 } 342 }); 343 } 344 345 /** 346 * Set whether we transitioned to recents from the most recent app. 347 * 348 * @param transitionedFromApp true if transitioned from the most recent app, false otherwise 349 */ setTransitionedFromApp(boolean transitionedFromApp)350 public void setTransitionedFromApp(boolean transitionedFromApp) { 351 mTransitionedFromApp = transitionedFromApp; 352 } 353 354 /** 355 * Set whether we're using a custom remote animation. If so, we will not do the default layout 356 * animation when entering recents and instead wait for the remote app surface to be ready to 357 * use. 358 * 359 * @param usingRemoteAnimation true if doing a remote animation, false o/w 360 */ setUsingRemoteAnimation(boolean usingRemoteAnimation)361 public void setUsingRemoteAnimation(boolean usingRemoteAnimation) { 362 mUsingRemoteAnimation = usingRemoteAnimation; 363 } 364 365 /** 366 * Handles input from the overview button. Launch the most recent task unless we just came from 367 * the app. In that case, we launch the next most recent. 368 */ handleOverviewCommand()369 public void handleOverviewCommand() { 370 List<Task> tasks = mTaskLoader.getCurrentTaskList(); 371 int tasksSize = tasks.size(); 372 if (tasksSize == 0) { 373 // Do nothing 374 return; 375 } 376 Task taskToLaunch; 377 if (mTransitionedFromApp && tasksSize > 1) { 378 // Launch the next most recent app 379 taskToLaunch = tasks.get(1); 380 } else { 381 // Launch the most recent app 382 taskToLaunch = tasks.get(0); 383 } 384 385 // See if view for this task is attached, and if so, animate launch from that view. 386 ArrayList<TaskItemView> itemViews = getTaskViews(); 387 for (int i = 0, size = itemViews.size(); i < size; i++) { 388 TaskItemView taskView = itemViews.get(i); 389 TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView); 390 if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) { 391 mTaskActionController.launchTaskFromView(holder); 392 return; 393 } 394 } 395 396 // Otherwise, just use a basic launch animation. 397 mTaskActionController.launchTask(taskToLaunch); 398 } 399 400 /** 401 * Set whether or not to show the scrim in between the view and the top insets. This only works 402 * if the view is being insetted in the first place. 403 * 404 * The scrim is added to the activity's root view to prevent animations on this view 405 * affecting the scrim. As a result, it is the activity's responsibility to show/hide this 406 * scrim as appropriate. 407 * 408 * @param showStatusBarForegroundScrim true to show the scrim, false to hide 409 */ setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim)410 public void setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim) { 411 mShowStatusBarForegroundScrim = showStatusBarForegroundScrim; 412 if (mShowStatusBarForegroundScrim != showStatusBarForegroundScrim) { 413 updateStatusBarScrim(); 414 } 415 } 416 updateStatusBarScrim()417 private void updateStatusBarScrim() { 418 boolean shouldShow = mInsets.top != 0 && mShowStatusBarForegroundScrim; 419 mActivity.getDragLayer().setForeground(shouldShow ? mStatusBarForegroundScrim : null); 420 } 421 422 /** 423 * Get the bottom most task view to animate to. 424 * 425 * @return the task view 426 */ getBottomTaskView()427 private @Nullable TaskItemView getBottomTaskView() { 428 int childCount = mTaskRecyclerView.getChildCount(); 429 for (int i = 0; i < childCount; i++) { 430 View view = mTaskRecyclerView.getChildAt(i); 431 if (mTaskRecyclerView.getChildViewHolder(view).getItemViewType() == ITEM_TYPE_TASK) { 432 return (TaskItemView) view; 433 } 434 } 435 return null; 436 } 437 438 /** 439 * Whether this view has processed all data changes and is ready to animate from the app to 440 * the overview. 441 * 442 * @return true if ready to animate app to overview, false otherwise 443 */ isReadyForRemoteAnim()444 public boolean isReadyForRemoteAnim() { 445 return !mTaskRecyclerView.hasPendingAdapterUpdates(); 446 } 447 448 /** 449 * Set a callback for whenever this view is ready to do a remote animation from the app to 450 * overview. See {@link #isReadyForRemoteAnim()}. 451 * 452 * @param callback callback to run when view is ready to animate 453 */ setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback)454 public void setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback) { 455 mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( 456 new ViewTreeObserver.OnGlobalLayoutListener() { 457 @Override 458 public void onGlobalLayout() { 459 if (isReadyForRemoteAnim()) { 460 callback.onReadyForRemoteAnim(); 461 mTaskRecyclerView.getViewTreeObserver(). 462 removeOnGlobalLayoutListener(this); 463 } 464 } 465 }); 466 } 467 468 /** 469 * Clear all tasks and animate out. 470 */ animateClearAllTasks()471 private void animateClearAllTasks() { 472 setEnabled(false); 473 ArrayList<TaskItemView> itemViews = getTaskViews(); 474 475 AnimatorSet clearAnim = new AnimatorSet(); 476 long currentDelay = 0; 477 478 // Animate each item view to the right and fade out. 479 for (int i = 0, size = itemViews.size(); i < size; i++) { 480 TaskItemView itemView = itemViews.get(i); 481 PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X, 482 0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO); 483 PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f); 484 ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView, 485 transXproperty, alphaProperty); 486 itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION); 487 itemAnim.setStartDelay(currentDelay); 488 489 clearAnim.play(itemAnim); 490 currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN; 491 } 492 493 // Animate view fading and leave recents when faded enough. 494 ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f) 495 .setDuration(CROSSFADE_DURATION); 496 contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY); 497 contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 498 private boolean mLeftRecents = false; 499 500 @Override 501 public void onAnimationUpdate(ValueAnimator valueAnimator) { 502 mContentView.setAlpha((float) valueAnimator.getAnimatedValue()); 503 // Leave recents while fading out. 504 if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) { 505 mActivityHelper.leaveRecents(); 506 mLeftRecents = true; 507 } 508 } 509 }); 510 511 clearAnim.play(contentAlpha); 512 clearAnim.addListener(new AnimatorListenerAdapter() { 513 @Override 514 public void onAnimationEnd(Animator animation) { 515 for (int i = 0, size = itemViews.size(); i < size; i++) { 516 TaskItemView itemView = itemViews.get(i); 517 itemView.setTranslationX(0); 518 itemView.setAlpha(1.0f); 519 } 520 setEnabled(true); 521 mContentView.setVisibility(GONE); 522 mTaskActionController.clearAllTasks(); 523 } 524 }); 525 clearAnim.start(); 526 } 527 528 /** 529 * Get attached task item views ordered by most recent. 530 * 531 * @return array list of attached task item views 532 */ getTaskViews()533 private ArrayList<TaskItemView> getTaskViews() { 534 int taskCount = mTaskRecyclerView.getChildCount(); 535 ArrayList<TaskItemView> itemViews = new ArrayList<>(); 536 for (int i = 0; i < taskCount; i ++) { 537 View child = mTaskRecyclerView.getChildAt(i); 538 if (child instanceof TaskItemView) { 539 itemViews.add((TaskItemView) child); 540 } 541 } 542 return itemViews; 543 } 544 545 /** 546 * Update the content view so that the appropriate view is shown based off the current list 547 * of tasks. 548 */ updateContentViewVisibility()549 private void updateContentViewVisibility() { 550 int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION; 551 if (mShowingContentView != mEmptyView && taskListSize == 0) { 552 mShowingContentView = mEmptyView; 553 crossfadeViews(mEmptyView, mContentView); 554 } 555 if (mShowingContentView != mContentView && taskListSize > 0) { 556 mShowingContentView = mContentView; 557 crossfadeViews(mContentView, mEmptyView); 558 } 559 } 560 561 /** 562 * Animate views so that one view fades in while the other fades out. 563 * 564 * @param fadeInView view that should fade in 565 * @param fadeOutView view that should fade out 566 */ crossfadeViews(View fadeInView, View fadeOutView)567 private void crossfadeViews(View fadeInView, View fadeOutView) { 568 fadeInView.animate().cancel(); 569 fadeInView.setVisibility(VISIBLE); 570 fadeInView.setAlpha(0f); 571 fadeInView.animate() 572 .alpha(1f) 573 .setDuration(CROSSFADE_DURATION) 574 .setListener(null); 575 576 fadeOutView.animate().cancel(); 577 fadeOutView.animate() 578 .alpha(0f) 579 .setDuration(CROSSFADE_DURATION) 580 .setListener(new AnimatorListenerAdapter() { 581 @Override 582 public void onAnimationEnd(Animator animation) { 583 fadeOutView.setVisibility(GONE); 584 } 585 }); 586 } 587 588 /** 589 * Schedule a one-shot layout animation on the next layout. Separate from 590 * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the 591 * view properties themselves, allowing more controllable behavior and making it easier to 592 * manage when the animation conflicts with another animation. 593 */ scheduleFadeInLayoutAnimation()594 private void scheduleFadeInLayoutAnimation() { 595 mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener( 596 new ViewTreeObserver.OnGlobalLayoutListener() { 597 @Override 598 public void onGlobalLayout() { 599 animateFadeInLayoutAnimation(); 600 mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 601 } 602 }); 603 } 604 605 /** 606 * Start animating the layout animation where items fade in. 607 */ animateFadeInLayoutAnimation()608 private void animateFadeInLayoutAnimation() { 609 if (mLayoutAnimation != null) { 610 // If layout animation still in progress, cancel and restart. 611 mLayoutAnimation.cancel(); 612 } 613 ArrayList<TaskItemView> views = getTaskViews(); 614 int delay = 0; 615 mLayoutAnimation = new AnimatorSet(); 616 for (int i = 0, size = views.size(); i < size; i++) { 617 TaskItemView view = views.get(i); 618 view.setAlpha(0.0f); 619 Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f); 620 alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay); 621 alphaAnim.addListener(new AnimatorListenerAdapter() { 622 @Override 623 public void onAnimationEnd(Animator animation) { 624 view.setAlpha(1.0f); 625 mLayingOutViews.remove(view); 626 } 627 }); 628 delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN; 629 mLayoutAnimation.play(alphaAnim); 630 mLayingOutViews.add(view); 631 } 632 mLayoutAnimation.addListener(new AnimatorListenerAdapter() { 633 @Override 634 public void onAnimationEnd(Animator animation) { 635 mLayoutAnimation = null; 636 } 637 }); 638 mLayoutAnimation.start(); 639 mStartedEnterAnimation = true; 640 } 641 642 /** 643 * Play remote app to recents animation when the app is the home activity. We use a simple 644 * cross-fade here. Note this is only used if the home activity is a separate app than the 645 * recents activity. 646 * 647 * @param anim animator set 648 * @param homeTarget the home surface thats closing 649 * @param recentsTarget the surface containing recents 650 */ playRemoteHomeToRecentsAnimation(@onNull AnimatorSet anim, @NonNull RemoteAnimationTargetCompat homeTarget, @NonNull RemoteAnimationTargetCompat recentsTarget)651 public void playRemoteHomeToRecentsAnimation(@NonNull AnimatorSet anim, 652 @NonNull RemoteAnimationTargetCompat homeTarget, 653 @NonNull RemoteAnimationTargetCompat recentsTarget) { 654 SyncRtSurfaceTransactionApplierCompat surfaceApplier = 655 new SyncRtSurfaceTransactionApplierCompat(this); 656 657 SurfaceParams[] params = new SurfaceParams[2]; 658 int boostedMode = MODE_CLOSING; 659 660 ValueAnimator remoteHomeAnim = ValueAnimator.ofFloat(0, 1); 661 remoteHomeAnim.setDuration(REMOTE_APP_TO_OVERVIEW_DURATION); 662 663 remoteHomeAnim.addUpdateListener(valueAnimator -> { 664 float val = (float) valueAnimator.getAnimatedValue(); 665 float alpha; 666 RemoteAnimationTargetCompat visibleTarget; 667 RemoteAnimationTargetCompat invisibleTarget; 668 if (val < .5f) { 669 visibleTarget = homeTarget; 670 invisibleTarget = recentsTarget; 671 alpha = 1 - (val * 2); 672 } else { 673 visibleTarget = recentsTarget; 674 invisibleTarget = homeTarget; 675 alpha = (val - .5f) * 2; 676 } 677 params[0] = new SurfaceParams(visibleTarget.leash, alpha, null /* matrix */, 678 null /* windowCrop */, getLayer(visibleTarget, boostedMode), 679 0 /* cornerRadius */); 680 params[1] = new SurfaceParams(invisibleTarget.leash, 0.0f, null /* matrix */, 681 null /* windowCrop */, getLayer(invisibleTarget, boostedMode), 682 0 /* cornerRadius */); 683 surfaceApplier.scheduleApply(params); 684 }); 685 anim.play(remoteHomeAnim); 686 animateFadeInLayoutAnimation(); 687 } 688 689 /** 690 * Play remote animation from app to recents. This should scale the currently closing app down 691 * to the recents thumbnail. 692 * 693 * @param anim animator set 694 * @param appTarget the app surface thats closing 695 * @param recentsTarget the surface containing recents 696 */ playRemoteAppToRecentsAnimation(@onNull AnimatorSet anim, @NonNull RemoteAnimationTargetCompat appTarget, @NonNull RemoteAnimationTargetCompat recentsTarget)697 public void playRemoteAppToRecentsAnimation(@NonNull AnimatorSet anim, 698 @NonNull RemoteAnimationTargetCompat appTarget, 699 @NonNull RemoteAnimationTargetCompat recentsTarget) { 700 TaskItemView bottomView = getBottomTaskView(); 701 if (bottomView == null) { 702 // This can be null if there were previously 0 tasks and the recycler view has not had 703 // enough time to take in the data change, bind a new view, and lay out the new view. 704 // TODO: Have a fallback to animate to 705 anim.play(ValueAnimator.ofInt(0, 1).setDuration(REMOTE_APP_TO_OVERVIEW_DURATION)); 706 return; 707 } 708 final Matrix appMatrix = new Matrix(); 709 playRemoteTransYAnim(anim, appMatrix); 710 playRemoteAppScaleDownAnim(anim, appMatrix, appTarget, recentsTarget, 711 bottomView.getThumbnailView()); 712 playRemoteTaskListFadeIn(anim, bottomView); 713 mStartedEnterAnimation = true; 714 } 715 716 /** 717 * Play translation Y animation for the remote app to recents animation. Animates over all task 718 * views as well as the closing app, easing them into their final vertical positions. 719 * 720 * @param anim animator set to play on 721 * @param appMatrix transformation matrix for the closing app surface 722 */ playRemoteTransYAnim(@onNull AnimatorSet anim, @NonNull Matrix appMatrix)723 private void playRemoteTransYAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix) { 724 final ArrayList<TaskItemView> views = getTaskViews(); 725 726 // Start Y translation from about halfway through the tasks list to the bottom thumbnail. 727 float taskHeight = getResources().getDimension(R.dimen.task_item_height); 728 float totalTransY = -(MAX_TASKS_TO_DISPLAY / 2.0f - 1) * taskHeight; 729 for (int i = 0, size = views.size(); i < size; i++) { 730 views.get(i).setTranslationY(totalTransY); 731 } 732 733 ValueAnimator transYAnim = ValueAnimator.ofFloat(totalTransY, 0); 734 transYAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION); 735 transYAnim.setInterpolator(FAST_OUT_SLOW_IN_2); 736 transYAnim.addUpdateListener(valueAnimator -> { 737 float transY = (float) valueAnimator.getAnimatedValue(); 738 for (int i = 0, size = views.size(); i < size; i++) { 739 views.get(i).setTranslationY(transY); 740 } 741 appMatrix.postTranslate(0, transY - totalTransY); 742 }); 743 transYAnim.addListener(new AnimatorListenerAdapter() { 744 @Override 745 public void onAnimationEnd(Animator animation) { 746 for (int i = 0, size = views.size(); i < size; i++) { 747 views.get(i).setTranslationY(0); 748 } 749 } 750 }); 751 anim.play(transYAnim); 752 } 753 754 /** 755 * Play the scale down animation for the remote app to recents animation where the app surface 756 * scales down to where the thumbnail is. 757 * 758 * @param anim animator set to play on 759 * @param appMatrix transformation matrix for the app surface 760 * @param appTarget closing app target 761 * @param recentsTarget opening recents target 762 * @param thumbnailView thumbnail view to animate to 763 */ playRemoteAppScaleDownAnim(@onNull AnimatorSet anim, @NonNull Matrix appMatrix, @NonNull RemoteAnimationTargetCompat appTarget, @NonNull RemoteAnimationTargetCompat recentsTarget, @NonNull View thumbnailView)764 private void playRemoteAppScaleDownAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix, 765 @NonNull RemoteAnimationTargetCompat appTarget, 766 @NonNull RemoteAnimationTargetCompat recentsTarget, 767 @NonNull View thumbnailView) { 768 // Identify where the entering remote app should animate to. 769 Rect endRect = new Rect(); 770 thumbnailView.getGlobalVisibleRect(endRect); 771 Rect appBounds = appTarget.sourceContainerBounds; 772 RectF currentAppRect = new RectF(); 773 774 SyncRtSurfaceTransactionApplierCompat surfaceApplier = 775 new SyncRtSurfaceTransactionApplierCompat(this); 776 777 // Keep recents visible throughout the animation. 778 SurfaceParams[] params = new SurfaceParams[2]; 779 // Closing app should stay on top. 780 int boostedMode = MODE_CLOSING; 781 params[0] = new SurfaceParams(recentsTarget.leash, 1f, null /* matrix */, 782 null /* windowCrop */, getLayer(recentsTarget, boostedMode), 0 /* cornerRadius */); 783 784 ValueAnimator remoteAppAnim = ValueAnimator.ofInt(0, 1); 785 remoteAppAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION); 786 remoteAppAnim.addUpdateListener(new MultiValueUpdateListener() { 787 private final FloatProp mScaleX; 788 private final FloatProp mScaleY; 789 private final FloatProp mTranslationX; 790 private final FloatProp mTranslationY; 791 private final FloatProp mAlpha; 792 793 { 794 // Scale down and move to view location. 795 float endScaleX = ((float) endRect.width()) / appBounds.width(); 796 mScaleX = new FloatProp(1f, endScaleX, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, 797 FAST_OUT_SLOW_IN_1); 798 float endScaleY = ((float) endRect.height()) / appBounds.height(); 799 mScaleY = new FloatProp(1f, endScaleY, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, 800 FAST_OUT_SLOW_IN_1); 801 float endTranslationX = endRect.left - 802 (appBounds.width() - thumbnailView.getWidth()) / 2.0f; 803 mTranslationX = new FloatProp(0, endTranslationX, 0, 804 REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_1); 805 float endTranslationY = endRect.top - 806 (appBounds.height() - thumbnailView.getHeight()) / 2.0f; 807 mTranslationY = new FloatProp(0, endTranslationY, 0, 808 REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_2); 809 mAlpha = new FloatProp(1.0f, 0, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, 810 ACCEL_2); 811 } 812 813 @Override 814 public void onUpdate(float percent) { 815 Matrix m = new Matrix(); 816 m.preScale(mScaleX.value, mScaleY.value, 817 appBounds.width() / 2.0f, appBounds.height() / 2.0f); 818 m.postTranslate(mTranslationX.value, mTranslationY.value); 819 appMatrix.preConcat(m); 820 params[1] = new SurfaceParams(appTarget.leash, mAlpha.value, appMatrix, 821 null /* windowCrop */, getLayer(appTarget, boostedMode), 822 0 /* cornerRadius */); 823 surfaceApplier.scheduleApply(params); 824 825 m.mapRect(currentAppRect, new RectF(appBounds)); 826 setViewToRect(thumbnailView, new RectF(endRect), currentAppRect); 827 appMatrix.reset(); 828 } 829 }); 830 remoteAppAnim.addListener(new AnimatorListenerAdapter() { 831 @Override 832 public void onAnimationEnd(Animator animation) { 833 thumbnailView.setTranslationY(0); 834 thumbnailView.setTranslationX(0); 835 thumbnailView.setScaleX(1); 836 thumbnailView.setScaleY(1); 837 } 838 }); 839 anim.play(remoteAppAnim); 840 } 841 842 /** 843 * Play task list fade in animation as part of remote app to recents animation. This animation 844 * ensures that the task views in the recents list fade in from bottom to top. 845 * 846 * @param anim animator set to play on 847 * @param appTaskView the task view associated with the remote app closing 848 */ playRemoteTaskListFadeIn(@onNull AnimatorSet anim, @NonNull TaskItemView appTaskView)849 private void playRemoteTaskListFadeIn(@NonNull AnimatorSet anim, 850 @NonNull TaskItemView appTaskView) { 851 long delay = REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY; 852 int childCount = mTaskRecyclerView.getChildCount(); 853 for (int i = 0; i < childCount; i++) { 854 ValueAnimator fadeAnim = ValueAnimator.ofFloat(0, 1.0f); 855 fadeAnim.setDuration(REMOTE_TO_RECENTS_ITEM_FADE_DURATION).setInterpolator(OUT_SLOW_IN); 856 fadeAnim.setStartDelay(delay); 857 View view = mTaskRecyclerView.getChildAt(i); 858 if (Objects.equals(view, appTaskView)) { 859 // Only animate icon and text for the view with snapshot animating in 860 final View icon = appTaskView.getIconView(); 861 final View label = appTaskView.getLabelView(); 862 863 icon.setAlpha(0.0f); 864 label.setAlpha(0.0f); 865 866 fadeAnim.addUpdateListener(alphaVal -> { 867 float val = alphaVal.getAnimatedFraction(); 868 869 icon.setAlpha(val); 870 label.setAlpha(val); 871 }); 872 fadeAnim.addListener(new AnimatorListenerAdapter() { 873 @Override 874 public void onAnimationEnd(Animator animation) { 875 icon.setAlpha(1.0f); 876 label.setAlpha(1.0f); 877 } 878 }); 879 } else { 880 // Otherwise, fade in the entire view. 881 view.setAlpha(0.0f); 882 fadeAnim.addUpdateListener(alphaVal -> { 883 float val = alphaVal.getAnimatedFraction(); 884 view.setAlpha(val); 885 }); 886 fadeAnim.addListener(new AnimatorListenerAdapter() { 887 @Override 888 public void onAnimationEnd(Animator animation) { 889 view.setAlpha(1.0f); 890 } 891 }); 892 } 893 anim.play(fadeAnim); 894 895 int itemType = mTaskRecyclerView.getChildViewHolder(view).getItemViewType(); 896 if (itemType == ITEM_TYPE_CLEAR_ALL) { 897 // Don't add delay. Clear all should animate at same time as next view. 898 continue; 899 } 900 delay += REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY; 901 } 902 } 903 904 /** 905 * Set view properties so that the view fits to the target rect. 906 * 907 * @param view view to set 908 * @param origRect original rect that view was located 909 * @param targetRect rect to set to 910 */ setViewToRect(View view, RectF origRect, RectF targetRect)911 private void setViewToRect(View view, RectF origRect, RectF targetRect) { 912 float dX = targetRect.left - origRect.left; 913 float dY = targetRect.top - origRect.top; 914 view.setTranslationX(dX); 915 view.setTranslationY(dY); 916 917 float scaleX = targetRect.width() / origRect.width(); 918 float scaleY = targetRect.height() / origRect.height(); 919 view.setPivotX(0); 920 view.setPivotY(0); 921 view.setScaleX(scaleX); 922 view.setScaleY(scaleY); 923 } 924 925 @Override setInsets(Rect insets)926 public void setInsets(Rect insets) { 927 mInsets = insets; 928 mTaskRecyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom); 929 mTaskRecyclerView.invalidateItemDecorations(); 930 if (mInsets.top != 0) { 931 updateStatusBarScrim(); 932 } 933 } 934 935 /** 936 * Callback for when this view is ready for a remote animation from app to overview. 937 */ 938 public interface onReadyForRemoteAnimCallback { 939 onReadyForRemoteAnim()940 void onReadyForRemoteAnim(); 941 } 942 } 943