Pre-inflate BubbleTextViews into Launcher/TaskBar All Apps RV

This CL ensures no inflation of BubbleTextView happens while binding applications, and reduces jank on slow device.

1. Let active/inactive all apps RVs share the same AllAppsRecyclerViewPool
2. Use worker thread to pre-inflate BubbleTextViews and add them to shared view pool on main thread

Bug: 287523421
Test: See before/after screenshot/video/trace attached in bug
Change-Id: I00213407be2c7c2d329997552785d0aa56c4d057
This commit is contained in:
Fengjiang Li
2023-06-15 12:28:42 -07:00
parent 1acda93e26
commit 1519c168da
6 changed files with 141 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ package com.android.launcher3.allapps;
import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
@@ -79,7 +80,6 @@ import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.model.StringCache;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
@@ -141,7 +141,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
private final SearchTransitionController mSearchTransitionController;
private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Rect mInsets = new Rect();
private final AllAppsStore mAllAppsStore = new AllAppsStore();
private final AllAppsStore mAllAppsStore;
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
@Override
@@ -194,6 +194,7 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivityContext = ActivityContext.lookupContext(context);
mAllAppsStore = new AllAppsStore(mActivityContext);
mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
mHeaderThreshold = getResources().getDimensionPixelSize(
@@ -559,6 +560,13 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
// Let main and work rv share same view pool.
((RecyclerView) mViewPager.getChildAt(0))
.setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
((RecyclerView) mViewPager.getChildAt(1))
.setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
}
if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
mWorkManager.newScrollListener());

View File

@@ -15,24 +15,30 @@
*/
package com.android.launcher3.allapps;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
import android.content.Context;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.recyclerview.AllAppsRecyclerViewPool;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
import java.util.Arrays;
@@ -45,8 +51,10 @@ import java.util.function.Predicate;
/**
* A utility class to maintain the collection of all apps.
*
* @param <T> The type of the context.
*/
public class AllAppsStore {
public class AllAppsStore<T extends Context & ActivityContext> {
// Defer updates flag used to defer all apps updates to the next draw.
public static final int DEFER_UPDATES_NEXT_DRAW = 1 << 0;
@@ -64,20 +72,36 @@ public class AllAppsStore {
private int mModelFlags;
private int mDeferUpdatesFlags = 0;
private boolean mUpdatePending = false;
private final AllAppsRecyclerViewPool mAllAppsRecyclerViewPool = new AllAppsRecyclerViewPool();
private final T mContext;
public AppInfo[] getApps() {
return mApps;
}
public AllAppsStore(@NonNull T context) {
mContext = context;
}
/**
* Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for
* the current set of apps.
*/
public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
mApps = apps;
mModelFlags = flags;
notifyUpdate();
mPackageUserKeytoUidMap = map;
// Preinflate all apps RV when apps has changed, which can happen after unlocking screen,
// rotating screen, or downloading/upgrading apps.
if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
mAllAppsRecyclerViewPool.preInflateAllAppsViewHolders(mContext);
}
}
RecycledViewPool getRecyclerViewPool() {
return mAllAppsRecyclerViewPool;
}
/**

View File

@@ -32,8 +32,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.allapps.search.SearchAdapterProvider;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.search.SearchAdapterProvider;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.views.ActivityContext;

View File

@@ -401,6 +401,12 @@ public final class FeatureFlags {
"ENABLE_RESPONSIVE_WORKSPACE", DISABLED,
"Enables new workspace grid calculations method.");
// TODO(Block 33): Clean up flags
public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
"ENABLE_ALL_APPS_RV_PREINFLATION", DISABLED,
"Enables preinflating all apps icons to avoid scrolling jank.");
public static class BooleanFlag {
private final boolean mCurrentValue;

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2023 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.recyclerview
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.launcher3.BubbleTextView
import com.android.launcher3.allapps.BaseAllAppsAdapter
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.views.ActivityContext
import java.util.concurrent.Future
private const val PREINFLATE_ICONS_ROW_COUNT = 4
private const val EXTRA_ICONS_COUNT = 2
/**
* An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
private var future: Future<Void>? = null
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
val preInflateCount = getPreinflateCount(context)
if (preInflateCount <= 0) {
return
}
// Because we perform onCreateViewHolder() on worker thread, we need a separate
// adapter/inflator object as they are not thread-safe. Note that the adapter
// just need to perform onCreateViewHolder(parent, VIEW_TYPE_ICON) so it doesn't need
// data source information.
val adapter: RecyclerView.Adapter<BaseAllAppsAdapter.ViewHolder> =
object : BaseAllAppsAdapter<T>(context, context.appsView.layoutInflater, null, null) {
override fun setAppsPerRow(appsPerRow: Int) = Unit
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
// Inflate view holders on background thread, and added to view pool on main thread.
future?.cancel(true)
future =
VIEW_PREINFLATION_EXECUTOR.submit<Void> {
val viewHolders =
Array(preInflateCount) {
adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
}
MAIN_EXECUTOR.execute {
for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
putRecycledView(viewHolders[i])
}
}
null
}
}
/**
* After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
* app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
* suffice fast scrolling.
*/
fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
val targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
return targetPreinflateCount - existingPreinflateCount
}
}

View File

@@ -21,6 +21,7 @@ import android.os.Process;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
@@ -58,6 +59,11 @@ public class Executors {
new LooperExecutor(
createAndStartNewLooper("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND));
/** A background executor to preinflate views. */
public static final ExecutorService VIEW_PREINFLATION_EXECUTOR =
java.util.concurrent.Executors.newSingleThreadExecutor();
/**
* Utility method to get a started handler thread statically
*/