diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml index 43ae9b8640..8f43192780 100644 --- a/quickstep/res/layout/task.xml +++ b/quickstep/res/layout/task.xml @@ -42,14 +42,8 @@ android:importantForAccessibility="noHideDescendants" android:background="#800000FF" android:layout_gravity="bottom" + android:gravity="center" + android:textColor="@android:color/white" android:visibility="gone" - > - - \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index cb214afdeb..cc49d46e09 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -16,16 +16,13 @@ package com.android.quickstep; -import android.content.Context; import android.graphics.Matrix; import android.view.View; -import androidx.annotation.AnyThread; - import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.R; -import com.android.launcher3.util.Preconditions; +import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.ResourceBasedOverride; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; @@ -34,11 +31,12 @@ import com.android.systemui.shared.recents.model.ThumbnailData; import java.util.ArrayList; import java.util.List; +import androidx.annotation.AnyThread; + /** * Factory class to create and add an overlays on the TaskView */ public class TaskOverlayFactory implements ResourceBasedOverride { - private static TaskOverlayFactory sInstance; /** Note that these will be shown in order from top to bottom, if available for the task. */ private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{ @@ -49,14 +47,9 @@ public class TaskOverlayFactory implements ResourceBasedOverride { new TaskSystemShortcut.Freeform() }; - public static TaskOverlayFactory get(Context context) { - Preconditions.assertUIThread(); - if (sInstance == null) { - sInstance = Overrides.getObject(TaskOverlayFactory.class, - context.getApplicationContext(), R.string.task_overlay_factory_class); - } - return sInstance; - } + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(c -> Overrides.getObject(TaskOverlayFactory.class, + c, R.string.task_overlay_factory_class)); @AnyThread public boolean needAssist() { diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 7a6b135cfa..8b6867f763 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -203,7 +203,7 @@ public class TouchInteractionService extends Service { mEventQueue = new MotionEventQueue(mMainThreadChoreographer, TouchConsumer.NO_OP); mOverviewInteractionState = OverviewInteractionState.INSTANCE.get(this); mOverviewCallbacks = OverviewCallbacks.get(this); - mTaskOverlayFactory = TaskOverlayFactory.get(this); + mTaskOverlayFactory = TaskOverlayFactory.INSTANCE.get(this); mTouchInteractionLog = new TouchInteractionLog(); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); mInputConsumer.registerInputConsumer(); diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index a92295e68f..b34d2bf9cd 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.widget.LinearLayout; import android.widget.TextView; import com.android.launcher3.Launcher; @@ -32,7 +31,8 @@ import com.android.launcher3.Utilities; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.systemui.shared.recents.model.Task; -public final class DigitalWellBeingToast extends LinearLayout { +public final class DigitalWellBeingToast extends TextView { + public interface InitializeCallback { void call(float saturation, String contentDescription); } @@ -56,12 +56,11 @@ public final class DigitalWellBeingToast extends LinearLayout { final long appRemainingTimeMs = -1; final boolean isGroupLimit = true; post(() -> { - final TextView remainingTimeText = findViewById(R.id.remaining_time); if (appUsageLimitTimeMs < 0) { setVisibility(GONE); } else { setVisibility(VISIBLE); - remainingTimeText.setText(getText(appRemainingTimeMs, isGroupLimit)); + setText(getText(appRemainingTimeMs, isGroupLimit)); } callback.call( diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 4cfd8cb06d..5cbae65d4b 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -81,6 +81,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.PendingAnimation; import com.android.launcher3.util.Themes; +import com.android.launcher3.util.ViewPool; import com.android.quickstep.OverviewCallbacks; import com.android.quickstep.QuickScrubController; import com.android.quickstep.RecentsAnimationWrapper; @@ -156,6 +157,8 @@ public abstract class RecentsView extends PagedView impl private final InvariantDeviceProfile mIdp; + private final ViewPool mTaskViewPool; + /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ @@ -304,6 +307,9 @@ public abstract class RecentsView extends PagedView impl .inflate(R.layout.overview_clear_all_button, this, false); mClearAllButton.setOnClickListener(this::dismissAllTasks); + mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, + 10 /* initial size */); + mIsRtl = !Utilities.isRtl(getResources()); setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); mTaskTopMargin = getResources() @@ -384,6 +390,7 @@ public abstract class RecentsView extends PagedView impl mHasVisibleTaskData.delete(task.key.id); taskView.onTaskListVisibilityChanged(false /* visible */); } + mTaskViewPool.recycle(taskView); } } @@ -487,10 +494,6 @@ public abstract class RecentsView extends PagedView impl int oldChildCount = getChildCount(); - // Ensure there are as many views as there are tasks in the stack (adding and trimming as - // necessary) - final LayoutInflater inflater = LayoutInflater.from(getContext()); - // Unload existing visible task data unloadVisibleTaskData(); @@ -503,7 +506,7 @@ public abstract class RecentsView extends PagedView impl removeView(mClearAllButton); } for (int i = getChildCount(); i < requiredTaskCount; i++) { - addView(inflater.inflate(R.layout.task, this, false)); + addView(mTaskViewPool.getView()); } while (getChildCount() > requiredTaskCount) { removeView(getChildAt(getChildCount() - 1)); @@ -754,8 +757,7 @@ public abstract class RecentsView extends PagedView impl public void showTask(int runningTaskId) { if (getChildCount() == 0) { // Add an empty view for now until the task plan is loaded and applied - final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) - .inflate(R.layout.task, this, false); + final TaskView taskView = mTaskViewPool.getView(); addView(taskView); addView(mClearAllButton); diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java index 0194b0b7ee..8169d73ee6 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java @@ -107,7 +107,7 @@ public class TaskThumbnailView extends View { public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius); - mOverlay = TaskOverlayFactory.get(context).createOverlay(this); + mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this); mPaint.setFilterBitmap(true); mBackgroundPaint.setColor(Color.WHITE); mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index 2946dfd736..ad63c248be 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -52,6 +52,7 @@ import com.android.launcher3.anim.Interpolators; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.ViewPool.Reusable; import com.android.quickstep.RecentsModel; import com.android.quickstep.TaskIconCache; import com.android.quickstep.TaskOverlayFactory; @@ -70,7 +71,7 @@ import java.util.function.Consumer; /** * A task in the Recents view. */ -public class TaskView extends FrameLayout implements PageCallbacks { +public class TaskView extends FrameLayout implements PageCallbacks, Reusable { private static final String TAG = TaskView.class.getSimpleName(); @@ -399,19 +400,29 @@ public class TaskView extends FrameLayout implements PageCallbacks { setIconAndDimTransitionProgress(iconScale, invert); } - public void resetVisualProperties() { + private void resetViewTransforms() { setZoomScale(1); setTranslationX(0f); setTranslationY(0f); setTranslationZ(0); setAlpha(1f); setIconScaleAndDim(1); + } + + public void resetVisualProperties() { + resetViewTransforms(); if (!getRecentsView().getQuickScrubController().isQuickSwitch()) { // Reset full screen progress unless we are doing back to back quick switch. setFullscreenProgress(0); } } + @Override + public void onRecycle() { + resetViewTransforms(); + setFullscreenProgress(0); + } + @Override public void onPageScroll(ScrollState scrollState) { float curveInterpolation = diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java new file mode 100644 index 0000000000..8af048d9d1 --- /dev/null +++ b/src/com/android/launcher3/util/ViewPool.java @@ -0,0 +1,115 @@ +/* + * 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.launcher3.util; + +import android.content.Context; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.util.ViewPool.Reusable; + +import androidx.annotation.AnyThread; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +/** + * Utility class to maintain a pool of reusable views. + * During initialization, views are inflated on the background thread. + */ +public class ViewPool { + + private final Object[] mPool; + + private final LayoutInflater mInflater; + private final ViewGroup mParent; + private final int mLayoutId; + + private int mCurrentSize = 0; + + public ViewPool(Context context, @Nullable ViewGroup parent, + int layoutId, int maxSize, int initialSize) { + mLayoutId = layoutId; + mParent = parent; + mInflater = LayoutInflater.from(context); + mPool = new Object[maxSize]; + + if (initialSize > 0) { + initPool(initialSize); + } + } + + @UiThread + private void initPool(int initialSize) { + Preconditions.assertUIThread(); + Handler handler = new Handler(); + + // Inflate views on a non looper thread. This allows us to catch errors like calling + // "new Handler()" in constructor easily. + new Thread(() -> { + for (int i = 0; i < initialSize; i++) { + T view = inflateNewView(); + handler.post(() -> addToPool(view)); + } + }).start(); + } + + @UiThread + public void recycle(T view) { + Preconditions.assertUIThread(); + view.onRecycle(); + addToPool(view); + } + + @UiThread + private void addToPool(T view) { + Preconditions.assertUIThread(); + if (mCurrentSize >= mPool.length) { + // pool is full + return; + } + + mPool[mCurrentSize] = view; + mCurrentSize++; + } + + @UiThread + public T getView() { + Preconditions.assertUIThread(); + if (mCurrentSize > 0) { + mCurrentSize--; + return (T) mPool[mCurrentSize]; + } + return inflateNewView(); + } + + @AnyThread + private T inflateNewView() { + return (T) mInflater.inflate(mLayoutId, mParent, false); + } + + /** + * Interface to indicate that a view is reusable + */ + public interface Reusable { + + /** + * Called when a view is recycled / added back to the pool + */ + void onRecycle(); + } +}