Merge changes I0ab00c03,Ib4e83e36,I72b17568,If4ea3575,If63bf25f, ... into ub-launcher3-qt-dev

* changes:
  Clear all button should not be swipable
  Use custom drawable for Recents Go thumbnails (1/2)
  Add clear all view that scales off device height
  Ensure recents scrolled to first task in landscape
  Enable landscape mode for Recents Go.
  Remove old recyclerview layout logic
  Calculate task height directly off portrait height
  Fix task adapter notify merge conflict
  Move clear all to recycler view (2/2)
  Move clear all to recycler view (1/2)
  Only switch item animator on content fill if needed
  Fix NPE and make getTask return Optional
  Fix ViewTreeObserver crash on Recents Go
  Fix snapshots not updating on app => overview.
  Fix views not being visible on Recents Go.
  Smarter task laying out based off onMeasure
  Change layout size to be dependent on device size
  Update Go recents visibility based off adapter
  Animate content fill animation to Recents Go
  Animate to bottom view in app => overview
  Differentiate empty UI from default in recents Go
  Add task content animation property
  Fix transition progress not applying to drawables
  Change layout anim from Animation => Animator
  Add drawable for default thumbnail for Recents Go
This commit is contained in:
TreeHugger Robot
2019-04-23 19:36:48 +00:00
committed by Android (Google) Code Review
20 changed files with 1116 additions and 213 deletions
@@ -14,13 +14,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<dimen name="task_item_half_vert_margin">8dp</dimen>
<dimen name="task_thumbnail_and_icon_view_size">60dp</dimen>
<dimen name="task_thumbnail_height">60dp</dimen>
<dimen name="task_thumbnail_width">36dp</dimen>
<dimen name="task_icon_size">36dp</dimen>
<dimen name="clear_all_button_width">106dp</dimen>
<dimen name="clear_all_button_height">36dp</dimen>
</resources>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray"/>
<corners android:radius="2dp"/>
</shape>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.quickstep.views.ClearAllItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/clear_all_item_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/clear_all_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/clear_all_button"
android:gravity="center"
android:text="@string/recents_clear_all"
android:textAllCaps="false"
android:textColor="@color/clear_all_button_text"
android:textSize="14sp"/>
</com.android.quickstep.views.ClearAllItemView>
@@ -19,31 +19,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/recent_task_content_view"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_task_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recent_task_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:scrollbars="none"/>
<Button
android:id="@+id/clear_all_button"
android:layout_width="@dimen/clear_all_button_width"
android:layout_height="@dimen/clear_all_button_height"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="@dimen/task_item_half_vert_margin"
android:background="@drawable/clear_all_button"
android:gravity="center"
android:text="@string/recents_clear_all"
android:textAllCaps="false"
android:textColor="@color/clear_all_button_text"
android:textSize="14sp"/>
</LinearLayout>
android:scrollbars="none"/>
<TextView
android:id="@+id/recent_task_empty_view"
android:layout_width="match_parent"
+10 -13
View File
@@ -17,26 +17,23 @@
<com.android.quickstep.views.TaskItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
<com.android.quickstep.views.TaskThumbnailIconView
android:id="@+id/task_icon_and_thumbnail"
android:layout_width="@dimen/task_thumbnail_and_icon_view_size"
android:layout_height="@dimen/task_thumbnail_and_icon_view_size"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="@dimen/task_item_half_vert_margin">
android:layout_marginTop="16dp">
<ImageView
android:id="@+id/task_thumbnail"
android:layout_width="@dimen/task_thumbnail_width"
android:layout_height="@dimen/task_thumbnail_height"
android:layout_gravity="top|start"/>
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/task_icon"
android:layout_width="@dimen/task_icon_size"
android:layout_height="@dimen/task_icon_size"
android:layout_gravity="bottom|end"/>
</FrameLayout>
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.android.quickstep.views.TaskThumbnailIconView>
<TextView
android:id="@+id/task_label"
android:layout_width="wrap_content"
@@ -18,6 +18,7 @@ package com.android.launcher3.uioverrides.states;
import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -51,6 +52,9 @@ public class OverviewState extends LauncherState {
public void onStateEnabled(Launcher launcher) {
IconRecentsView recentsView = launcher.getOverviewPanel();
recentsView.onBeginTransitionToOverview();
// Request orientation be set to unspecified, letting the system decide the best
// orientation.
launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
}
@Override
@@ -131,10 +131,11 @@ final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> imple
return anim;
}
View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
View thumbnailView = mRecentsView.getBottomThumbnailView();
if (thumbnailView == null) {
// TODO: We should either 1) guarantee the view is loaded before attempting this
// or 2) have a backup animation.
// This can be null if there were previously 0 tasks and the recycler view has not had
// enough time to take in the data change, bind a new view, and lay out the new view.
// TODO: Have a fallback to animate to
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
/**
* Holder for clear all button view in task recycler view.
*/
final class ClearAllHolder extends ViewHolder {
public ClearAllHolder(@NonNull View itemView) {
super(itemView);
}
}
@@ -0,0 +1,276 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep;
import static android.view.View.ALPHA;
import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.recyclerview.widget.SimpleItemAnimator;
import com.android.quickstep.views.TaskItemView;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* An item animator that is only set and used for the transition from the empty loading UI to
* the filled task content UI. The animation starts from the bottom to top, changing all valid
* empty item views to be filled and removing all extra empty views.
*/
public final class ContentFillItemAnimator extends SimpleItemAnimator {
private static final class PendingAnimation {
ViewHolder viewHolder;
int animType;
PendingAnimation(ViewHolder vh, int type) {
viewHolder = vh;
animType = type;
}
}
private static final int ANIM_TYPE_REMOVE = 0;
private static final int ANIM_TYPE_CHANGE = 1;
private static final int ITEM_BETWEEN_DELAY = 40;
private static final int ITEM_CHANGE_DURATION = 150;
private static final int ITEM_REMOVE_DURATION = 150;
/**
* Animations that have been registered to occur together at the next call of
* {@link #runPendingAnimations()} but have not started.
*/
private final ArrayList<PendingAnimation> mPendingAnims = new ArrayList<>();
/**
* Animations that have started and are running.
*/
private final ArrayList<ObjectAnimator> mRunningAnims = new ArrayList<>();
private Runnable mOnFinishRunnable;
/**
* Set runnable to run after the content fill animation is fully completed.
*
* @param runnable runnable to run on end
*/
public void setOnAnimationFinishedRunnable(Runnable runnable) {
mOnFinishRunnable = runnable;
}
@Override
public void setChangeDuration(long changeDuration) {
throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ "duration changed.");
}
@Override
public void setRemoveDuration(long removeDuration) {
throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+ "duration changed.");
}
@Override
public boolean animateRemove(ViewHolder holder) {
PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
mPendingAnims.add(pendAnim);
return true;
}
private void animateRemoveImpl(ViewHolder holder, long startDelay) {
final View view = holder.itemView;
if (holder.itemView.getAlpha() == 0) {
// View is already visually removed. We can just get rid of it now.
view.setAlpha(1.0f);
dispatchRemoveFinished(holder);
dispatchFinishedWhenDone();
return;
}
final ObjectAnimator anim = ObjectAnimator.ofFloat(
holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
anim.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(Animator animation) {
view.setAlpha(1);
dispatchRemoveFinished(holder);
mRunningAnims.remove(anim);
dispatchFinishedWhenDone();
}
}
);
anim.start();
mRunningAnims.add(anim);
}
@Override
public boolean animateAdd(ViewHolder holder) {
dispatchAddFinished(holder);
return false;
}
@Override
public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
int toY) {
dispatchMoveFinished(holder);
return false;
}
@Override
public boolean animateChange(ViewHolder oldHolder,
ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
// Only support changes where the holders are the same
if (oldHolder == newHolder) {
PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
mPendingAnims.add(pendAnim);
return true;
}
dispatchChangeFinished(oldHolder, true /* oldItem */);
dispatchChangeFinished(newHolder, false /* oldItem */);
return false;
}
private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
TaskItemView itemView = (TaskItemView) viewHolder.itemView;
final ObjectAnimator anim =
ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
anim.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchChangeStarting(viewHolder, true /* oldItem */);
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchChangeFinished(viewHolder, true /* oldItem */);
mRunningAnims.remove(anim);
dispatchFinishedWhenDone();
}
}
);
anim.start();
mRunningAnims.add(anim);
}
@Override
public void runPendingAnimations() {
// Run animations bottom to top.
mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
int delay = 0;
while (!mPendingAnims.isEmpty()) {
PendingAnimation curAnim = mPendingAnims.remove(0);
ViewHolder vh = curAnim.viewHolder;
switch (curAnim.animType) {
case ANIM_TYPE_REMOVE:
animateRemoveImpl(vh, delay);
break;
case ANIM_TYPE_CHANGE:
animateChangeImpl(vh, delay);
break;
default:
break;
}
delay += ITEM_BETWEEN_DELAY;
}
}
@Override
public void endAnimation(@NonNull ViewHolder item) {
for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
PendingAnimation pendAnim = mPendingAnims.get(i);
if (pendAnim.viewHolder == item) {
mPendingAnims.remove(i);
switch (pendAnim.animType) {
case ANIM_TYPE_REMOVE:
dispatchRemoveFinished(item);
break;
case ANIM_TYPE_CHANGE:
dispatchChangeFinished(item, true /* oldItem */);
break;
default:
break;
}
}
}
dispatchFinishedWhenDone();
}
@Override
public void endAnimations() {
for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
PendingAnimation pendAnim = mPendingAnims.get(i);
ViewHolder item = pendAnim.viewHolder;
switch (pendAnim.animType) {
case ANIM_TYPE_REMOVE:
dispatchRemoveFinished(item);
break;
case ANIM_TYPE_CHANGE:
dispatchChangeFinished(item, true /* oldItem */);
break;
default:
break;
}
mPendingAnims.remove(i);
}
for (int i = 0; i < mRunningAnims.size(); i++) {
ObjectAnimator anim = mRunningAnims.get(i);
anim.end();
}
dispatchAnimationsFinished();
}
@Override
public boolean isRunning() {
return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
}
@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
if (!payloads.isEmpty()
&& (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
return true;
}
return super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
if (mOnFinishRunnable != null) {
mOnFinishRunnable.run();
}
}
}
}
@@ -15,6 +15,8 @@
*/
package com.android.quickstep;
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
import android.app.ActivityOptions;
import android.view.View;
@@ -42,7 +44,7 @@ public final class TaskActionController {
* @param viewHolder the task view holder to launch
*/
public void launchTask(TaskHolder viewHolder) {
if (viewHolder.getTask() == null) {
if (!viewHolder.getTask().isPresent()) {
return;
}
TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
@@ -53,8 +55,9 @@ public final class TaskActionController {
int height = v.getMeasuredHeight();
ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
opts, null /* resultCallback */, null /* resultCallbackHandler */);
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
viewHolder.getTask().get().key, opts, null /* resultCallback */,
null /* resultCallbackHandler */);
}
/**
@@ -63,11 +66,11 @@ public final class TaskActionController {
* @param viewHolder the task view holder to remove
*/
public void removeTask(TaskHolder viewHolder) {
if (viewHolder.getTask() == null) {
if (!viewHolder.getTask().isPresent()) {
return;
}
int position = viewHolder.getAdapterPosition();
Task task = viewHolder.getTask();
Task task = viewHolder.getTask().get();
ActivityManagerWrapper.getInstance().removeTask(task.key.id);
mLoader.removeTask(task);
mAdapter.notifyItemRemoved(position);
@@ -80,6 +83,6 @@ public final class TaskActionController {
int count = mAdapter.getItemCount();
ActivityManagerWrapper.getInstance().removeAllRecentTasks();
mLoader.clearAllTasks();
mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count);
mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
}
}
@@ -15,13 +15,15 @@
*/
package com.android.quickstep;
import android.util.ArrayMap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.R;
import com.android.quickstep.views.TaskItemView;
@@ -29,18 +31,25 @@ import com.android.systemui.shared.recents.model.Task;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
* appropriate {@link Task} from the recents task list.
*/
public final class TaskAdapter extends Adapter<TaskHolder> {
public final class TaskAdapter extends Adapter<ViewHolder> {
public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
public static final int MAX_TASKS_TO_DISPLAY = 6;
public static final int TASKS_START_POSITION = 1;
public static final int ITEM_TYPE_TASK = 0;
public static final int ITEM_TYPE_CLEAR_ALL = 1;
private static final int MAX_TASKS_TO_DISPLAY = 6;
private static final String TAG = "TaskAdapter";
private final TaskListLoader mLoader;
private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
private TaskActionController mTaskActionController;
private OnClickListener mClearAllListener;
private boolean mIsShowingLoadingUi;
public TaskAdapter(@NonNull TaskListLoader loader) {
@@ -51,6 +60,10 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
mTaskActionController = taskActionController;
}
public void setOnClearAllClickListener(OnClickListener listener) {
mClearAllListener = listener;
}
/**
* Sets all positions in the task adapter to loading views, binding new views if necessary.
* This changes the task adapter's view of the data, so the appropriate notify events should be
@@ -63,75 +76,103 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
mIsShowingLoadingUi = isShowingLoadingUi;
}
/**
* Get task item view for a given task id if it's attached to the view.
*
* @param taskId task id to search for
* @return corresponding task item view if it's attached, null otherwise
*/
public @Nullable TaskItemView getTaskItemView(int taskId) {
return mTaskIdToViewMap.get(taskId);
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case ITEM_TYPE_TASK:
TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.task_item_view, parent, false);
TaskHolder taskHolder = new TaskHolder(itemView);
itemView.setOnClickListener(view -> mTaskActionController.launchTask(taskHolder));
return taskHolder;
case ITEM_TYPE_CLEAR_ALL:
View clearView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.clear_all_button, parent, false);
ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
clearViewButton.setOnClickListener(mClearAllListener);
return clearAllHolder;
default:
throw new IllegalArgumentException("No known holder for item type: " + viewType);
}
}
@Override
public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.task_item_view, parent, false);
TaskHolder holder = new TaskHolder(itemView);
itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
return holder;
public void onBindViewHolder(ViewHolder holder, int position) {
onBindViewHolderInternal(holder, position, false /* willAnimate */);
}
@Override
public void onBindViewHolder(TaskHolder holder, int position) {
if (mIsShowingLoadingUi) {
holder.bindEmptyUi();
public void onBindViewHolder(@NonNull ViewHolder holder, int position,
@NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
return;
}
List<Task> tasks = mLoader.getCurrentTaskList();
if (position >= tasks.size()) {
// Task list has updated.
return;
int changeType = (int) payloads.get(0);
if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
// Bind in preparation for animation
onBindViewHolderInternal(holder, position, true /* willAnimate */);
} else {
throw new IllegalArgumentException("Payload content is not a valid change event type: "
+ changeType);
}
}
private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
boolean willAnimate) {
int itemType = getItemViewType(position);
switch (itemType) {
case ITEM_TYPE_TASK:
TaskHolder taskHolder = (TaskHolder) holder;
if (mIsShowingLoadingUi) {
taskHolder.bindEmptyUi();
return;
}
List<Task> tasks = mLoader.getCurrentTaskList();
int taskPos = position - TASKS_START_POSITION;
if (taskPos >= tasks.size()) {
// Task list has updated.
return;
}
Task task = tasks.get(taskPos);
taskHolder.bindTask(task, willAnimate /* willAnimate */);
mLoader.loadTaskIconAndLabel(task, () -> {
// Ensure holder still has the same task.
if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
taskHolder.getTaskItemView().setIcon(task.icon);
taskHolder.getTaskItemView().setLabel(task.titleDescription);
}
});
mLoader.loadTaskThumbnail(task, () -> {
if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
taskHolder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
}
});
break;
case ITEM_TYPE_CLEAR_ALL:
// Nothing to bind.
break;
default:
throw new IllegalArgumentException("No known holder for item type: " + itemType);
}
Task task = tasks.get(position);
holder.bindTask(task);
mLoader.loadTaskIconAndLabel(task, () -> {
// Ensure holder still has the same task.
if (Objects.equals(task, holder.getTask())) {
holder.getTaskItemView().setIcon(task.icon);
holder.getTaskItemView().setLabel(task.titleDescription);
}
});
mLoader.loadTaskThumbnail(task, () -> {
if (Objects.equals(task, holder.getTask())) {
holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
}
});
}
@Override
public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
if (holder.getTask() == null) {
return;
}
mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
}
@Override
public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
if (holder.getTask() == null) {
return;
}
mTaskIdToViewMap.remove(holder.getTask().key.id);
public int getItemViewType(int position) {
// Bottom is always clear all button.
return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
}
@Override
public int getItemCount() {
int itemCount = TASKS_START_POSITION;
if (mIsShowingLoadingUi) {
// Show loading version of all items.
return MAX_TASKS_TO_DISPLAY;
itemCount += MAX_TASKS_TO_DISPLAY;
} else {
return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
}
return itemCount;
}
}
@@ -15,12 +15,16 @@
*/
package com.android.quickstep;
import androidx.annotation.Nullable;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.quickstep.views.TaskItemView;
import com.android.systemui.shared.recents.model.Task;
import java.util.Optional;
/**
* A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
* etc.) to the view.
@@ -40,13 +44,28 @@ public final class TaskHolder extends ViewHolder {
}
/**
* Bind a task to the holder, resetting the view and preparing it for content to load in.
* Bind the task model to the holder. This will take the current task content in the task
* object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
* the content to animate to at a later time. If the task does not have all its content loaded,
* the view will prepare appropriate default placeholders and it is the callers responsibility
* to change them at a later time.
*
* Regardless of whether it is animating, input handlers will be bound immediately (see
* {@link TaskActionController}).
*
* @param task the task to bind to the view
* @param willAnimate true if UI should animate in later, false if it should apply immediately
*/
public void bindTask(Task task) {
public void bindTask(@NonNull Task task, boolean willAnimate) {
mTask = task;
mTaskItemView.resetTaskItemView();
Bitmap thumbnail = (task.thumbnail != null) ? task.thumbnail.thumbnail : null;
if (willAnimate) {
mTaskItemView.startContentAnimation(task.icon, thumbnail, task.titleDescription);
} else {
mTaskItemView.setIcon(task.icon);
mTaskItemView.setThumbnail(thumbnail);
mTaskItemView.setLabel(task.titleDescription);
}
}
/**
@@ -55,10 +74,7 @@ public final class TaskHolder extends ViewHolder {
*/
public void bindEmptyUi() {
mTask = null;
// TODO: Set the task view to a loading, empty UI.
// Temporarily using the one below for visual confirmation but should be swapped out to new
// UI later.
mTaskItemView.resetTaskItemView();
mTaskItemView.resetToEmptyUi();
}
/**
@@ -66,7 +82,7 @@ public final class TaskHolder extends ViewHolder {
*
* @return the current task
*/
public @Nullable Task getTask() {
return mTask;
public Optional<Task> getTask() {
return Optional.ofNullable(mTask);
}
}
@@ -38,23 +38,9 @@ public final class TaskListLoader {
private ArrayList<Task> mTaskList = new ArrayList<>();
private int mTaskListChangeId;
private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
Task foundTask = null;
for (Task task : mTaskList) {
if (task.key.id == taskId) {
foundTask = task;
break;
}
}
if (foundTask != null) {
foundTask.thumbnail = thumbnailData;
}
return foundTask;
};
public TaskListLoader(Context context) {
mRecentsModel = RecentsModel.INSTANCE.get(context);
mRecentsModel.addThumbnailChangeListener(listener);
}
/**
@@ -17,6 +17,9 @@ package com.android.quickstep;
import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -45,4 +48,14 @@ public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
mTaskActionController.removeTask((TaskHolder) viewHolder);
}
}
@Override
public int getSwipeDirs(@NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
// Clear all button should not be swipable.
return 0;
}
return super.getSwipeDirs(recyclerView, viewHolder);
}
}
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import com.android.systemui.shared.recents.model.ThumbnailData;
/**
* Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
* orientation the thumbnail was taken in matches the desired orientation. In addition, the
* thumbnail always fills into the containing bounds.
*/
public final class ThumbnailDrawable extends Drawable {
private final Paint mPaint = new Paint();
private final Matrix mMatrix = new Matrix();
private final ThumbnailData mThumbnailData;
private int mRequestedOrientation;
public ThumbnailDrawable(@NonNull ThumbnailData thumbnailData, int requestedOrientation) {
mThumbnailData = thumbnailData;
mRequestedOrientation = requestedOrientation;
updateMatrix();
}
/**
* Set the requested orientation.
*
* @param orientation the orientation we want the thumbnail to be in
*/
public void setRequestedOrientation(int orientation) {
if (mRequestedOrientation != orientation) {
mRequestedOrientation = orientation;
updateMatrix();
}
}
@Override
public void draw(Canvas canvas) {
if (mThumbnailData.thumbnail == null) {
return;
}
canvas.drawBitmap(mThumbnailData.thumbnail, mMatrix, mPaint);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
updateMatrix();
}
@Override
public void setAlpha(int alpha) {
final int oldAlpha = mPaint.getAlpha();
if (alpha != oldAlpha) {
mPaint.setAlpha(alpha);
invalidateSelf();
}
}
@Override
public int getAlpha() {
return mPaint.getAlpha();
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
invalidateSelf();
}
@Override
public ColorFilter getColorFilter() {
return mPaint.getColorFilter();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
private void updateMatrix() {
if (mThumbnailData.thumbnail == null) {
return;
}
mMatrix.reset();
float scaleX;
float scaleY;
Rect bounds = getBounds();
Bitmap thumbnail = mThumbnailData.thumbnail;
if (mRequestedOrientation != mThumbnailData.orientation) {
// Rotate and translate so that top left is the same.
mMatrix.postRotate(90, 0, 0);
mMatrix.postTranslate(thumbnail.getHeight(), 0);
scaleX = (float) bounds.width() / thumbnail.getHeight();
scaleY = (float) bounds.height() / thumbnail.getWidth();
} else {
scaleX = (float) bounds.width() / thumbnail.getWidth();
scaleY = (float) bounds.height() / thumbnail.getHeight();
}
// Scale to fill.
mMatrix.postScale(scaleX, scaleY);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.views;
import static com.android.quickstep.views.TaskLayoutUtils.getClearAllItemHeight;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* Recycler view item that lays out the clear all button and measures the space it takes based on
* the device height.
*/
public final class ClearAllItemView extends FrameLayout {
public ClearAllItemView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int buttonHeight = getClearAllItemHeight(getContext());
int newHeightSpec = MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, newHeightSpec);
}
}
@@ -15,8 +15,13 @@
*/
package com.android.quickstep.views;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -24,30 +29,37 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View;
import android.view.ViewDebug;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.LayoutAnimationController;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.R;
import com.android.quickstep.ContentFillItemAnimator;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.RecentsToActivityHelper;
import com.android.quickstep.TaskActionController;
import com.android.quickstep.TaskAdapter;
import com.android.quickstep.TaskHolder;
import com.android.quickstep.TaskListLoader;
import com.android.quickstep.TaskSwipeCallback;
import com.android.systemui.shared.recents.model.Task;
import java.util.ArrayList;
import java.util.Optional;
/**
* Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
@@ -89,24 +101,48 @@ public final class IconRecentsView extends FrameLayout {
private final Context mContext;
private final TaskListLoader mTaskLoader;
private final TaskAdapter mTaskAdapter;
private final LinearLayoutManager mTaskLayoutManager;
private final TaskActionController mTaskActionController;
private final LayoutAnimationController mLayoutAnimation;
private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
private final ContentFillItemAnimator mLoadingContentItemAnimator =
new ContentFillItemAnimator();
private RecentsToActivityHelper mActivityHelper;
private RecyclerView mTaskRecyclerView;
private View mShowingContentView;
private View mEmptyView;
private View mContentView;
private View mClearAllView;
private boolean mTransitionedFromApp;
private AnimatorSet mLayoutAnimation;
private final ArraySet<View> mLayingOutViews = new ArraySet<>();
private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
ArrayList<TaskItemView> itemViews = getTaskViews();
for (int i = 0, size = itemViews.size(); i < size; i++) {
TaskItemView taskView = itemViews.get(i);
TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
Optional<Task> optTask = taskHolder.getTask();
if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
Task task = optTask.get();
// Update thumbnail on the task.
task.thumbnail = thumbnailData;
taskView.setThumbnail(thumbnailData.thumbnail);
return task;
}
}
return null;
};
public IconRecentsView(Context context, AttributeSet attrs) {
super(context, attrs);
BaseActivity activity = BaseActivity.fromContext(context);
mContext = context;
mTaskLoader = new TaskListLoader(mContext);
mTaskAdapter = new TaskAdapter(mTaskLoader);
mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
mTaskAdapter.setActionController(mTaskActionController);
mLayoutAnimation = createLayoutAnimation();
mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
}
@Override
@@ -115,15 +151,30 @@ public final class IconRecentsView extends FrameLayout {
if (mTaskRecyclerView == null) {
mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
mTaskRecyclerView.setAdapter(mTaskAdapter);
mTaskRecyclerView.setLayoutManager(
new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
ItemTouchHelper helper = new ItemTouchHelper(
new TaskSwipeCallback(mTaskActionController));
helper.attachToRecyclerView(mTaskRecyclerView);
mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
mTaskRecyclerView.addOnChildAttachStateChangeListener(
new OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
// Child view was added that is not part of current layout animation
// so restart the animation.
animateFadeInLayoutAnimation();
}
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) { }
});
mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
() -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
mEmptyView = findViewById(R.id.recent_task_empty_view);
mContentView = findViewById(R.id.recent_task_content_view);
mContentView = mTaskRecyclerView;
mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onChanged() {
@@ -135,19 +186,17 @@ public final class IconRecentsView extends FrameLayout {
updateContentViewVisibility();
}
});
mClearAllView = findViewById(R.id.clear_all_button);
mClearAllView.setOnClickListener(v -> animateClearAllTasks());
// TODO: Move layout param logic into onMeasure
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
TaskItemView[] itemViews = getTaskViews();
for (TaskItemView itemView : itemViews) {
itemView.setEnabled(enabled);
int childCount = mTaskRecyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
}
mClearAllView.setEnabled(enabled);
}
/**
@@ -165,8 +214,13 @@ public final class IconRecentsView extends FrameLayout {
* becomes visible.
*/
public void onBeginTransitionToOverview() {
mTaskRecyclerView.scheduleLayoutAnimation();
if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
// Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
// all tasks should be visible to fill up the screen in portrait mode and the view will
// not be scrollable.
mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
}
scheduleFadeInLayoutAnimation();
// Load any task changes
if (!mTaskLoader.needsToLoad()) {
return;
@@ -174,9 +228,24 @@ public final class IconRecentsView extends FrameLayout {
mTaskAdapter.setIsShowingLoadingUi(true);
mTaskAdapter.notifyDataSetChanged();
mTaskLoader.loadTaskList(tasks -> {
int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
mTaskAdapter.setIsShowingLoadingUi(false);
// TODO: Animate the loading UI out and the loaded data in.
mTaskAdapter.notifyDataSetChanged();
int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
if (numEmptyItems < numActualItems) {
throw new IllegalStateException("There are less empty item views than the number "
+ "of items to animate to.");
}
// Possible that task list loads faster than adapter changes propagate to layout so
// only start content fill animation if there aren't any pending adapter changes.
if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
// Set item animator for content filling animation. The item animator will switch
// back to the default on completion
mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
}
mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
numEmptyItems - numActualItems);
mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
});
}
@@ -194,35 +263,37 @@ public final class IconRecentsView extends FrameLayout {
* the app. In that case, we launch the next most recent.
*/
public void handleOverviewCommand() {
int childCount = mTaskRecyclerView.getChildCount();
if (childCount == 0) {
// TODO(130735711): Need to address case where most recent task is off screen/unattached.
ArrayList<TaskItemView> taskViews = getTaskViews();
int taskViewsSize = taskViews.size();
if (taskViewsSize <= 1) {
// Do nothing
return;
}
TaskHolder taskToLaunch;
if (mTransitionedFromApp && childCount > 1) {
if (mTransitionedFromApp && taskViewsSize > 1) {
// Launch the next most recent app
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1);
TaskItemView itemView = taskViews.get(1);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
} else {
// Launch the most recent app
TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0);
TaskItemView itemView = taskViews.get(0);
taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
}
mTaskActionController.launchTask(taskToLaunch);
}
/**
* Get the thumbnail view associated with a task for the purposes of animation.
* Get the bottom most thumbnail view to animate to.
*
* @param taskId task id of thumbnail view to get
* @return the thumbnail view for the task if attached, null otherwise
* @return the thumbnail view if laid out
*/
public @Nullable View getThumbnailViewForTask(int taskId) {
TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
if (view == null) {
public @Nullable View getBottomThumbnailView() {
ArrayList<TaskItemView> taskViews = getTaskViews();
if (taskViews.isEmpty()) {
return null;
}
TaskItemView view = taskViews.get(0);
return view.getThumbnailView();
}
@@ -231,13 +302,14 @@ public final class IconRecentsView extends FrameLayout {
*/
private void animateClearAllTasks() {
setEnabled(false);
TaskItemView[] itemViews = getTaskViews();
ArrayList<TaskItemView> itemViews = getTaskViews();
AnimatorSet clearAnim = new AnimatorSet();
long currentDelay = 0;
// Animate each item view to the right and fade out.
for (TaskItemView itemView : itemViews) {
for (int i = 0, size = itemViews.size(); i < size; i++) {
TaskItemView itemView = itemViews.get(i);
PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
@@ -272,7 +344,8 @@ public final class IconRecentsView extends FrameLayout {
clearAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
for (TaskItemView itemView : itemViews) {
for (int i = 0, size = itemViews.size(); i < size; i++) {
TaskItemView itemView = itemViews.get(i);
itemView.setTranslationX(0);
itemView.setAlpha(1.0f);
}
@@ -287,13 +360,16 @@ public final class IconRecentsView extends FrameLayout {
/**
* Get attached task item views ordered by most recent.
*
* @return array of attached task item views
* @return array list of attached task item views
*/
private TaskItemView[] getTaskViews() {
private ArrayList<TaskItemView> getTaskViews() {
int taskCount = mTaskRecyclerView.getChildCount();
TaskItemView[] itemViews = new TaskItemView[taskCount];
ArrayList<TaskItemView> itemViews = new ArrayList<>();
for (int i = 0; i < taskCount; i ++) {
itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
View child = mTaskRecyclerView.getChildAt(i);
if (child instanceof TaskItemView) {
itemViews.add((TaskItemView) child);
}
}
return itemViews;
}
@@ -303,12 +379,14 @@ public final class IconRecentsView extends FrameLayout {
* of tasks.
*/
private void updateContentViewVisibility() {
int taskListSize = mTaskLoader.getCurrentTaskList().size();
if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) {
int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
if (mShowingContentView != mEmptyView && taskListSize == 0) {
mShowingContentView = mEmptyView;
crossfadeViews(mEmptyView, mContentView);
mActivityHelper.leaveRecents();
}
if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) {
if (mShowingContentView != mContentView && taskListSize > 0) {
mShowingContentView = mContentView;
crossfadeViews(mContentView, mEmptyView);
}
}
@@ -320,6 +398,7 @@ public final class IconRecentsView extends FrameLayout {
* @param fadeOutView view that should fade out
*/
private void crossfadeViews(View fadeInView, View fadeOutView) {
fadeInView.animate().cancel();
fadeInView.setVisibility(VISIBLE);
fadeInView.setAlpha(0f);
fadeInView.animate()
@@ -327,6 +406,7 @@ public final class IconRecentsView extends FrameLayout {
.setDuration(CROSSFADE_DURATION)
.setListener(null);
fadeOutView.animate().cancel();
fadeOutView.animate()
.alpha(0f)
.setDuration(CROSSFADE_DURATION)
@@ -338,17 +418,56 @@ public final class IconRecentsView extends FrameLayout {
});
}
private static LayoutAnimationController createLayoutAnimation() {
AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
/**
* Schedule a one-shot layout animation on the next layout. Separate from
* {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
* view properties themselves, allowing more controllable behavior and making it easier to
* manage when the animation conflicts with another animation.
*/
private void scheduleFadeInLayoutAnimation() {
mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
animateFadeInLayoutAnimation();
mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
Animation alphaAnim = new AlphaAnimation(0, 1);
alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
anim.addAnimation(alphaAnim);
LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
layoutAnim.setDelay(
(float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
return layoutAnim;
/**
* Start animating the layout animation where items fade in.
*/
private void animateFadeInLayoutAnimation() {
if (mLayoutAnimation != null) {
// If layout animation still in progress, cancel and restart.
mLayoutAnimation.cancel();
}
ArrayList<TaskItemView> views = getTaskViews();
int delay = 0;
mLayoutAnimation = new AnimatorSet();
for (int i = 0, size = views.size(); i < size; i++) {
TaskItemView view = views.get(i);
view.setAlpha(0.0f);
Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
alphaAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setAlpha(1.0f);
mLayingOutViews.remove(view);
}
});
delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
mLayoutAnimation.play(alphaAnim);
mLayingOutViews.add(view);
}
mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLayoutAnimation = null;
}
});
mLayoutAnimation.start();
}
}
@@ -15,16 +15,21 @@
*/
package com.android.quickstep.views;
import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
@@ -34,16 +39,41 @@ import com.android.launcher3.R;
*/
public final class TaskItemView extends LinearLayout {
private static final String EMPTY_LABEL = "";
private static final String DEFAULT_LABEL = "...";
private final Drawable mDefaultIcon;
private final Drawable mDefaultThumbnail;
private final TaskLayerDrawable mIconDrawable;
private final TaskLayerDrawable mThumbnailDrawable;
private TextView mLabelView;
private ImageView mIconView;
private ImageView mThumbnailView;
private float mContentTransitionProgress;
/**
* Property representing the content transition progress of the view. 1.0f represents that the
* currently bound icon, thumbnail, and label are fully animated in and visible.
*/
public static FloatProperty CONTENT_TRANSITION_PROGRESS =
new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
@Override
public void setValue(TaskItemView view, float progress) {
view.setContentTransitionProgress(progress);
}
@Override
public Float get(TaskItemView view) {
return view.mContentTransitionProgress;
}
};
public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs);
mDefaultIcon = context.getResources().getDrawable(
android.R.drawable.sym_def_app_icon, context.getTheme());
Resources res = context.getResources();
mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
mIconDrawable = new TaskLayerDrawable(context);
mThumbnailDrawable = new TaskLayerDrawable(context);
}
@Override
@@ -52,15 +82,28 @@ public final class TaskItemView extends LinearLayout {
mLabelView = findViewById(R.id.task_label);
mThumbnailView = findViewById(R.id.task_thumbnail);
mIconView = findViewById(R.id.task_icon);
mThumbnailView.setImageDrawable(mThumbnailDrawable);
mIconView.setImageDrawable(mIconDrawable);
resetToEmptyUi();
CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int taskHeight = getTaskHeight(getContext());
int newHeightSpec = MeasureSpec.makeMeasureSpec(taskHeight,MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, newHeightSpec);
}
/**
* Resets task item view to default values.
* Resets task item view to empty, loading UI.
*/
public void resetTaskItemView() {
setLabel(DEFAULT_LABEL);
setIcon(null);
setThumbnail(null);
public void resetToEmptyUi() {
mIconDrawable.resetDrawable();
mThumbnailDrawable.resetDrawable();
setLabel(EMPTY_LABEL);
}
/**
@@ -69,11 +112,8 @@ public final class TaskItemView extends LinearLayout {
* @param label task label
*/
public void setLabel(@Nullable String label) {
if (label == null) {
mLabelView.setText(DEFAULT_LABEL);
return;
}
mLabelView.setText(label);
mLabelView.setText(getSafeLabel(label));
// TODO: Animation for label
}
/**
@@ -86,11 +126,7 @@ public final class TaskItemView extends LinearLayout {
// The icon proper is actually smaller than the drawable and has "padding" on the side for
// the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
// view if we want the icon to be flush with the bottom of the thumbnail.
if (icon == null) {
mIconView.setImageDrawable(mDefaultIcon);
return;
}
mIconView.setImageDrawable(icon);
mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
}
/**
@@ -99,16 +135,48 @@ public final class TaskItemView extends LinearLayout {
* @param thumbnail task thumbnail for the task
*/
public void setThumbnail(@Nullable Bitmap thumbnail) {
if (thumbnail == null) {
mThumbnailView.setImageBitmap(null);
mThumbnailView.setBackgroundColor(Color.GRAY);
return;
}
mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
mThumbnailView.setImageBitmap(thumbnail);
mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnail));
}
public View getThumbnailView() {
return mThumbnailView;
}
/**
* Start a new animation from the current task content to the specified new content. The caller
* is responsible for the actual animation control via the property
* {@link #CONTENT_TRANSITION_PROGRESS}.
*
* @param endIcon the icon to animate to
* @param endThumbnail the thumbnail to animate to
* @param endLabel the label to animate to
*/
public void startContentAnimation(@Nullable Drawable endIcon, @Nullable Bitmap endThumbnail,
@Nullable String endLabel) {
mIconDrawable.startNewTransition(getSafeIcon(endIcon));
mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
// TODO: Animation for label
setContentTransitionProgress(0.0f);
}
private void setContentTransitionProgress(float progress) {
mContentTransitionProgress = progress;
mIconDrawable.setTransitionProgress(progress);
mThumbnailDrawable.setTransitionProgress(progress);
// TODO: Animation for label
}
private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
return (icon != null) ? icon : mDefaultIcon;
}
private @NonNull Drawable getSafeThumbnail(@Nullable Bitmap thumbnail) {
return (thumbnail != null) ? new BitmapDrawable(getResources(), thumbnail)
: mDefaultThumbnail;
}
private @NonNull String getSafeLabel(@Nullable String label) {
return (label != null) ? label : DEFAULT_LABEL;
}
}
@@ -31,6 +31,7 @@ import com.android.launcher3.R;
*/
public final class TaskLayerDrawable extends LayerDrawable {
private final Drawable mEmptyDrawable;
private float mProgress;
public TaskLayerDrawable(Context context) {
super(new Drawable[0]);
@@ -50,6 +51,7 @@ public final class TaskLayerDrawable extends LayerDrawable {
*/
public void setCurrentDrawable(@NonNull Drawable drawable) {
setDrawable(0, drawable);
applyTransitionProgress(mProgress);
}
/**
@@ -82,9 +84,18 @@ public final class TaskLayerDrawable extends LayerDrawable {
if (progress > 1 || progress < 0) {
throw new IllegalArgumentException("Transition progress should be between 0 and 1");
}
mProgress = progress;
applyTransitionProgress(progress);
}
private void applyTransitionProgress(float progress) {
int drawableAlpha = (int) (progress * 255);
getDrawable(0).setAlpha(drawableAlpha);
getDrawable(1).setAlpha(255 - drawableAlpha);
if (getDrawable(0) != getDrawable(1)) {
// Only do this if it's a different drawable so that it fades out.
// Otherwise, we'd just be overwriting the front drawable's alpha.
getDrawable(1).setAlpha(255 - drawableAlpha);
}
invalidateSelf();
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.views;
import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
import android.content.Context;
import com.android.launcher3.InvariantDeviceProfile;
/**
* Utils to determine dynamically task and view sizes based off the device height and width.
*/
public final class TaskLayoutUtils {
private static final float CLEAR_ALL_ITEM_TO_HEIGHT_RATIO = 7.0f / 64;
private TaskLayoutUtils() {}
/**
* Calculate task height based off the available height in portrait mode such that when the
* recents list is full, the total height fills in the available device height perfectly. In
* landscape mode, we keep the same task height so that tasks scroll off the top.
*
* @param context current context
* @return task height
*/
public static int getTaskHeight(Context context) {
final int availableHeight =
InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
final int availableTaskSpace = availableHeight - getClearAllItemHeight(context);
return (int) (availableTaskSpace * 1.0f / MAX_TASKS_TO_DISPLAY);
}
/**
* Calculate clear all item height scaled to available height in portrait mode.
*
* @param context current context
* @return clear all item height
*/
public static int getClearAllItemHeight(Context context) {
final int availableHeight =
InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
return (int) (CLEAR_ALL_ITEM_TO_HEIGHT_RATIO * availableHeight);
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.views;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.R;
/**
* Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
* within the view. Side length is determined by height.
*/
final class TaskThumbnailIconView extends ViewGroup {
private final Rect mTmpFrameRect = new Rect();
private final Rect mTmpChildRect = new Rect();
private View mThumbnailView;
private View mIconView;
private static final float SUBITEM_FRAME_RATIO = .6f;
public TaskThumbnailIconView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mThumbnailView = findViewById(R.id.task_thumbnail);
mIconView = findViewById(R.id.task_icon);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
int width = height;
setMeasuredDimension(width, height);
int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
if (mThumbnailView.getVisibility() != GONE) {
int thumbnailHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
int thumbnailWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
}
if (mIconView.getVisibility() != GONE) {
int iconHeightSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
int iconWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
measureChild(mIconView, iconWidthSpec, iconHeightSpec);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mTmpFrameRect.left = getPaddingLeft();
mTmpFrameRect.right = right - left - getPaddingRight();
mTmpFrameRect.top = getPaddingTop();
mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
// Layout the thumbnail to the top-start corner of the view
if (mThumbnailView.getVisibility() != GONE) {
final int width = mThumbnailView.getMeasuredWidth();
final int height = mThumbnailView.getMeasuredHeight();
final int thumbnailGravity = Gravity.TOP | Gravity.START;
Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
mTmpChildRect.right, mTmpChildRect.bottom);
}
// Layout the icon to the bottom-end corner of the view
if (mIconView.getVisibility() != GONE) {
final int width = mIconView.getMeasuredWidth();
final int height = mIconView.getMeasuredHeight();
int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
mTmpChildRect.right, mTmpChildRect.bottom);
}
}
}