From 1acda93e26ae714e6e76d948a8032a61e338fe90 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Wed, 5 Jul 2023 12:54:45 -0700 Subject: [PATCH 1/2] Fix calculation of all apps recyclerview pool size of app icons grid.numShownAllAppsColumns is the column size of all apps, whereas mNumAppsPerRow is the column size of workspace, we should use the former one to calculate num of all apps icons Bug: 287523421 Flag: N/A Test: Open all apps from launcher and taskbar and scroll it, Expect scorlling works without jank. Change-Id: Ife488e5853c84f6cc94e1e9e7edae67844275439 --- src/com/android/launcher3/allapps/AllAppsRecyclerView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 7c5c003915..602d1a38a5 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -96,8 +96,8 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); - pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows - * (mNumAppsPerRow + 1)); + pool.setMaxRecycledViews( + AllAppsGridAdapter.VIEW_TYPE_ICON, (approxRows + 1) * grid.numShownAllAppsColumns); } @Override From 1519c168da4798f6345f08dc2deaeff6df5c00a7 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Thu, 15 Jun 2023 12:28:42 -0700 Subject: [PATCH 2/2] 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 --- .../allapps/ActivityAllAppsContainerView.java | 12 ++- .../launcher3/allapps/AllAppsStore.java | 28 +++++- .../launcher3/allapps/BaseAllAppsAdapter.java | 2 +- .../launcher3/config/FeatureFlags.java | 6 ++ .../recyclerview/AllAppsRecyclerViewPool.kt | 92 +++++++++++++++++++ src/com/android/launcher3/util/Executors.java | 6 ++ 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt 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 */