Lazily binding folder pages and icons

Folders are bound before they are opened and unbound on close. This
allows us to recycle the views in folders

Bug: 139051851
Change-Id: Ic1ed3265c0f583af54d73dab6f2751bc95266ea9
This commit is contained in:
Sunny Goyal 2019-08-16 11:59:55 -07:00
parent 72fddba837
commit ac6f69f78d
9 changed files with 304 additions and 285 deletions

View File

@ -20,4 +20,5 @@
style="@style/BaseIcon"
android:textColor="?attr/folderTextColor"
android:includeFontPadding="false"
android:hapticFeedbackEnabled="false"
launcher:iconDisplay="folder" />

View File

@ -93,13 +93,6 @@ public class FolderInfo extends ItemInfo {
itemsChanged(animate);
}
public void setTitle(CharSequence title) {
this.title = title;
for (int i = 0; i < mListeners.size(); i++) {
mListeners.get(i).onTitleChanged(title);
}
}
@Override
public void onAddToDatabase(ContentWriter writer) {
super.onAddToDatabase(writer);
@ -121,18 +114,10 @@ public class FolderInfo extends ItemInfo {
}
}
public void prepareAutoUpdate() {
for (int i = 0; i < mListeners.size(); i++) {
mListeners.get(i).prepareAutoUpdate();
}
}
public interface FolderListener {
public void onAdd(WorkspaceItemInfo item, int rank);
public void onRemove(WorkspaceItemInfo item);
public void onTitleChanged(CharSequence title);
public void onItemsChanged(boolean animate);
public void prepareAutoUpdate();
}
public boolean hasOption(int optionFlag) {

View File

@ -2340,6 +2340,11 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns,
// override the previous page so we don't log the page switch.
mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
// Cache one page worth of icons
getViewCache().setCacheSize(R.layout.folder_application,
mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
getViewCache().setCacheSize(R.layout.folder_page, 2);
TraceHelper.endSection("finishBindingItems");
}

View File

@ -92,7 +92,6 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.PackageUserKey;
@ -134,9 +133,6 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
private static final int DEFAULT_PAGE = 0;
public static final boolean MAP_NO_RECURSE = false;
public static final boolean MAP_RECURSE = true;
private LayoutTransition mLayoutTransition;
@Thunk final WallpaperManager mWallpaperManager;
@ -2809,7 +2805,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
* Removes all folder listeners
*/
public void removeFolderListeners() {
mapOverItems(false, new ItemOperator() {
mapOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view) {
if (view instanceof FolderIcon) {
@ -2961,7 +2957,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
public View getFirstMatch(final ItemOperator operator) {
final View[] value = new View[1];
mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
mapOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (operator.evaluate(info, v)) {
@ -2984,7 +2980,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
final View[] matches = new View[operators.length];
// For efficiency, the outer loop should be CellLayout.
for (CellLayout cellLayout : cellLayouts) {
mapOverCellLayout(MAP_NO_RECURSE, cellLayout, (info, v) -> {
mapOverCellLayout(cellLayout, (info, v) -> {
for (int i = 0; i < operators.length; ++i) {
if (matches[i] == null && operators[i].evaluate(info, v)) {
matches[i] = v;
@ -3009,7 +3005,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
}
void clearDropTargets() {
mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
mapOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (v instanceof DropTarget) {
@ -3054,10 +3050,12 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
} else if (itemToRemove.container >= 0) {
// The item may belong to a folder.
View parent = idToViewMap.get(itemToRemove.container);
if (parent != null) {
if (parent instanceof FolderIcon) {
FolderInfo folderInfo = (FolderInfo) parent.getTag();
folderInfo.prepareAutoUpdate();
folderInfo.remove((WorkspaceItemInfo) itemToRemove, false);
if (((FolderIcon) parent).getFolder().isOpen()) {
((FolderIcon) parent).getFolder().close(false /* animate */);
}
}
}
}
@ -3081,18 +3079,17 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
/**
* Map the operator over the shortcuts and widgets, return the first-non-null value.
*
* @param recurse true: iterate over folder children. false: op get the folders themselves.
* @param op the operator to map over the shortcuts
*/
public void mapOverItems(boolean recurse, ItemOperator op) {
public void mapOverItems(ItemOperator op) {
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
if (mapOverCellLayout(recurse, layout, op)) {
if (mapOverCellLayout(layout, op)) {
return;
}
}
}
private boolean mapOverCellLayout(boolean recurse, CellLayout layout, ItemOperator op) {
private boolean mapOverCellLayout(CellLayout layout, ItemOperator op) {
// TODO(b/128460496) Potential race condition where layout is not yet loaded
if (layout == null) {
return false;
@ -3102,103 +3099,68 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
final int itemCount = container.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = container.getChildAt(itemIdx);
ItemInfo info = (ItemInfo) item.getTag();
if (recurse && info instanceof FolderInfo && item instanceof FolderIcon) {
FolderIcon folder = (FolderIcon) item;
ArrayList<View> folderChildren = folder.getFolder().getIconsInReadingOrder();
// map over all the children in the folder
final int childCount = folderChildren.size();
for (int childIdx = 0; childIdx < childCount; childIdx++) {
View child = folderChildren.get(childIdx);
info = (ItemInfo) child.getTag();
if (op.evaluate(info, child)) {
return true;
}
}
} else {
if (op.evaluate(info, item)) {
return true;
}
if (op.evaluate((ItemInfo) item.getTag(), item)) {
return true;
}
}
return false;
}
void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
int total = shortcuts.size();
final HashSet<WorkspaceItemInfo> updates = new HashSet<>(total);
final IntSet folderIds = new IntSet();
final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
ItemOperator op = (info, v) -> {
if (v instanceof BubbleTextView && updates.contains(info)) {
WorkspaceItemInfo si = (WorkspaceItemInfo) info;
BubbleTextView shortcut = (BubbleTextView) v;
Drawable oldIcon = shortcut.getIcon();
boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
&& ((PreloadIconDrawable) oldIcon).hasNotCompleted();
shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
((FolderIcon) v).updatePreviewItems(updates::contains);
}
for (int i = 0; i < total; i++) {
WorkspaceItemInfo s = shortcuts.get(i);
updates.add(s);
folderIds.add(s.container);
// Iterate all items
return false;
};
mapOverItems(op);
Folder openFolder = Folder.getOpen(mLauncher);
if (openFolder != null) {
openFolder.iterateOverItems(op);
}
mapOverItems(MAP_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView &&
updates.contains(info)) {
WorkspaceItemInfo si = (WorkspaceItemInfo) info;
BubbleTextView shortcut = (BubbleTextView) v;
Drawable oldIcon = shortcut.getIcon();
boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
&& ((PreloadIconDrawable) oldIcon).hasNotCompleted();
shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
}
// process all the shortcuts
return false;
}
});
// Update folder icons
mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof FolderInfo && folderIds.contains(info.id)) {
((FolderInfo) info).itemsChanged(false);
}
// process all the shortcuts
return false;
}
});
}
public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
final IntSet folderIds = new IntSet();
mapOverItems(MAP_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
if (!packageUserKey.updateFromItemInfo(info)
|| updatedDots.test(packageUserKey)) {
((BubbleTextView) v).applyDotState(info, true /* animate */);
folderIds.add(info.container);
}
}
// process all the shortcuts
return false;
}
});
Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
|| updatedDots.test(packageUserKey);
// Update folder icons
mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof FolderInfo && folderIds.contains(info.id)
&& v instanceof FolderIcon) {
ItemOperator op = (info, v) -> {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
if (matcher.test(info)) {
((BubbleTextView) v).applyDotState(info, true /* animate */);
}
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
FolderInfo fi = (FolderInfo) info;
if (fi.contents.stream().anyMatch(matcher)) {
FolderDotInfo folderDotInfo = new FolderDotInfo();
for (WorkspaceItemInfo si : ((FolderInfo) info).contents) {
for (WorkspaceItemInfo si : fi.contents) {
folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
}
((FolderIcon) v).setDotInfo(folderDotInfo);
}
// process all the shortcuts
return false;
}
});
// process all the shortcuts
return false;
};
mapOverItems(op);
Folder folder = Folder.getOpen(mLauncher);
if (folder != null) {
folder.iterateOverItems(op);
}
}
public void removeAbandonedPromise(String packageName, UserHandle user) {
@ -3210,21 +3172,25 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
}
public void updateRestoreItems(final HashSet<ItemInfo> updates) {
mapOverItems(MAP_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
&& updates.contains(info)) {
((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
} else if (v instanceof PendingAppWidgetHostView
&& info instanceof LauncherAppWidgetInfo
&& updates.contains(info)) {
((PendingAppWidgetHostView) v).applyState();
}
// process all the shortcuts
return false;
ItemOperator op = (info, v) -> {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
&& updates.contains(info)) {
((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
} else if (v instanceof PendingAppWidgetHostView
&& info instanceof LauncherAppWidgetInfo
&& updates.contains(info)) {
((PendingAppWidgetHostView) v).applyState();
} else if (v instanceof FolderIcon && info instanceof FolderInfo) {
((FolderIcon) v).updatePreviewItems(updates::contains);
}
});
// process all the shortcuts
return false;
};
mapOverItems(op);
Folder folder = Folder.getOpen(mLauncher);
if (folder != null) {
folder.iterateOverItems(op);
}
}
public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
@ -3248,7 +3214,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
} else {
// widgetRefresh will automatically run when the packages are updated.
// For now just update the progress bars
mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
mapOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view) {
if (view instanceof PendingAppWidgetHostView
@ -3372,7 +3338,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
mRefreshPending = false;
ArrayList<PendingAppWidgetHostView> views = new ArrayList<>(mInfos.size());
mapOverItems(MAP_NO_RECURSE, (info, view) -> {
mapOverItems((info, view) -> {
if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) {
views.add((PendingAppWidgetHostView) view);
}

View File

@ -170,8 +170,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
private boolean mDeleteFolderOnDropCompleted = false;
private boolean mSuppressFolderDeletion = false;
private boolean mItemAddedBackToSelfViaIcon = false;
@Thunk float mFolderIconPivotX;
@Thunk float mFolderIconPivotY;
private boolean mIsEditingName = false;
@ViewDebug.ExportedProperty(category = "launcher")
@ -310,7 +308,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
// Convert to a string here to ensure that no other state associated with the text field
// gets saved.
String newTitle = mFolderName.getText().toString();
mInfo.setTitle(newTitle);
mInfo.title = newTitle;
mFolderIcon.onTitleChanged(newTitle);
mLauncher.getModelWriter().updateItemInDatabase(mInfo);
if (TextUtils.isEmpty(mInfo.title)) {
@ -385,7 +384,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
ArrayList<WorkspaceItemInfo> children = info.contents;
Collections.sort(children, ITEM_POS_COMPARATOR);
updateItemLocationsInDatabaseBatch();
mContent.bindItems(children);
DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
if (lp == null) {
@ -393,13 +391,10 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
lp.customPosition = true;
setLayoutParams(lp);
}
centerAboutIcon();
mItemsInvalidated = true;
updateTextViewFocus();
mInfo.addListener(this);
if (TextUtils.isEmpty(mInfo.title)) {
if (!TextUtils.isEmpty(mInfo.title)) {
mFolderName.setText(mInfo.title);
mFolderName.setHint(null);
} else {
@ -408,11 +403,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
}
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon.post(new Runnable() {
public void run() {
if (getItemCount() <= 1) {
replaceFolderWithFinalItem();
}
mFolderIcon.post(() -> {
if (getItemCount() <= 1) {
replaceFolderWithFinalItem();
}
});
}
@ -472,18 +465,50 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
return folderCount >= MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION;
}
/**
* Opens the folder as part of a drag operation
*/
public void beginExternalDrag() {
mIsExternalDrag = true;
mDragInProgress = true;
// Since this folder opened by another controller, it might not get onDrop or
// onDropComplete. Perform cleanup once drag-n-drop ends.
mDragController.addDragListener(this);
ArrayList<WorkspaceItemInfo> items = new ArrayList<>(mInfo.contents);
mEmptyCellRank = items.size();
items.add(null); // Add an empty spot at the end
animateOpen(items, mEmptyCellRank / mContent.itemsPerPage());
}
/**
* Opens the user folder described by the specified tag. The opening of the folder
* is animated relative to the specified View. If the View is null, no animation
* is played.
*/
public void animateOpen() {
animateOpen(mInfo.contents, 0);
}
/**
* Opens the user folder described by the specified tag. The opening of the folder
* is animated relative to the specified View. If the View is null, no animation
* is played.
*/
private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
Folder openFolder = getOpen(mLauncher);
if (openFolder != null && openFolder != this) {
// Close any open folder before opening a folder.
openFolder.close(true);
}
mContent.bindItems(items);
centerAboutIcon();
mItemsInvalidated = true;
updateTextViewFocus();
mIsOpen = true;
DragLayer dragLayer = mLauncher.getDragLayer();
@ -500,10 +525,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
}
mContent.completePendingPageChanges();
if (!mDragInProgress) {
// Open on the first page.
mContent.snapToPageImmediately(0);
}
mContent.snapToPageImmediately(pageNo);
// This is set to true in close(), but isn't reset to false until onDropCompleted(). This
// leads to an inconsistent state if you drag out of the folder and drag back in without
@ -574,16 +596,6 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
mContent.verifyVisibleHighResIcons(mContent.getNextPage());
}
public void beginExternalDrag() {
mEmptyCellRank = mContent.allocateRankForNewItem();
mIsExternalDrag = true;
mDragInProgress = true;
// Since this folder opened by another controller, it might not get onDrop or
// onDropComplete. Perform cleanup once drag-n-drop ends.
mDragController.addDragListener(this);
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_FOLDER) != 0;
@ -668,6 +680,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
} else if (mDragInProgress) {
mDeleteFolderOnDropCompleted = true;
}
} else if (!mDragInProgress) {
mContent.unbindItems();
}
mSuppressFolderDeletion = false;
clearDragInfo();
@ -953,24 +967,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
setPivotX(folderPivotX);
setPivotY(folderPivotY);
mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
(1.0f * folderPivotX / width));
mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
(1.0f * folderPivotY / height));
lp.width = width;
lp.height = height;
lp.x = left;
lp.y = top;
}
public float getPivotXForIconAnimation() {
return mFolderIconPivotX;
}
public float getPivotYForIconAnimation() {
return mFolderIconPivotY;
}
private int getContentAreaHeight() {
DeviceProfile grid = mLauncher.getDeviceProfile();
int maxContentAreaHeight = grid.availableHeightPx
@ -1031,7 +1033,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
}
public int getItemCount() {
return mContent.getItemCount();
return mInfo.contents.size();
}
@Thunk void replaceFolderWithFinalItem() {
@ -1039,7 +1041,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
Runnable onCompleteRunnable = new Runnable() {
@Override
public void run() {
int itemCount = mInfo.contents.size();
int itemCount = getItemCount();
if (itemCount <= 1) {
View newIcon = null;
@ -1116,6 +1118,8 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
return false;
}
});
} else {
setOnKeyListener(null);
}
}
@ -1242,9 +1246,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
item.cellY);
updateItemLocationsInDatabaseBatch();
ArrayList<View> items = new ArrayList<>(getIconsInReadingOrder());
items.add(rank, mContent.createAndAddViewForRank(item, rank));
mContent.arrangeChildren(items);
if (mContent.areViewsBound()) {
mContent.createAndAddViewForRank(item, rank);
}
mItemsInvalidated = true;
}
@ -1275,12 +1279,11 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
updateTextViewFocus();
}
@Override
public void prepareAutoUpdate() {
close(false);
}
public void onTitleChanged(CharSequence title) {
/**
* Utility methods to iterate over items of the view
*/
public void iterateOverItems(ItemOperator op) {
mContent.iterateOverItems(op);
}
/**
@ -1289,14 +1292,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
public ArrayList<View> getIconsInReadingOrder() {
if (mItemsInvalidated) {
mItemsInReadingOrder.clear();
mContent.iterateOverItems(new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view) {
mItemsInReadingOrder.add(view);
return false;
}
});
mContent.iterateOverItems((i, v) -> !mItemsInReadingOrder.add(v));
mItemsInvalidated = false;
}
return mItemsInReadingOrder;

View File

@ -69,6 +69,7 @@ import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* An icon that can appear on in the workspace representing an {@link Folder}.
@ -99,7 +100,7 @@ public class FolderIcon extends FrameLayout implements FolderListener {
ClippedFolderIconLayoutRule mPreviewLayoutRule;
private PreviewItemManager mPreviewItemManager;
private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
private List<BubbleTextView> mCurrentPreviewItems = new ArrayList<>();
private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
boolean mAnimating = false;
@ -255,7 +256,6 @@ public class FolderIcon extends FrameLayout implements FolderListener {
OnAlarmListener mOnOpenListener = new OnAlarmListener() {
public void onAlarm(Alarm alarm) {
mFolder.beginExternalDrag();
mFolder.animateOpen();
}
};
@ -321,18 +321,17 @@ public class FolderIcon extends FrameLayout implements FolderListener {
int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1);
boolean itemAdded = false;
if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) {
List<BubbleTextView> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
mInfo.add(item, index, false);
mCurrentPreviewItems.clear();
mCurrentPreviewItems.addAll(getPreviewIconsOnPage(0));
mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
if (!oldPreviewItems.equals(mCurrentPreviewItems)) {
for (int i = 0; i < mCurrentPreviewItems.size(); ++i) {
if (mCurrentPreviewItems.get(i).getTag().equals(item)) {
// If the item dropped is going to be in the preview, we update the
// index here to reflect its position in the preview.
index = i;
}
int newIndex = mCurrentPreviewItems.indexOf(item);
if (newIndex >= 0) {
// If the item dropped is going to be in the preview, we update the
// index here to reflect its position in the preview.
index = newIndex;
}
mPreviewItemManager.hidePreviewItem(index, true);
@ -531,11 +530,10 @@ public class FolderIcon extends FrameLayout implements FolderListener {
}
/**
* Returns the list of "preview items" on {@param page}.
* Returns the list of items which should be visible in the preview
*/
public List<BubbleTextView> getPreviewIconsOnPage(int page) {
return mPreviewVerifier.setFolderInfo(mFolder.mInfo)
.previewItemsForPage(page, mFolder.getIconsInReadingOrder());
public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
}
@Override
@ -553,11 +551,14 @@ public class FolderIcon extends FrameLayout implements FolderListener {
private void updatePreviewItems(boolean animate) {
mPreviewItemManager.updatePreviewItems(animate);
mCurrentPreviewItems.clear();
mCurrentPreviewItems.addAll(getPreviewIconsOnPage(0));
mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
}
@Override
public void prepareAutoUpdate() {
/**
* Updates the preview items which match the provided condition
*/
public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
mPreviewItemManager.updatePreviewItems(itemCheck);
}
@Override
@ -580,7 +581,6 @@ public class FolderIcon extends FrameLayout implements FolderListener {
requestLayout();
}
@Override
public void onTitleChanged(CharSequence title) {
mFolderName.setText(title);
setContentDescription(getContext().getString(R.string.folder_name_format, title));

View File

@ -24,10 +24,10 @@ import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewDebug;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
@ -46,11 +46,14 @@ import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.ViewCache;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
public class FolderPagedView extends PagedView<PageIndicatorDots> {
@ -69,12 +72,12 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
public final boolean mIsRtl;
private final LayoutInflater mInflater;
private final ViewGroupFocusHelper mFocusIndicatorHelper;
@Thunk final ArrayMap<View, Runnable> mPendingAnimations = new ArrayMap<>();
private final FolderGridOrganizer mOrganizer;
private final ViewCache mViewCache;
private int mAllocatedContentSize;
@ViewDebug.ExportedProperty(category = "launcher")
@ -84,17 +87,20 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
private Folder mFolder;
// If the views are attached to the folder or not. A folder should be bound when its
// animating or is open.
private boolean mViewsBound = false;
public FolderPagedView(Context context, AttributeSet attrs) {
super(context, attrs);
InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
mOrganizer = new FolderGridOrganizer(profile);
mInflater = LayoutInflater.from(context);
mIsRtl = Utilities.isRtl(getResources());
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
mViewCache = BaseActivity.fromContext(context).getViewCache();
}
public void setFolder(Folder folder) {
@ -127,35 +133,50 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
/**
* Binds items to the layout.
*/
public void bindItems(ArrayList<WorkspaceItemInfo> items) {
ArrayList<View> icons = new ArrayList<>();
for (WorkspaceItemInfo item : items) {
icons.add(createNewView(item));
public void bindItems(List<WorkspaceItemInfo> items) {
if (mViewsBound) {
unbindItems();
}
arrangeChildren(icons);
}
public void allocateSpaceForRank(int rank) {
ArrayList<View> views = new ArrayList<>(mFolder.getIconsInReadingOrder());
views.add(rank, null);
arrangeChildren(views);
arrangeChildren(items.stream().map(this::createNewView).collect(Collectors.toList()));
mViewsBound = true;
}
/**
* Create space for a new item at the end, and returns the rank for that item.
* Also sets the current page to the last page.
* Removes all the icons from the folder
*/
public int allocateRankForNewItem() {
int rank = getItemCount();
allocateSpaceForRank(rank);
setCurrentPage(rank / mOrganizer.getMaxItemsPerPage());
return rank;
public void unbindItems() {
for (int i = getChildCount() - 1; i >= 0; i--) {
CellLayout page = (CellLayout) getChildAt(i);
ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
for (int j = container.getChildCount() - 1; j >= 0; j--) {
mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
}
page.removeAllViews();
mViewCache.recycleView(R.layout.folder_page, page);
}
removeAllViews();
mViewsBound = false;
}
/**
* Returns true if the icons are bound to the folder
*/
public boolean areViewsBound() {
return mViewsBound;
}
/**
* Creates and adds an icon corresponding to the provided rank
* @return the created icon
*/
public View createAndAddViewForRank(WorkspaceItemInfo item, int rank) {
View icon = createNewView(item);
allocateSpaceForRank(rank);
addViewForRank(icon, item, rank);
if (!mViewsBound) {
return icon;
}
ArrayList<View> views = new ArrayList<>(mFolder.getIconsInReadingOrder());
views.add(rank, icon);
arrangeChildren(views);
return icon;
}
@ -173,16 +194,24 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
@SuppressLint("InflateParams")
public View createNewView(WorkspaceItemInfo item) {
final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
R.layout.folder_application, null, false);
if (item == null) {
return null;
}
final BubbleTextView textView = mViewCache.getView(
R.layout.folder_application, getContext(), null);
textView.applyFromWorkspaceItem(item);
textView.setHapticFeedbackEnabled(false);
textView.setOnClickListener(ItemClickHandler.INSTANCE);
textView.setOnLongClickListener(mFolder);
textView.setOnFocusChangeListener(mFocusIndicatorHelper);
textView.setLayoutParams(new CellLayout.LayoutParams(
item.cellX, item.cellY, item.spanX, item.spanY));
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) textView.getLayoutParams();
if (lp == null) {
textView.setLayoutParams(new CellLayout.LayoutParams(
item.cellX, item.cellY, item.spanX, item.spanY));
} else {
lp.cellX = item.cellX;
lp.cellY = item.cellY;
lp.cellHSpan = lp.cellVSpan = 1;
}
return textView;
}
@ -197,7 +226,7 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
private CellLayout createAndAddNewPage() {
DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
CellLayout page = (CellLayout) mInflater.inflate(R.layout.folder_page, this, false);
CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
page.setInvertIfRtl(true);
@ -240,7 +269,7 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
* @param list the ordered list of children.
*/
@SuppressLint("RtlHardcoded")
public void arrangeChildren(ArrayList<View> list) {
public void arrangeChildren(List<View> list) {
int itemCount = list.size();
ArrayList<CellLayout> pages = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
@ -313,16 +342,6 @@ public class FolderPagedView extends PagedView<PageIndicatorDots> {
(getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
}
public int getItemCount() {
int lastPageIndex = getChildCount() - 1;
if (lastPageIndex < 0) {
// If there are no pages, nothing has yet been added to the folder.
return 0;
}
return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
+ lastPageIndex * mOrganizer.getMaxItemsPerPage();
}
/**
* @return the rank of the cell nearest to the provided pixel position.
*/

View File

@ -17,6 +17,8 @@ package com.android.launcher3.folder;
import android.graphics.drawable.Drawable;
import com.android.launcher3.WorkspaceItemInfo;
/**
* Manages the parameters used to draw a Folder preview item.
*/
@ -25,9 +27,10 @@ class PreviewItemDrawingParams {
float transY;
float scale;
float overlayAlpha;
FolderPreviewItemAnim anim;
public FolderPreviewItemAnim anim;
public boolean hidden;
Drawable drawable;
public Drawable drawable;
public WorkspaceItemInfo item;
PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
this.transX = transX;

View File

@ -23,28 +23,51 @@ import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.graphics.DrawableFactory;
import com.android.launcher3.graphics.PreloadIconDrawable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}.
*/
public class PreviewItemManager {
private FolderIcon mIcon;
private static final FloatProperty<PreviewItemManager> CURRENT_PAGE_ITEMS_TRANS_X =
new FloatProperty<PreviewItemManager>("currentPageItemsTransX") {
@Override
public void setValue(PreviewItemManager manager, float v) {
manager.mCurrentPageItemsTransX = v;
manager.onParamsChanged();
}
@Override
public Float get(PreviewItemManager manager) {
return manager.mCurrentPageItemsTransX;
}
};
private final Context mContext;
private final FolderIcon mIcon;
private final DrawableFactory mDrawableFactory;
private final int mIconSize;
// These variables are all associated with the drawing of the preview; they are stored
// as member variables for shared usage and to avoid computation on each frame
@ -69,7 +92,10 @@ public class PreviewItemManager {
private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200;
public PreviewItemManager(FolderIcon icon) {
mContext = icon.getContext();
mIcon = icon;
mDrawableFactory = DrawableFactory.INSTANCE.get(mContext);
mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
}
/**
@ -200,7 +226,7 @@ public class PreviewItemManager {
}
void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
List<BubbleTextView> items = mIcon.getPreviewIconsOnPage(page);
List<WorkspaceItemInfo> items = mIcon.getPreviewItemsOnPage(page);
int prevNumItems = params.size();
// We adjust the size of the list to match the number of items in the preview.
@ -214,13 +240,7 @@ public class PreviewItemManager {
int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
for (int i = 0; i < params.size(); i++) {
PreviewItemDrawingParams p = params.get(i);
p.drawable = items.get(i).getCompoundDrawables()[1];
if (p.drawable != null && !mIcon.mFolder.isOpen()) {
// Set the callback to FolderIcon as it is responsible to drawing the icon. The
// callback will be released when the folder is opened.
p.drawable.setCallback(mIcon);
}
setDrawable(p, items.get(i));
if (!animate) {
computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
@ -253,14 +273,8 @@ public class PreviewItemManager {
buildParamsForPage(currentPage, mCurrentPageParams, false);
onParamsChanged();
ValueAnimator slideAnimator = ValueAnimator.ofFloat(0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
slideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentPageItemsTransX = (float) valueAnimator.getAnimatedValue();
onParamsChanged();
}
});
ValueAnimator slideAnimator = ObjectAnimator
.ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
slideAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@ -277,6 +291,25 @@ public class PreviewItemManager {
buildParamsForPage(0, mFirstPageParams, animate);
}
void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
boolean modified = false;
for (PreviewItemDrawingParams param : mFirstPageParams) {
if (itemCheck.test(param.item)) {
setDrawable(param, param.item);
modified = true;
}
}
for (PreviewItemDrawingParams param : mCurrentPageParams) {
if (itemCheck.test(param.item)) {
setDrawable(param, param.item);
modified = true;
}
}
if (modified) {
mIcon.invalidate();
}
}
boolean verifyDrawable(@NonNull Drawable who) {
for (int i = 0; i < mFirstPageParams.size(); i++) {
if (mFirstPageParams.get(i).drawable == who) {
@ -296,46 +329,46 @@ public class PreviewItemManager {
* - Moving into a new position
* - Moving out of the preview
*
* @param oldParams The list of items in the old preview.
* @param newParams The list of items in the new preview.
* @param oldItems The list of items in the old preview.
* @param newItems The list of items in the new preview.
* @param dropped The item that was dropped onto the FolderIcon.
*/
public void onDrop(List<BubbleTextView> oldParams, List<BubbleTextView> newParams,
public void onDrop(List<WorkspaceItemInfo> oldItems, List<WorkspaceItemInfo> newItems,
WorkspaceItemInfo dropped) {
int numItems = newParams.size();
int numItems = newItems.size();
final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
buildParamsForPage(0, params, false);
// New preview items for items that are moving in (except for the dropped item).
List<BubbleTextView> moveIn = new ArrayList<>();
for (BubbleTextView btv : newParams) {
if (!oldParams.contains(btv) && !btv.getTag().equals(dropped)) {
moveIn.add(btv);
List<WorkspaceItemInfo> moveIn = new ArrayList<>();
for (WorkspaceItemInfo newItem : newItems) {
if (!oldItems.contains(newItem) && !newItem.equals(dropped)) {
moveIn.add(newItem);
}
}
for (int i = 0; i < moveIn.size(); ++i) {
int prevIndex = newParams.indexOf(moveIn.get(i));
int prevIndex = newItems.indexOf(moveIn.get(i));
PreviewItemDrawingParams p = params.get(prevIndex);
computePreviewItemDrawingParams(prevIndex, numItems, p);
updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newParams.indexOf(moveIn.get(i)),
updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)),
numItems);
}
// Items that are moving into new positions within the preview.
for (int newIndex = 0; newIndex < newParams.size(); ++newIndex) {
int oldIndex = oldParams.indexOf(newParams.get(newIndex));
for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) {
int oldIndex = oldItems.indexOf(newItems.get(newIndex));
if (oldIndex >= 0 && newIndex != oldIndex) {
PreviewItemDrawingParams p = params.get(newIndex);
updateTransitionParam(p, newParams.get(newIndex), oldIndex, newIndex, numItems);
updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems);
}
}
// Old preview items that need to be moved out.
List<BubbleTextView> moveOut = new ArrayList<>(oldParams);
moveOut.removeAll(newParams);
List<WorkspaceItemInfo> moveOut = new ArrayList<>(oldItems);
moveOut.removeAll(newItems);
for (int i = 0; i < moveOut.size(); ++i) {
BubbleTextView item = moveOut.get(i);
int oldIndex = oldParams.indexOf(item);
WorkspaceItemInfo item = moveOut.get(i);
int oldIndex = oldItems.indexOf(item);
PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null);
updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems);
params.add(0, p); // We want these items first so that they are on drawn last.
@ -348,14 +381,9 @@ public class PreviewItemManager {
}
}
private void updateTransitionParam(final PreviewItemDrawingParams p, BubbleTextView btv,
private void updateTransitionParam(final PreviewItemDrawingParams p, WorkspaceItemInfo item,
int prevIndex, int newIndex, int numItems) {
p.drawable = btv.getCompoundDrawables()[1];
if (!mIcon.mFolder.isOpen()) {
// Set the callback to FolderIcon as it is responsible to drawing the icon. The
// callback will be released when the folder is opened.
p.drawable.setCallback(mIcon);
}
setDrawable(p, item);
FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems,
newIndex, numItems, DROP_IN_ANIMATION_DURATION, null);
@ -364,4 +392,20 @@ public class PreviewItemManager {
}
p.anim = anim;
}
private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
if (item.hasPromiseIconUi()) {
PreloadIconDrawable drawable = mDrawableFactory.newPendingIcon(mContext, item);
drawable.setLevel(item.getInstallProgress());
p.drawable = drawable;
} else {
p.drawable = mDrawableFactory.newIcon(mContext, item);
}
p.drawable.setBounds(0, 0, mIconSize, mIconSize);
p.item = item;
// Set the callback to FolderIcon as it is responsible to drawing the icon. The
// callback will be released when the folder is opened.
p.drawable.setCallback(mIcon);
}
}