| /* |
| * Copyright (C) 2008 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.folder; |
| |
| import static com.android.launcher3.Flags.enableCursorHoverStates; |
| import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; |
| import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; |
| import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Looper; |
| import android.util.AttributeSet; |
| import android.util.Property; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewDebug; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.app.animation.Interpolators; |
| import com.android.launcher3.Alarm; |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.CellLayout; |
| import com.android.launcher3.CheckLongPressHelper; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.DropTarget.DragObject; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherSettings; |
| import com.android.launcher3.OnAlarmListener; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Reorderable; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.Workspace; |
| import com.android.launcher3.allapps.ActivityAllAppsContainerView; |
| import com.android.launcher3.celllayout.CellLayoutLayoutParams; |
| import com.android.launcher3.dot.FolderDotInfo; |
| import com.android.launcher3.dragndrop.BaseItemDragListener; |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.dragndrop.DragView; |
| import com.android.launcher3.dragndrop.DraggableView; |
| import com.android.launcher3.icons.DotRenderer; |
| import com.android.launcher3.logger.LauncherAtom.FromState; |
| import com.android.launcher3.logger.LauncherAtom.ToState; |
| import com.android.launcher3.logging.InstanceId; |
| import com.android.launcher3.logging.StatsLogManager; |
| import com.android.launcher3.model.data.FolderInfo; |
| import com.android.launcher3.model.data.FolderInfo.FolderListener; |
| import com.android.launcher3.model.data.FolderInfo.LabelState; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.model.data.WorkspaceItemFactory; |
| import com.android.launcher3.model.data.WorkspaceItemInfo; |
| import com.android.launcher3.util.Executors; |
| import com.android.launcher3.util.MultiTranslateDelegate; |
| import com.android.launcher3.util.Thunk; |
| import com.android.launcher3.views.ActivityContext; |
| import com.android.launcher3.views.IconLabelDotView; |
| 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}. |
| */ |
| public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView, |
| DraggableView, Reorderable { |
| |
| private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); |
| @Thunk ActivityContext mActivity; |
| @Thunk Folder mFolder; |
| public FolderInfo mInfo; |
| |
| private CheckLongPressHelper mLongPressHelper; |
| |
| static final int DROP_IN_ANIMATION_DURATION = 400; |
| |
| // Flag whether the folder should open itself when an item is dragged over is enabled. |
| public static final boolean SPRING_LOADING_ENABLED = true; |
| |
| // Delay when drag enters until the folder opens, in miliseconds. |
| private static final int ON_OPEN_DELAY = 800; |
| |
| @Thunk BubbleTextView mFolderName; |
| |
| PreviewBackground mBackground = new PreviewBackground(); |
| private boolean mBackgroundIsVisible = true; |
| |
| FolderGridOrganizer mPreviewVerifier; |
| ClippedFolderIconLayoutRule mPreviewLayoutRule; |
| private PreviewItemManager mPreviewItemManager; |
| private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); |
| private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>(); |
| |
| boolean mAnimating = false; |
| |
| private Alarm mOpenAlarm = new Alarm(Looper.getMainLooper()); |
| |
| private boolean mForceHideDot; |
| @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) |
| private FolderDotInfo mDotInfo = new FolderDotInfo(); |
| private DotRenderer mDotRenderer; |
| @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) |
| private DotRenderer.DrawParams mDotParams; |
| private float mDotScale; |
| private Animator mDotScaleAnim; |
| |
| private Rect mTouchArea = new Rect(); |
| |
| private float mScaleForReorderBounce = 1f; |
| |
| private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY |
| = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") { |
| @Override |
| public Float get(FolderIcon folderIcon) { |
| return folderIcon.mDotScale; |
| } |
| |
| @Override |
| public void set(FolderIcon folderIcon, Float value) { |
| folderIcon.mDotScale = value; |
| folderIcon.invalidate(); |
| } |
| }; |
| |
| public FolderIcon(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| init(); |
| } |
| |
| public FolderIcon(Context context) { |
| super(context); |
| init(); |
| } |
| |
| private void init() { |
| mLongPressHelper = new CheckLongPressHelper(this); |
| mPreviewLayoutRule = new ClippedFolderIconLayoutRule(); |
| mPreviewItemManager = new PreviewItemManager(this); |
| mDotParams = new DotRenderer.DrawParams(); |
| } |
| |
| public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId, |
| T activityContext, ViewGroup group, FolderInfo folderInfo) { |
| Folder folder = Folder.fromXml(activityContext); |
| |
| FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo); |
| folder.setFolderIcon(icon); |
| folder.bind(folderInfo); |
| icon.setFolder(folder); |
| return icon; |
| } |
| |
| /** |
| * Builds a FolderIcon to be added to the Launcher |
| */ |
| public static FolderIcon inflateIcon(int resId, ActivityContext activity, |
| @Nullable ViewGroup group, FolderInfo folderInfo) { |
| @SuppressWarnings("all") // suppress dead code warning |
| final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; |
| if (error) { |
| throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + |
| "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + |
| "is dependent on this"); |
| } |
| |
| DeviceProfile grid = activity.getDeviceProfile(); |
| LayoutInflater inflater = (group != null) |
| ? LayoutInflater.from(group.getContext()) |
| : activity.getLayoutInflater(); |
| FolderIcon icon = (FolderIcon) inflater.inflate(resId, group, false); |
| |
| icon.setClipToPadding(false); |
| icon.mFolderName = icon.findViewById(R.id.folder_icon_name); |
| icon.mFolderName.setText(folderInfo.title); |
| icon.mFolderName.setCompoundDrawablePadding(0); |
| FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); |
| lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; |
| |
| icon.setTag(folderInfo); |
| icon.setOnClickListener(activity.getItemOnClickListener()); |
| icon.mInfo = folderInfo; |
| icon.mActivity = activity; |
| icon.mDotRenderer = grid.mDotRendererWorkSpace; |
| |
| icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title)); |
| |
| // Keep the notification dot up to date with the sum of all the content's dots. |
| FolderDotInfo folderDotInfo = new FolderDotInfo(); |
| for (WorkspaceItemInfo si : folderInfo.contents) { |
| folderDotInfo.addDotInfo(activity.getDotInfoForItem(si)); |
| } |
| icon.setDotInfo(folderDotInfo); |
| |
| icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); |
| |
| icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile()); |
| icon.mPreviewVerifier.setFolderInfo(folderInfo); |
| icon.updatePreviewItems(false); |
| |
| folderInfo.addListener(icon); |
| |
| return icon; |
| } |
| |
| public void animateBgShadowAndStroke() { |
| mBackground.fadeInBackgroundShadow(); |
| mBackground.animateBackgroundStroke(); |
| } |
| |
| public BubbleTextView getFolderName() { |
| return mFolderName; |
| } |
| |
| public void getPreviewBounds(Rect outBounds) { |
| mPreviewItemManager.recomputePreviewDrawingParams(); |
| mBackground.getBounds(outBounds); |
| // The preview items go outside of the bounds of the background. |
| Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR); |
| } |
| |
| public float getBackgroundStrokeWidth() { |
| return mBackground.getStrokeWidth(); |
| } |
| |
| public Folder getFolder() { |
| return mFolder; |
| } |
| |
| private void setFolder(Folder folder) { |
| mFolder = folder; |
| } |
| |
| private boolean willAcceptItem(ItemInfo item) { |
| final int itemType = item.itemType; |
| return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || |
| itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && |
| item != mInfo && !mFolder.isOpen()); |
| } |
| |
| public boolean acceptDrop(ItemInfo dragInfo) { |
| return !mFolder.isDestroyed() && willAcceptItem(dragInfo); |
| } |
| |
| public void addItem(WorkspaceItemInfo item) { |
| mInfo.add(item, true); |
| } |
| |
| public void removeItem(WorkspaceItemInfo item, boolean animate) { |
| mInfo.remove(item, animate); |
| } |
| |
| public void onDragEnter(ItemInfo dragInfo) { |
| if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return; |
| CellLayoutLayoutParams lp = (CellLayoutLayoutParams) getLayoutParams(); |
| CellLayout cl = (CellLayout) getParent().getParent(); |
| |
| mBackground.animateToAccept(cl, lp.getCellX(), lp.getCellY()); |
| mOpenAlarm.setOnAlarmListener(mOnOpenListener); |
| if (SPRING_LOADING_ENABLED && |
| ((dragInfo instanceof WorkspaceItemFactory) |
| || (dragInfo instanceof WorkspaceItemInfo) |
| || (dragInfo instanceof PendingAddShortcutInfo))) { |
| mOpenAlarm.setAlarm(ON_OPEN_DELAY); |
| } |
| } |
| |
| OnAlarmListener mOnOpenListener = new OnAlarmListener() { |
| public void onAlarm(Alarm alarm) { |
| mFolder.beginExternalDrag(); |
| } |
| }; |
| |
| public Drawable prepareCreateAnimation(final View destView) { |
| return mPreviewItemManager.prepareCreateAnimation(destView); |
| } |
| |
| public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, |
| final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, |
| float scaleRelativeToDragLayer) { |
| final DragView srcView = d.dragView; |
| prepareCreateAnimation(destView); |
| addItem(destInfo); |
| // This will animate the first item from it's position as an icon into its |
| // position as the first item in the preview |
| mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null) |
| .start(); |
| |
| // This will animate the dragView (srcView) into the new folder |
| onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1, |
| false /* itemReturnedOnFailedDrop */); |
| } |
| |
| public void performDestroyAnimation(Runnable onCompleteRunnable) { |
| // This will animate the final item in the preview to be full size. |
| mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable) |
| .start(); |
| } |
| |
| public void onDragExit() { |
| mBackground.animateToRest(); |
| mOpenAlarm.cancelAlarm(); |
| } |
| |
| private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, |
| float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) { |
| item.cellX = -1; |
| item.cellY = -1; |
| DragView animateView = d.dragView; |
| // Typically, the animateView corresponds to the DragView; however, if this is being done |
| // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we |
| // will not have a view to animate |
| if (animateView != null && mActivity instanceof Launcher) { |
| final Launcher launcher = (Launcher) mActivity; |
| DragLayer dragLayer = launcher.getDragLayer(); |
| Rect to = finalRect; |
| if (to == null) { |
| to = new Rect(); |
| Workspace<?> workspace = launcher.getWorkspace(); |
| // Set cellLayout and this to it's final state to compute final animation locations |
| workspace.setFinalTransitionTransform(); |
| float scaleX = getScaleX(); |
| float scaleY = getScaleY(); |
| setScaleX(1.0f); |
| setScaleY(1.0f); |
| scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); |
| // Finished computing final animation locations, restore current state |
| setScaleX(scaleX); |
| setScaleY(scaleY); |
| workspace.resetTransitionTransform(); |
| } |
| |
| int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1); |
| boolean itemAdded = false; |
| if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) { |
| List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); |
| mInfo.add(item, index, false); |
| mCurrentPreviewItems.clear(); |
| mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); |
| |
| if (!oldPreviewItems.equals(mCurrentPreviewItems)) { |
| 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); |
| mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item); |
| itemAdded = true; |
| } else { |
| removeItem(item, false); |
| } |
| } |
| |
| if (!itemAdded) { |
| mInfo.add(item, index, true); |
| } |
| |
| int[] center = new int[2]; |
| float scale = getLocalCenterForIndex(index, numItemsInPreview, center); |
| center[0] = Math.round(scaleRelativeToDragLayer * center[0]); |
| center[1] = Math.round(scaleRelativeToDragLayer * center[1]); |
| |
| to.offset(center[0] - animateView.getMeasuredWidth() / 2, |
| center[1] - animateView.getMeasuredHeight() / 2); |
| |
| float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f; |
| |
| float finalScale = scale * scaleRelativeToDragLayer; |
| |
| // Account for potentially different icon sizes with non-default grid settings |
| if (d.dragSource instanceof ActivityAllAppsContainerView) { |
| DeviceProfile grid = mActivity.getDeviceProfile(); |
| float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx); |
| finalScale *= containerScale; |
| } |
| |
| final int finalIndex = index; |
| dragLayer.animateView(animateView, to, finalAlpha, |
| finalScale, finalScale, DROP_IN_ANIMATION_DURATION, |
| Interpolators.DECELERATE_2, |
| () -> { |
| mPreviewItemManager.hidePreviewItem(finalIndex, false); |
| mFolder.showItem(item); |
| }, |
| DragLayer.ANIMATION_END_DISAPPEAR, null); |
| |
| mFolder.hideItem(item); |
| |
| if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true); |
| |
| FolderNameInfos nameInfos = new FolderNameInfos(); |
| Executors.MODEL_EXECUTOR.post(() -> { |
| d.folderNameProvider.getSuggestedFolderName( |
| getContext(), mInfo.contents, nameInfos); |
| postDelayed(() -> { |
| setLabelSuggestion(nameInfos, d.logInstanceId); |
| invalidate(); |
| }, DROP_IN_ANIMATION_DURATION); |
| }); |
| } else { |
| addItem(item); |
| } |
| } |
| |
| /** |
| * Set the suggested folder name. |
| */ |
| public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) { |
| if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) { |
| return; |
| } |
| if (nameInfos == null || !nameInfos.hasSuggestions()) { |
| StatsLogManager.newInstance(getContext()).logger() |
| .withInstanceId(instanceId) |
| .withItemInfo(mInfo) |
| .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS); |
| return; |
| } |
| if (!nameInfos.hasPrimary()) { |
| StatsLogManager.newInstance(getContext()).logger() |
| .withInstanceId(instanceId) |
| .withItemInfo(mInfo) |
| .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY); |
| return; |
| } |
| CharSequence newTitle = nameInfos.getLabels()[0]; |
| FromState fromState = mInfo.getFromLabelState(); |
| |
| mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter()); |
| onTitleChanged(mInfo.title); |
| mFolder.mFolderName.setText(mInfo.title); |
| |
| // Logging for folder creation flow |
| StatsLogManager.newInstance(getContext()).logger() |
| .withInstanceId(instanceId) |
| .withItemInfo(mInfo) |
| .withFromState(fromState) |
| .withToState(ToState.TO_SUGGESTION0) |
| // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter, |
| // event is assumed to be folder creation on the server side. |
| .withEditText(newTitle.toString()) |
| .log(LAUNCHER_FOLDER_AUTO_LABELED); |
| } |
| |
| |
| public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { |
| WorkspaceItemInfo item; |
| if (d.dragInfo instanceof WorkspaceItemFactory) { |
| // Came from all apps -- make a copy |
| item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext()); |
| } else if (d.dragSource instanceof BaseItemDragListener){ |
| // Came from a different window -- make a copy |
| item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); |
| } else { |
| item = (WorkspaceItemInfo) d.dragInfo; |
| } |
| mFolder.notifyDrop(); |
| onDrop(item, d, null, 1.0f, |
| itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(), |
| itemReturnedOnFailedDrop |
| ); |
| } |
| |
| public void setDotInfo(FolderDotInfo dotInfo) { |
| updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot()); |
| mDotInfo = dotInfo; |
| } |
| |
| public ClippedFolderIconLayoutRule getLayoutRule() { |
| return mPreviewLayoutRule; |
| } |
| |
| @Override |
| public void setForceHideDot(boolean forceHideDot) { |
| if (mForceHideDot == forceHideDot) { |
| return; |
| } |
| mForceHideDot = forceHideDot; |
| |
| if (forceHideDot) { |
| invalidate(); |
| } else if (hasDot()) { |
| animateDotScale(0, 1); |
| } |
| } |
| |
| /** |
| * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false |
| * (the dot is being added or removed). |
| */ |
| private void updateDotScale(boolean wasDotted, boolean isDotted) { |
| float newDotScale = isDotted ? 1f : 0f; |
| // Animate when a dot is first added or when it is removed. |
| if ((wasDotted ^ isDotted) && isShown()) { |
| animateDotScale(newDotScale); |
| } else { |
| cancelDotScaleAnim(); |
| mDotScale = newDotScale; |
| invalidate(); |
| } |
| } |
| |
| private void cancelDotScaleAnim() { |
| if (mDotScaleAnim != null) { |
| mDotScaleAnim.cancel(); |
| } |
| } |
| |
| public void animateDotScale(float... dotScales) { |
| cancelDotScaleAnim(); |
| mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); |
| mDotScaleAnim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mDotScaleAnim = null; |
| } |
| }); |
| mDotScaleAnim.start(); |
| } |
| |
| public boolean hasDot() { |
| return mDotInfo != null && mDotInfo.hasDot(); |
| } |
| |
| private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { |
| mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams( |
| Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams); |
| |
| mTmpParams.transX += mBackground.basePreviewOffsetX; |
| mTmpParams.transY += mBackground.basePreviewOffsetY; |
| |
| float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize(); |
| float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2; |
| float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2; |
| |
| center[0] = Math.round(offsetX); |
| center[1] = Math.round(offsetY); |
| return mTmpParams.scale; |
| } |
| |
| public void setFolderBackground(PreviewBackground bg) { |
| mBackground = bg; |
| mBackground.setInvalidateDelegate(this); |
| } |
| |
| @Override |
| public void setIconVisible(boolean visible) { |
| mBackgroundIsVisible = visible; |
| invalidate(); |
| } |
| |
| public boolean getIconVisible() { |
| return mBackgroundIsVisible; |
| } |
| |
| public PreviewBackground getFolderBackground() { |
| return mBackground; |
| } |
| |
| public PreviewItemManager getPreviewItemManager() { |
| return mPreviewItemManager; |
| } |
| |
| @Override |
| protected void dispatchDraw(Canvas canvas) { |
| super.dispatchDraw(canvas); |
| |
| if (!mBackgroundIsVisible) return; |
| |
| mPreviewItemManager.recomputePreviewDrawingParams(); |
| |
| if (!mBackground.drawingDelegated()) { |
| mBackground.drawBackground(canvas); |
| } |
| |
| if (mCurrentPreviewItems.isEmpty() && !mAnimating) return; |
| |
| mPreviewItemManager.draw(canvas); |
| |
| if (!mBackground.drawingDelegated()) { |
| mBackground.drawBackgroundStroke(canvas); |
| } |
| |
| drawDot(canvas); |
| } |
| |
| public void drawDot(Canvas canvas) { |
| if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) { |
| Rect iconBounds = mDotParams.iconBounds; |
| // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered |
| int iconSize = mActivity.getDeviceProfile().iconSizePx; |
| iconBounds.left = (getWidth() - iconSize) / 2; |
| iconBounds.right = iconBounds.left + iconSize; |
| iconBounds.top = getPaddingTop(); |
| iconBounds.bottom = iconBounds.top + iconSize; |
| |
| float iconScale = (float) mBackground.previewSize / iconSize; |
| Utilities.scaleRectAboutCenter(iconBounds, iconScale); |
| |
| // If we are animating to the accepting state, animate the dot out. |
| mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); |
| mDotParams.dotColor = mBackground.getDotColor(); |
| mDotRenderer.draw(canvas, mDotParams); |
| } |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| boolean shouldCenterIcon = mActivity.getDeviceProfile().iconCenterVertically; |
| if (shouldCenterIcon) { |
| int iconSize = mActivity.getDeviceProfile().iconSizePx; |
| Paint.FontMetrics fm = mFolderName.getPaint().getFontMetrics(); |
| int cellHeightPx = iconSize + mFolderName.getCompoundDrawablePadding() |
| + (int) Math.ceil(fm.bottom - fm.top); |
| setPadding(getPaddingLeft(), (MeasureSpec.getSize(heightMeasureSpec) |
| - cellHeightPx) / 2, getPaddingRight(), getPaddingBottom()); |
| } |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| /** Sets the visibility of the icon's title text */ |
| public void setTextVisible(boolean visible) { |
| if (visible) { |
| mFolderName.setVisibility(VISIBLE); |
| } else { |
| mFolderName.setVisibility(INVISIBLE); |
| } |
| } |
| |
| public boolean getTextVisible() { |
| return mFolderName.getVisibility() == VISIBLE; |
| } |
| |
| /** |
| * Returns the list of items which should be visible in the preview |
| */ |
| public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) { |
| return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents); |
| } |
| |
| @Override |
| protected boolean verifyDrawable(@NonNull Drawable who) { |
| return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who); |
| } |
| |
| @Override |
| public void onItemsChanged(boolean animate) { |
| updatePreviewItems(animate); |
| invalidate(); |
| requestLayout(); |
| } |
| |
| private void updatePreviewItems(boolean animate) { |
| mPreviewItemManager.updatePreviewItems(animate); |
| mCurrentPreviewItems.clear(); |
| mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); |
| } |
| |
| /** |
| * Updates the preview items which match the provided condition |
| */ |
| public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) { |
| mPreviewItemManager.updatePreviewItems(itemCheck); |
| } |
| |
| @Override |
| public void onAdd(WorkspaceItemInfo item, int rank) { |
| updatePreviewItems(false); |
| boolean wasDotted = mDotInfo.hasDot(); |
| mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item)); |
| boolean isDotted = mDotInfo.hasDot(); |
| updateDotScale(wasDotted, isDotted); |
| setContentDescription(getAccessiblityTitle(mInfo.title)); |
| invalidate(); |
| requestLayout(); |
| } |
| |
| @Override |
| public void onRemove(List<WorkspaceItemInfo> items) { |
| updatePreviewItems(false); |
| boolean wasDotted = mDotInfo.hasDot(); |
| items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo); |
| boolean isDotted = mDotInfo.hasDot(); |
| updateDotScale(wasDotted, isDotted); |
| setContentDescription(getAccessiblityTitle(mInfo.title)); |
| invalidate(); |
| requestLayout(); |
| } |
| |
| public void onTitleChanged(CharSequence title) { |
| mFolderName.setText(title); |
| setContentDescription(getAccessiblityTitle(title)); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| if (event.getAction() == MotionEvent.ACTION_DOWN |
| && shouldIgnoreTouchDown(event.getX(), event.getY())) { |
| return false; |
| } |
| |
| // Call the superclass onTouchEvent first, because sometimes it changes the state to |
| // isPressed() on an ACTION_UP |
| super.onTouchEvent(event); |
| mLongPressHelper.onTouchEvent(event); |
| // Keep receiving the rest of the events |
| return true; |
| } |
| |
| /** |
| * Returns true if the touch down at the provided position be ignored |
| */ |
| protected boolean shouldIgnoreTouchDown(float x, float y) { |
| mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), |
| getHeight() - getPaddingBottom()); |
| return !mTouchArea.contains((int) x, (int) y); |
| } |
| |
| @Override |
| public void cancelLongPress() { |
| super.cancelLongPress(); |
| mLongPressHelper.cancelLongPress(); |
| } |
| |
| public void removeListeners() { |
| mInfo.removeListener(this); |
| mInfo.removeListener(mFolder); |
| } |
| |
| private boolean isInHotseat() { |
| return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; |
| } |
| |
| public void clearLeaveBehindIfExists() { |
| if (getParent() instanceof FolderIconParent) { |
| ((FolderIconParent) getParent()).clearFolderLeaveBehind(this); |
| } |
| } |
| |
| public void drawLeaveBehindIfExists() { |
| if (getParent() instanceof FolderIconParent) { |
| ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this); |
| } |
| } |
| |
| public void onFolderClose(int currentPage) { |
| mPreviewItemManager.onFolderClose(currentPage); |
| } |
| |
| @Override |
| public MultiTranslateDelegate getTranslateDelegate() { |
| return mTranslateDelegate; |
| } |
| |
| @Override |
| public void setReorderBounceScale(float scale) { |
| mScaleForReorderBounce = scale; |
| super.setScaleX(scale); |
| super.setScaleY(scale); |
| } |
| |
| @Override |
| public float getReorderBounceScale() { |
| return mScaleForReorderBounce; |
| } |
| |
| @Override |
| public int getViewType() { |
| return DRAGGABLE_ICON; |
| } |
| |
| @Override |
| public void getWorkspaceVisualDragBounds(Rect bounds) { |
| getPreviewBounds(bounds); |
| } |
| |
| /** |
| * Returns a formatted accessibility title for folder |
| */ |
| public String getAccessiblityTitle(CharSequence title) { |
| int size = mInfo.contents.size(); |
| if (size < MAX_NUM_ITEMS_IN_PREVIEW) { |
| return getContext().getString(R.string.folder_name_format_exact, title, size); |
| } else { |
| return getContext().getString(R.string.folder_name_format_overflow, title, |
| MAX_NUM_ITEMS_IN_PREVIEW); |
| } |
| } |
| |
| @Override |
| public void onHoverChanged(boolean hovered) { |
| super.onHoverChanged(hovered); |
| if (enableCursorHoverStates()) { |
| mBackground.setHovered(hovered); |
| } |
| } |
| |
| /** |
| * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. |
| */ |
| public interface FolderIconParent { |
| /** |
| * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a |
| * gap where the FolderIcon would be when the Folder is closed. |
| */ |
| void drawFolderLeaveBehindForIcon(FolderIcon child); |
| /** |
| * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed. |
| */ |
| void clearFolderLeaveBehind(FolderIcon child); |
| } |
| } |