/* * 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.AppPairInfo; 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(getContext()); private boolean mBackgroundIsVisible = true; FolderGridOrganizer mPreviewVerifier; ClippedFolderIconLayoutRule mPreviewLayoutRule; private PreviewItemManager mPreviewItemManager; private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); private List 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 DOT_SCALE_PROPERTY = new Property(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 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 (ItemInfo si : folderInfo.getContents()) { 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 (Folder.willAcceptItemType(itemType) && item != mInfo && !mFolder.isOpen()); } public boolean acceptDrop(ItemInfo dragInfo) { return !mFolder.isDestroyed() && willAcceptItem(dragInfo); } public void addItem(ItemInfo item) { mInfo.add(item, true); } public void removeItem(ItemInfo 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 PendingAddShortcutInfo) || Folder.willAccept(dragInfo))) { 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 ItemInfo destInfo, final View destView, final ItemInfo 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 ItemInfo 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 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.getAppContents(), 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) { ItemInfo 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 if (d.dragInfo instanceof AppPairInfo) { // dragged item is app pair item = new AppPairInfo((AppPairInfo) d.dragInfo); } else { // dragged item is WorkspaceItemInfo item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); } } else { item = d.dragInfo; } mFolder.notifyDrop(); onDrop(item, d, null, 1.0f, itemReturnedOnFailedDrop ? item.rank : mInfo.getContents().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 getPreviewItemsOnPage(int page) { return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents()); } @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 itemCheck) { mPreviewItemManager.updatePreviewItems(itemCheck); } @Override public void onAdd(ItemInfo 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 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(); } @Override 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.getContents().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); } }