From 3c5a08ada14741b01844f8deb40b82dc0431f692 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Sun, 1 May 2022 12:18:14 -0700 Subject: [PATCH] Implementing support for item diffing instead of creating out the complete UI on every update Bug: 229860311 Test: Verified locally Change-Id: I5712b5d76878a0ed72cc1392ede59b3778b7a1dc --- .../android/launcher3/BaseRecyclerView.java | 8 -- .../allapps/ActivityAllAppsContainerView.java | 16 ++- .../allapps/AllAppsFastScrollHelper.java | 4 +- .../allapps/AllAppsRecyclerView.java | 37 +++-- .../allapps/AlphabeticalAppsList.java | 129 +++++++++--------- .../launcher3/allapps/BaseAllAppsAdapter.java | 59 +++----- .../allapps/WorkAdapterProvider.java | 10 +- .../search/AppsSearchContainerLayout.java | 14 +- .../search/DefaultAppSearchAlgorithm.java | 3 +- .../widget/picker/WidgetsRecyclerView.java | 15 -- .../android/launcher3/ui/WorkProfileTest.java | 24 +++- 11 files changed, 149 insertions(+), 170 deletions(-) diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index 9369bdc2fd..b6d3fc5304 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -23,7 +23,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; -import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.compat.AccessibilityManagerCompat; @@ -197,13 +196,6 @@ public abstract class BaseRecyclerView extends RecyclerView { if (mScrollbar != null) { mScrollbar.reattachThumbToScroll(); } - if (getLayoutManager() instanceof LinearLayoutManager) { - LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); - if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { - // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). - return; - } - } scrollToPosition(0); } } \ No newline at end of file diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index e279f59aaf..47f2dd50aa 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -29,11 +29,13 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.DeviceProfile.DeviceProfileListenable; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.allapps.search.SearchAdapterProvider; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.views.AppLauncher; +import java.util.ArrayList; import java.util.Objects; /** @@ -95,11 +97,15 @@ public class ActivityAllAppsContainerView results) { + if (getApps().setSearchResults(results)) { + for (int i = 0; i < mAH.size(); i++) { + if (mAH.get(i).mRecyclerView != null) { + mAH.get(i).mRecyclerView.onSearchResultsChanged(); + } } } } diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index f97eb28dda..7067fa225b 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -37,10 +37,10 @@ public class AllAppsFastScrollHelper { * Smooth scrolls the recycler view to the given section. */ public void smoothScrollToSection(FastScrollSectionInfo info) { - if (mTargetFastScrollPosition == info.fastScrollToItem.position) { + if (mTargetFastScrollPosition == info.position) { return; } - mTargetFastScrollPosition = info.fastScrollToItem.position; + mTargetFastScrollPosition = info.position; mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition)); } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 7dbe711716..c4e977b5fa 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -71,6 +71,26 @@ public class AllAppsRecyclerView extends BaseRecyclerView { public void onChanged() { mCachedScrollPositions.clear(); } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + onChanged(); + } }; // The empty-search result background @@ -241,17 +261,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView { // Find the fastscroll section that maps to this touch fraction List fastScrollSections = mApps.getFastScrollerSections(); - AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); - for (int i = 1; i < fastScrollSections.size(); i++) { - AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); - if (info.touchFraction > touchFraction) { - break; - } - lastInfo = info; + int count = fastScrollSections.size(); + if (count == 0) { + return ""; } - - mFastScrollHelper.smoothScrollToSection(lastInfo); - return lastInfo.sectionName; + int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1); + AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index); + mFastScrollHelper.smoothScrollToSection(section); + return section.sectionName; } @Override diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 2a16210861..d9b0a157b8 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -15,9 +15,14 @@ */ package com.android.launcher3.allapps; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ALL_APPS_DIVIDER; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_EMPTY_SEARCH; +import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_SEARCH_MARKET; import android.content.Context; +import androidx.recyclerview.widget.DiffUtil; + import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; @@ -51,14 +56,13 @@ public class AlphabeticalAppsList implement */ public static class FastScrollSectionInfo { // The section name - public String sectionName; - // The AdapterItem to scroll to for this section - public AdapterItem fastScrollToItem; - // The touch fraction that should map to this fast scroll section info - public float touchFraction; + public final String sectionName; + // The item position + public final int position; - public FastScrollSectionInfo(String sectionName) { + public FastScrollSectionInfo(String sectionName, int position) { this.sectionName = sectionName; + this.position = position; } } @@ -106,13 +110,6 @@ public class AlphabeticalAppsList implement mAdapter = adapter; } - /** - * Returns all the apps. - */ - public List getApps() { - return mApps; - } - /** * Returns fast scroller sections of all the current filtered applications. */ @@ -247,77 +244,49 @@ public class AlphabeticalAppsList implement * mCachedSectionNames to have been calculated for the set of all apps in mApps. */ public void updateAdapterItems() { - refillAdapterItems(); - refreshRecyclerView(); - } - - private void refreshRecyclerView() { - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } - } - - private void refillAdapterItems() { - String lastSectionName = null; - FastScrollSectionInfo lastFastScrollerSectionInfo = null; - int position = 0; - + List oldItems = new ArrayList<>(mAdapterItems); // Prepare to update the list of sections, filtered apps, etc. - mAccessibilityResultsCount = 0; mFastScrollerSections.clear(); mAdapterItems.clear(); + mAccessibilityResultsCount = 0; // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the // ordered set of sections - if (!hasFilter()) { - mAccessibilityResultsCount = mApps.size(); + int position = 0; if (mWorkAdapterProvider != null) { position += mWorkAdapterProvider.addWorkItems(mAdapterItems); if (!mWorkAdapterProvider.shouldShowWorkApps()) { return; } } + String lastSectionName = null; for (AppInfo info : mApps) { - String sectionName = info.sectionName; + mAdapterItems.add(AdapterItem.asApp(info)); + String sectionName = info.sectionName; // Create a new section if the section names do not match if (!sectionName.equals(lastSectionName)) { lastSectionName = sectionName; - lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); - mFastScrollerSections.add(lastFastScrollerSectionInfo); + mFastScrollerSections.add(new FastScrollSectionInfo(sectionName, position)); } - - // Create an app item - AdapterItem appItem = AdapterItem.asApp(position++, info); - if (lastFastScrollerSectionInfo.fastScrollToItem == null) { - lastFastScrollerSectionInfo.fastScrollToItem = appItem; - } - - mAdapterItems.add(appItem); + position++; } } else { - int count = mSearchResults.size(); - for (int i = 0; i < count; i++) { - AdapterItem adapterItem = mSearchResults.get(i); - adapterItem.position = i; - mAdapterItems.add(adapterItem); - - if (adapterItem.isCountedForAccessibility()) { - mAccessibilityResultsCount++; - } - } + mAdapterItems.addAll(mSearchResults); if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { // Append the search market item if (hasNoFilteredResults()) { - mAdapterItems.add(AdapterItem.asEmptySearch(position++)); + mAdapterItems.add(new AdapterItem(VIEW_TYPE_EMPTY_SEARCH)); } else { - mAdapterItems.add(AdapterItem.asAllAppsDivider(position++)); + mAdapterItems.add(new AdapterItem(VIEW_TYPE_ALL_APPS_DIVIDER)); } - mAdapterItems.add(AdapterItem.asMarketSearch(position++)); - + mAdapterItems.add(new AdapterItem(VIEW_TYPE_SEARCH_MARKET)); } } + mAccessibilityResultsCount = (int) mAdapterItems.stream() + .filter(AdapterItem::isCountedForAccessibility).count(); + if (mNumAppsPerRowAllApps != 0) { // Update the number of rows in the adapter after we do all the merging (otherwise, we // would have to shift the values again) @@ -340,19 +309,43 @@ public class AlphabeticalAppsList implement } } mNumAppRowsInAdapter = rowIndex + 1; + } - // Pre-calculate all the fast scroller fractions - float perSectionTouchFraction = 1f / mFastScrollerSections.size(); - float cumulativeTouchFraction = 0f; - for (FastScrollSectionInfo info : mFastScrollerSections) { - AdapterItem item = info.fastScrollToItem; - if (!BaseAllAppsAdapter.isIconViewType(item.viewType)) { - info.touchFraction = 0f; - continue; - } - info.touchFraction = cumulativeTouchFraction; - cumulativeTouchFraction += perSectionTouchFraction; - } + if (mAdapter != null) { + DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false) + .dispatchUpdatesTo(mAdapter); } } + + private static class MyDiffCallback extends DiffUtil.Callback { + + private final List mOldList; + private final List mNewList; + + MyDiffCallback(List oldList, List newList) { + mOldList = oldList; + mNewList = newList; + } + + @Override + public int getOldListSize() { + return mOldList.size(); + } + + @Override + public int getNewListSize() { + return mNewList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition)); + } + } + } diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 8ac2536c42..c7c4607d6e 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -88,10 +88,8 @@ public abstract class BaseAllAppsAdapter ex */ public static class AdapterItem { /** Common properties */ - // The index of this adapter item in the list - public int position; // The type of this item - public int viewType; + public final int viewType; // The row that this item shows up on public int rowIndex; @@ -100,50 +98,37 @@ public abstract class BaseAllAppsAdapter ex // The associated ItemInfoWithIcon for the item public AppInfo itemInfo = null; + public AdapterItem(int viewType) { + this.viewType = viewType; + } + /** * Factory method for AppIcon AdapterItem */ - public static AdapterItem asApp(int pos, AppInfo appInfo) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_ICON; - item.position = pos; + public static AdapterItem asApp(AppInfo appInfo) { + AdapterItem item = new AdapterItem(VIEW_TYPE_ICON); item.itemInfo = appInfo; return item; } - /** - * Factory method for empty search results view - */ - public static AdapterItem asEmptySearch(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_EMPTY_SEARCH; - item.position = pos; - return item; - } - - /** - * Factory method for a dividerView in AllAppsSearch - */ - public static AdapterItem asAllAppsDivider(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER; - item.position = pos; - return item; - } - - /** - * Factory method for a market search button - */ - public static AdapterItem asMarketSearch(int pos) { - AdapterItem item = new AdapterItem(); - item.viewType = VIEW_TYPE_SEARCH_MARKET; - item.position = pos; - return item; - } - protected boolean isCountedForAccessibility() { return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET; } + + /** + * Returns true if the items represent the same object + */ + public boolean isSameAs(AdapterItem other) { + return (other.viewType != viewType) && (other.getClass() == getClass()); + } + + /** + * This is called only if {@link #isSameAs} returns true to check if the contents are same + * as well. Returning true will prevent redrawing of thee item. + */ + public boolean isContentSame(AdapterItem other) { + return itemInfo == null && other.itemInfo == null; + } } protected final T mActivityContext; diff --git a/src/com/android/launcher3/allapps/WorkAdapterProvider.java b/src/com/android/launcher3/allapps/WorkAdapterProvider.java index ce44958b19..76d08c8084 100644 --- a/src/com/android/launcher3/allapps/WorkAdapterProvider.java +++ b/src/com/android/launcher3/allapps/WorkAdapterProvider.java @@ -19,10 +19,10 @@ import android.content.SharedPreferences; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.TextView; import com.android.launcher3.R; +import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.model.StringCache; import com.android.launcher3.views.ActivityContext; @@ -107,13 +107,9 @@ public class WorkAdapterProvider extends BaseAdapterProvider { public int addWorkItems(ArrayList adapterItems) { if (mState == WorkProfileManager.STATE_DISABLED) { //add disabled card here. - AllAppsGridAdapter.AdapterItem disabledCard = new AllAppsGridAdapter.AdapterItem(); - disabledCard.viewType = VIEW_TYPE_WORK_DISABLED_CARD; - adapterItems.add(disabledCard); + adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD)); } else if (mState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) { - AllAppsGridAdapter.AdapterItem eduCard = new AllAppsGridAdapter.AdapterItem(); - eduCard.viewType = VIEW_TYPE_WORK_EDU_CARD; - adapterItems.add(eduCard); + adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD)); } return adapterItems.size(); diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java index bc2c318ecb..6539c05d84 100644 --- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java +++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java @@ -38,7 +38,6 @@ import com.android.launcher3.Insettable; import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsStore; -import com.android.launcher3.allapps.AlphabeticalAppsList; import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.allapps.SearchUiManager; import com.android.launcher3.search.SearchCallback; @@ -57,7 +56,6 @@ public class AppsSearchContainerLayout extends ExtendedEditText private final AllAppsSearchBarController mSearchBarController; private final SpannableStringBuilder mSearchQueryBuilder; - private AlphabeticalAppsList mApps; private ActivityAllAppsContainerView mAppsView; // The amount of pixels to shift down and overlap with the rest of the content. @@ -131,7 +129,6 @@ public class AppsSearchContainerLayout extends ExtendedEditText @Override public void initializeSearch(ActivityAllAppsContainerView appsView) { - mApps = appsView.getApps(); mAppsView = appsView; mSearchBarController.initialize( new DefaultAppSearchAlgorithm(getContext()), @@ -170,17 +167,14 @@ public class AppsSearchContainerLayout extends ExtendedEditText @Override public void onSearchResult(String query, ArrayList items) { if (items != null) { - mApps.setSearchResults(items); - notifyResultChanged(); + mAppsView.setSearchResults(items); mAppsView.setLastSearchQuery(query); } } @Override public void clearSearchResult() { - if (mApps.setSearchResults(null)) { - notifyResultChanged(); - } + mAppsView.setSearchResults(null); // Clear the search query mSearchQueryBuilder.clear(); @@ -189,10 +183,6 @@ public class AppsSearchContainerLayout extends ExtendedEditText mAppsView.onClearSearchResult(); } - private void notifyResultChanged() { - mAppsView.onSearchResultsChanged(); - } - @Override public void setInsets(Rect insets) { MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java index 33d0082228..4eceb7184e 100644 --- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java +++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java @@ -85,8 +85,7 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm { for (int i = 0; i < total && resultCount < MAX_RESULTS_COUNT; i++) { AppInfo info = apps.get(i); if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) { - AdapterItem appItem = AdapterItem.asApp(resultCount, info); - result.add(appItem); + result.add(AdapterItem.asApp(info)); resultCount++; } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java index f780f03948..755e4a9085 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java @@ -239,21 +239,6 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch mHeaderViewDimensionsProvider = headerViewDimensionsProvider; } - @Override - public void scrollToTop() { - if (mScrollbar != null) { - mScrollbar.reattachThumbToScroll(); - } - - if (getLayoutManager() instanceof LinearLayoutManager) { - if (getCurrentScrollY() == 0) { - // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). - return; - } - } - scrollToPosition(0); - } - /** * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until * {@code untilIndex}. diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java index f31e4f315c..7c1be1de09 100644 --- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java +++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java @@ -25,11 +25,14 @@ import static org.junit.Assert.assertTrue; import android.util.Log; import android.view.View; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.AllAppsPagedView; import com.android.launcher3.allapps.WorkAdapterProvider; import com.android.launcher3.allapps.WorkEduCard; +import com.android.launcher3.allapps.WorkPausedCard; import com.android.launcher3.allapps.WorkProfileManager; import com.android.launcher3.tapl.LauncherInstrumentation; @@ -38,6 +41,7 @@ import org.junit.Before; import org.junit.Test; import java.util.Objects; +import java.util.function.Predicate; public class WorkProfileTest extends AbstractLauncherUiTest { @@ -130,6 +134,8 @@ public class WorkProfileTest extends AbstractLauncherUiTest { return manager.getCurrentState() == WorkProfileManager.STATE_DISABLED; }, LauncherInstrumentation.WAIT_TIME_MS); + waitForWorkCard("Work paused card not shown", view -> view instanceof WorkPausedCard); + // start work profile toggle ON test executeOnLauncher(l -> { ActivityAllAppsContainerView allApps = l.getAppsView(); @@ -154,9 +160,19 @@ public class WorkProfileTest extends AbstractLauncherUiTest { l.getAppsView().getWorkManager().reset(); }); - waitForLauncherCondition("Work profile education not shown", - l -> l.getAppsView().getActiveRecyclerView() - .findViewHolderForAdapterPosition(0).itemView instanceof WorkEduCard, - LauncherInstrumentation.WAIT_TIME_MS); + waitForWorkCard("Work profile education not shown", view -> view instanceof WorkEduCard); + } + + private void waitForWorkCard(String message, Predicate workCardCheck) { + waitForLauncherCondition(message, l -> { + l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST); + ViewHolder holder = l.getAppsView().getActiveRecyclerView() + .findViewHolderForAdapterPosition(0); + try { + return holder != null && workCardCheck.test(holder.itemView); + } finally { + l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST); + } + }, LauncherInstrumentation.WAIT_TIME_MS); } }