Merge "Implementing support for item diffing instead of creating out the complete UI on every update" into tm-dev

This commit is contained in:
TreeHugger Robot
2022-05-04 23:11:04 +00:00
committed by Android (Google) Code Review
11 changed files with 149 additions and 170 deletions
@@ -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);
}
}
@@ -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<T extends Context & AppLauncher
mHeader.reset(false);
}
/** Invoke when the search results change. */
public void onSearchResultsChanged() {
for (int i = 0; i < mAH.size(); i++) {
if (mAH.get(i).mRecyclerView != null) {
mAH.get(i).mRecyclerView.onSearchResultsChanged();
/**
* Sets results list for search
*/
public void setSearchResults(ArrayList<AdapterItem> results) {
if (getApps().setSearchResults(results)) {
for (int i = 0; i < mAH.size(); i++) {
if (mAH.get(i).mRecyclerView != null) {
mAH.get(i).mRecyclerView.onSearchResultsChanged();
}
}
}
}
@@ -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));
}
@@ -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<AlphabeticalAppsList.FastScrollSectionInfo> 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
@@ -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;
@@ -52,14 +57,13 @@ public class AlphabeticalAppsList<T extends Context & ActivityContext> 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;
}
}
@@ -107,13 +111,6 @@ public class AlphabeticalAppsList<T extends Context & ActivityContext> implement
mAdapter = adapter;
}
/**
* Returns all the apps.
*/
public List<AppInfo> getApps() {
return mApps;
}
/**
* Returns fast scroller sections of all the current filtered applications.
*/
@@ -235,77 +232,49 @@ public class AlphabeticalAppsList<T extends Context & ActivityContext> 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<AdapterItem> 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)
@@ -328,19 +297,43 @@ public class AlphabeticalAppsList<T extends Context & ActivityContext> 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<AdapterItem> mOldList;
private final List<AdapterItem> mNewList;
MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> 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));
}
}
}
@@ -88,10 +88,8 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> 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<T extends Context & ActivityContext> 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;
@@ -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<AllAppsGridAdapter.AdapterItem> 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();
@@ -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<AdapterItem> 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();
@@ -85,8 +85,7 @@ public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
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++;
}
}
@@ -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}.
@@ -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<View> 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);
}
}