Show ASAP for Recents Go and load content after

Recent UX discussion led to decision to show as soon as we have the list
and order and then load the content in after as soon as it comes. This
introduces API in TaskListLoader to facilitate this and default values
for the view when the content is not yet loaded.

Bug: 114136250
Test: Go to recents, see content load in
Change-Id: I6766cf014e3de78894353614157dbc8798031c2f
This commit is contained in:
Kevin 2019-03-19 16:59:32 -07:00
parent bd9bd70dcb
commit b589241fd6
4 changed files with 116 additions and 69 deletions

View File

@ -75,8 +75,20 @@ public final class TaskAdapter extends Adapter<TaskHolder> {
// Task list has updated.
return;
}
holder.bindTask(tasks.get(position));
Task task = tasks.get(position);
holder.bindTask(task);
mLoader.loadTaskIconAndLabel(task, () -> {
// Ensure holder still has the same task.
if (task.equals(holder.getTask())) {
holder.getTaskItemView().setIcon(task.icon);
holder.getTaskItemView().setLabel(task.titleDescription);
}
});
mLoader.loadTaskThumbnail(task, () -> {
if (task.equals(holder.getTask())) {
holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
}
});
}
@Override

View File

@ -35,17 +35,18 @@ public final class TaskHolder extends ViewHolder {
mTaskItemView = itemView;
}
public TaskItemView getTaskItemView() {
return mTaskItemView;
}
/**
* Bind task content to the view. This includes the task icon and title as well as binding
* input handlers such as which task to launch/remove.
* Bind a task to the holder, resetting the view and preparing it for content to load in.
*
* @param task the task to bind to the view
*/
public void bindTask(Task task) {
mTask = task;
mTaskItemView.setLabel(task.titleDescription);
mTaskItemView.setIcon(task.icon);
mTaskItemView.setThumbnail(task.thumbnail.thumbnail);
mTaskItemView.resetTaskItemView();
}
/**

View File

@ -25,7 +25,6 @@ import com.android.systemui.shared.recents.model.Task;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
@ -39,35 +38,48 @@ 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);
}
/**
* Returns the current task list as of the last completed load (see
* {@link #loadTaskList}) as a read-only list. This list of tasks is guaranteed to always have
* all its task content loaded.
* Returns the current task list as of the last completed load (see {@link #loadTaskList}) as a
* read-only list. This list of tasks is not guaranteed to have all content loaded.
*
* @return the current list of tasks w/ all content loaded
* @return the current list of tasks
*/
public List<Task> getCurrentTaskList() {
return Collections.unmodifiableList(mTaskList);
}
/**
* Fetches the most recent tasks and updates the task list asynchronously. In addition it
* loads the content for each task (icon and label). The callback and task list being updated
* only occur when all task content is fully loaded and up-to-date.
* Fetches the most recent tasks and updates the task list asynchronously. This call does not
* provide guarantees the task content (icon, thumbnail, label) are loaded but will fill in
* what it has. May run the callback immediately if there have been no changes in the task
* list.
*
* @param onTasksLoadedCallback callback for when the tasks are fully loaded. Done on the UI
* thread
* @param onLoadedCallback callback to run when task list is loaded
*/
public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onTasksLoadedCallback) {
public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onLoadedCallback) {
if (mRecentsModel.isTaskListValid(mTaskListChangeId)) {
// Current task list is already up to date. No need to update.
if (onTasksLoadedCallback != null) {
onTasksLoadedCallback.accept(mTaskList);
if (onLoadedCallback != null) {
onLoadedCallback.accept(mTaskList);
}
return;
}
@ -76,15 +88,45 @@ public final class TaskListLoader {
// Reverse tasks to put most recent at the bottom of the view
Collections.reverse(tasks);
// Load task content
loadTaskContents(tasks, () -> {
mTaskList = tasks;
if (onTasksLoadedCallback != null) {
onTasksLoadedCallback.accept(mTaskList);
for (Task task : tasks) {
int loadedPos = mTaskList.indexOf(task);
if (loadedPos == -1) {
continue;
}
});
Task loadedTask = mTaskList.get(loadedPos);
task.icon = loadedTask.icon;
task.titleDescription = loadedTask.titleDescription;
task.thumbnail = loadedTask.thumbnail;
}
mTaskList = tasks;
onLoadedCallback.accept(tasks);
});
}
/**
* Load task icon and label asynchronously if it is not already loaded in the task. If the task
* already has an icon, this calls the callback immediately.
*
* @param task task to update with icon + label
* @param onLoadedCallback callback to run when task has icon and label
*/
public void loadTaskIconAndLabel(Task task, @Nullable Runnable onLoadedCallback) {
mRecentsModel.getIconCache().updateIconInBackground(task,
loadedTask -> onLoadedCallback.run());
}
/**
* Load thumbnail asynchronously if not already loaded in the task. If the task already has a
* thumbnail or if the thumbnail is cached, this calls the callback immediately.
*
* @param task task to update with the thumbnail
* @param onLoadedCallback callback to run when task has thumbnail
*/
public void loadTaskThumbnail(Task task, @Nullable Runnable onLoadedCallback) {
mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
thumbnail -> onLoadedCallback.run());
}
/**
* Removes the task from the current task list.
*/
@ -98,42 +140,4 @@ public final class TaskListLoader {
void clearAllTasks() {
mTaskList.clear();
}
/**
* Loads task content for a list of tasks, including the label, icon, and thumbnail. For content
* that isn't cached, load the content asynchronously in the background.
*
* @param tasksToLoad list of tasks that need to load their content
* @param onFullyLoadedCallback runnable to run after all tasks have loaded their content
*/
private void loadTaskContents(ArrayList<Task> tasksToLoad,
@Nullable Runnable onFullyLoadedCallback) {
// Make two load requests per task, one for the icon/title and one for the thumbnail.
AtomicInteger loadRequestsCount = new AtomicInteger(tasksToLoad.size() * 2);
Runnable itemLoadedRunnable = () -> {
if (loadRequestsCount.decrementAndGet() == 0 && onFullyLoadedCallback != null) {
onFullyLoadedCallback.run();
}
};
for (Task task : tasksToLoad) {
// Load icon and title.
int index = mTaskList.indexOf(task);
if (index >= 0) {
// If we've already loaded the task and have its content then just copy it over.
Task loadedTask = mTaskList.get(index);
task.titleDescription = loadedTask.titleDescription;
task.icon = loadedTask.icon;
itemLoadedRunnable.run();
} else {
// Otherwise, load the content in the background.
mRecentsModel.getIconCache().updateIconInBackground(task,
loadedTask -> itemLoadedRunnable.run());
}
// Load the thumbnail. May return immediately and synchronously if the thumbnail is
// cached.
mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
thumbnail -> itemLoadedRunnable.run());
}
}
}

View File

@ -17,6 +17,7 @@ package com.android.quickstep.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
@ -24,6 +25,8 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
/**
@ -31,12 +34,16 @@ import com.android.launcher3.R;
*/
public final class TaskItemView extends LinearLayout {
private static final String DEFAULT_LABEL = "...";
private final Drawable mDefaultIcon;
private TextView mLabelView;
private ImageView mIconView;
private ImageView mThumbnailView;
public TaskItemView(Context context, AttributeSet attrs) {
super(context, attrs);
mDefaultIcon = context.getResources().getDrawable(
android.R.drawable.sym_def_app_icon, context.getTheme());
}
@Override
@ -48,33 +55,56 @@ public final class TaskItemView extends LinearLayout {
}
/**
* Set the label for the task item.
* Resets task item view to default values.
*/
public void resetTaskItemView() {
setLabel(DEFAULT_LABEL);
setIcon(null);
setThumbnail(null);
}
/**
* Set the label for the task item. Sets to a default label if null.
*
* @param label task label
*/
public void setLabel(String label) {
public void setLabel(@Nullable String label) {
if (label == null) {
mLabelView.setText(DEFAULT_LABEL);
return;
}
mLabelView.setText(label);
}
/**
* Set the icon for the task item.
* Set the icon for the task item. Sets to a default icon if null.
*
* @param icon task icon
*/
public void setIcon(Drawable icon) {
public void setIcon(@Nullable Drawable icon) {
// TODO: Scale the icon up based off the padding on the side
// 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);
}
/**
* Set the task thumbnail for the task.
* Set the task thumbnail for the task. Sets to a default thumbnail if null.
*
* @param thumbnail task thumbnail for the task
*/
public void setThumbnail(Bitmap thumbnail) {
public void setThumbnail(@Nullable Bitmap thumbnail) {
if (thumbnail == null) {
mThumbnailView.setImageBitmap(null);
mThumbnailView.setBackgroundColor(Color.GRAY);
return;
}
mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
mThumbnailView.setImageBitmap(thumbnail);
}