diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 4590125c39..cb4012f788 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -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 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 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 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()); diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java index 06af970cfc..ac48709d09 100644 --- a/src/com/android/launcher3/allapps/AllAppsStore.java +++ b/src/com/android/launcher3/allapps/AllAppsStore.java @@ -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 The type of the context. */ -public class AllAppsStore { +public class AllAppsStore { // 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 map) { + public void setApps(AppInfo[] apps, int flags, Map 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; } /** diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 8fa42765b0..72a01958fd 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -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; diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index df24620082..1ac2d72824 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -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; diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt new file mode 100644 index 0000000000..26dde29d36 --- /dev/null +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -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 : RecycledViewPool() { + + private var future: Future? = null + + /** + * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate. + */ + fun 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 = + object : BaseAllAppsAdapter(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 { + 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 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 + } +} diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java index 6978e0c2a4..dec4b5ca8d 100644 --- a/src/com/android/launcher3/util/Executors.java +++ b/src/com/android/launcher3/util/Executors.java @@ -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 */