1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.folder; 18 19 import static com.android.launcher3.Flags.enableCursorHoverStates; 20 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; 21 import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer; 22 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELED; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS; 26 import static com.android.launcher3.model.data.FolderInfo.willAcceptItemType; 27 28 import android.animation.Animator; 29 import android.animation.AnimatorListenerAdapter; 30 import android.animation.ObjectAnimator; 31 import android.content.Context; 32 import android.graphics.Canvas; 33 import android.graphics.Paint; 34 import android.graphics.Rect; 35 import android.graphics.drawable.Drawable; 36 import android.os.Looper; 37 import android.util.AttributeSet; 38 import android.util.Property; 39 import android.view.LayoutInflater; 40 import android.view.MotionEvent; 41 import android.view.View; 42 import android.view.ViewDebug; 43 import android.view.ViewGroup; 44 import android.widget.FrameLayout; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 49 import com.android.app.animation.Interpolators; 50 import com.android.launcher3.Alarm; 51 import com.android.launcher3.BubbleTextView; 52 import com.android.launcher3.CellLayout; 53 import com.android.launcher3.CheckLongPressHelper; 54 import com.android.launcher3.DeviceProfile; 55 import com.android.launcher3.DropTarget.DragObject; 56 import com.android.launcher3.Launcher; 57 import com.android.launcher3.LauncherSettings; 58 import com.android.launcher3.OnAlarmListener; 59 import com.android.launcher3.R; 60 import com.android.launcher3.Reorderable; 61 import com.android.launcher3.Utilities; 62 import com.android.launcher3.Workspace; 63 import com.android.launcher3.allapps.ActivityAllAppsContainerView; 64 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 65 import com.android.launcher3.dot.FolderDotInfo; 66 import com.android.launcher3.dragndrop.BaseItemDragListener; 67 import com.android.launcher3.dragndrop.DragLayer; 68 import com.android.launcher3.dragndrop.DragView; 69 import com.android.launcher3.dragndrop.DraggableView; 70 import com.android.launcher3.icons.DotRenderer; 71 import com.android.launcher3.logger.LauncherAtom.FromState; 72 import com.android.launcher3.logger.LauncherAtom.ToState; 73 import com.android.launcher3.logging.InstanceId; 74 import com.android.launcher3.logging.StatsLogManager; 75 import com.android.launcher3.model.data.AppPairInfo; 76 import com.android.launcher3.model.data.FolderInfo; 77 import com.android.launcher3.model.data.FolderInfo.LabelState; 78 import com.android.launcher3.model.data.ItemInfo; 79 import com.android.launcher3.model.data.WorkspaceItemFactory; 80 import com.android.launcher3.model.data.WorkspaceItemInfo; 81 import com.android.launcher3.util.Executors; 82 import com.android.launcher3.util.MultiTranslateDelegate; 83 import com.android.launcher3.util.Thunk; 84 import com.android.launcher3.views.ActivityContext; 85 import com.android.launcher3.views.FloatingIconViewCompanion; 86 import com.android.launcher3.widget.PendingAddShortcutInfo; 87 88 import java.util.ArrayList; 89 import java.util.List; 90 import java.util.function.Predicate; 91 92 /** 93 * An icon that can appear on in the workspace representing an {@link Folder}. 94 */ 95 public class FolderIcon extends FrameLayout implements FloatingIconViewCompanion, 96 DraggableView, Reorderable { 97 98 private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); 99 @Thunk ActivityContext mActivity; 100 @Thunk Folder mFolder; 101 public FolderInfo mInfo; 102 103 private CheckLongPressHelper mLongPressHelper; 104 105 static final int DROP_IN_ANIMATION_DURATION = 400; 106 107 // Flag whether the folder should open itself when an item is dragged over is enabled. 108 public static final boolean SPRING_LOADING_ENABLED = true; 109 110 // Delay when drag enters until the folder opens, in miliseconds. 111 private static final int ON_OPEN_DELAY = 800; 112 113 @Thunk BubbleTextView mFolderName; 114 115 PreviewBackground mBackground = new PreviewBackground(getContext()); 116 private boolean mBackgroundIsVisible = true; 117 118 FolderGridOrganizer mPreviewVerifier; 119 ClippedFolderIconLayoutRule mPreviewLayoutRule; 120 private PreviewItemManager mPreviewItemManager; 121 private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); 122 private List<ItemInfo> mCurrentPreviewItems = new ArrayList<>(); 123 124 boolean mAnimating = false; 125 126 private Alarm mOpenAlarm = new Alarm(Looper.getMainLooper()); 127 128 private boolean mForceHideDot; 129 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 130 private final FolderDotInfo mDotInfo = new FolderDotInfo(); 131 private DotRenderer mDotRenderer; 132 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 133 private DotRenderer.DrawParams mDotParams; 134 private float mDotScale; 135 private Animator mDotScaleAnim; 136 137 private Rect mTouchArea = new Rect(); 138 139 private float mScaleForReorderBounce = 1f; 140 141 private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY 142 = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") { 143 @Override 144 public Float get(FolderIcon folderIcon) { 145 return folderIcon.mDotScale; 146 } 147 148 @Override 149 public void set(FolderIcon folderIcon, Float value) { 150 folderIcon.mDotScale = value; 151 folderIcon.invalidate(); 152 } 153 }; 154 FolderIcon(Context context, AttributeSet attrs)155 public FolderIcon(Context context, AttributeSet attrs) { 156 super(context, attrs); 157 init(); 158 } 159 FolderIcon(Context context)160 public FolderIcon(Context context) { 161 super(context); 162 init(); 163 } 164 init()165 private void init() { 166 mLongPressHelper = new CheckLongPressHelper(this); 167 mPreviewLayoutRule = new ClippedFolderIconLayoutRule(); 168 mPreviewItemManager = new PreviewItemManager(this); 169 mDotParams = new DotRenderer.DrawParams(); 170 } 171 inflateFolderAndIcon(int resId, T activityContext, ViewGroup group, FolderInfo folderInfo)172 public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId, 173 T activityContext, ViewGroup group, FolderInfo folderInfo) { 174 Folder folder = Folder.fromXml(activityContext); 175 176 FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo); 177 folder.setFolderIcon(icon); 178 folder.bind(folderInfo); 179 180 icon.setFolder(folder); 181 return icon; 182 } 183 184 /** 185 * Builds a FolderIcon to be added to the activity. 186 * This method doesn't add any listeners to the FolderInfo, and hence any changes to the info 187 * will not be reflected in the folder. 188 */ inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, FolderInfo folderInfo)189 public static FolderIcon inflateIcon(int resId, ActivityContext activity, 190 @Nullable ViewGroup group, FolderInfo folderInfo) { 191 @SuppressWarnings("all") // suppress dead code warning 192 final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; 193 if (error) { 194 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + 195 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + 196 "is dependent on this"); 197 } 198 199 DeviceProfile grid = activity.getDeviceProfile(); 200 LayoutInflater inflater = (group != null) 201 ? LayoutInflater.from(group.getContext()) 202 : activity.getLayoutInflater(); 203 FolderIcon icon = (FolderIcon) inflater.inflate(resId, group, false); 204 205 icon.setClipToPadding(false); 206 icon.mFolderName = icon.findViewById(R.id.folder_icon_name); 207 icon.mFolderName.setText(folderInfo.title); 208 icon.mFolderName.setCompoundDrawablePadding(0); 209 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); 210 lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; 211 212 icon.setTag(folderInfo); 213 icon.setOnClickListener(activity.getItemOnClickListener()); 214 icon.mInfo = folderInfo; 215 icon.mActivity = activity; 216 icon.mDotRenderer = grid.mDotRendererWorkSpace; 217 218 icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title)); 219 icon.updateDotInfo(); 220 221 icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); 222 223 icon.mPreviewVerifier = createFolderGridOrganizer(activity.getDeviceProfile()); 224 icon.mPreviewVerifier.setFolderInfo(folderInfo); 225 icon.updatePreviewItems(false); 226 227 return icon; 228 } 229 animateBgShadowAndStroke()230 public void animateBgShadowAndStroke() { 231 mBackground.fadeInBackgroundShadow(); 232 mBackground.animateBackgroundStroke(); 233 } 234 getFolderName()235 public BubbleTextView getFolderName() { 236 return mFolderName; 237 } 238 getPreviewBounds(Rect outBounds)239 public void getPreviewBounds(Rect outBounds) { 240 mPreviewItemManager.recomputePreviewDrawingParams(); 241 mBackground.getBounds(outBounds); 242 // The preview items go outside of the bounds of the background. 243 Utilities.scaleRectAboutCenter(outBounds, 244 ClippedFolderIconLayoutRule.getIconOverlapFactor()); 245 } 246 getBackgroundStrokeWidth()247 public float getBackgroundStrokeWidth() { 248 return mBackground.getStrokeWidth(); 249 } 250 getFolder()251 public Folder getFolder() { 252 return mFolder; 253 } 254 setFolder(Folder folder)255 private void setFolder(Folder folder) { 256 mFolder = folder; 257 } 258 willAcceptItem(ItemInfo item)259 private boolean willAcceptItem(ItemInfo item) { 260 return (willAcceptItemType(item.itemType) && item != mInfo && !mFolder.isOpen()); 261 } 262 acceptDrop(ItemInfo dragInfo)263 public boolean acceptDrop(ItemInfo dragInfo) { 264 return !mFolder.isDestroyed() && willAcceptItem(dragInfo); 265 } 266 onDragEnter(ItemInfo dragInfo)267 public void onDragEnter(ItemInfo dragInfo) { 268 if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return; 269 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) getLayoutParams(); 270 CellLayout cl = (CellLayout) getParent().getParent(); 271 272 mBackground.animateToAccept(cl, lp.getCellX(), lp.getCellY()); 273 mOpenAlarm.setOnAlarmListener(mOnOpenListener); 274 if (SPRING_LOADING_ENABLED && 275 ((dragInfo instanceof WorkspaceItemFactory) 276 || (dragInfo instanceof PendingAddShortcutInfo) 277 || Folder.willAccept(dragInfo))) { 278 mOpenAlarm.setAlarm(ON_OPEN_DELAY); 279 } 280 } 281 282 OnAlarmListener mOnOpenListener = new OnAlarmListener() { 283 public void onAlarm(Alarm alarm) { 284 mFolder.beginExternalDrag(); 285 } 286 }; 287 prepareCreateAnimation(final View destView)288 public Drawable prepareCreateAnimation(final View destView) { 289 return mPreviewItemManager.prepareCreateAnimation(destView); 290 } 291 performCreateAnimation(final ItemInfo destInfo, final View destView, final ItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)292 public void performCreateAnimation(final ItemInfo destInfo, final View destView, 293 final ItemInfo srcInfo, final DragObject d, Rect dstRect, 294 float scaleRelativeToDragLayer) { 295 prepareCreateAnimation(destView); 296 getFolder().addFolderContent(destInfo); 297 // This will animate the first item from it's position as an icon into its 298 // position as the first item in the preview 299 mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null) 300 .start(); 301 302 // This will animate the dragView (srcView) into the new folder 303 onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1, 304 false /* itemReturnedOnFailedDrop */); 305 } 306 performDestroyAnimation(Runnable onCompleteRunnable)307 public void performDestroyAnimation(Runnable onCompleteRunnable) { 308 // This will animate the final item in the preview to be full size. 309 mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable) 310 .start(); 311 } 312 onDragExit()313 public void onDragExit() { 314 mBackground.animateToRest(); 315 mOpenAlarm.cancelAlarm(); 316 } 317 onDrop(final ItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)318 private void onDrop(final ItemInfo item, DragObject d, Rect finalRect, 319 float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) { 320 item.cellX = -1; 321 item.cellY = -1; 322 DragView animateView = d.dragView; 323 // Typically, the animateView corresponds to the DragView; however, if this is being done 324 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we 325 // will not have a view to animate 326 if (animateView != null && mActivity instanceof Launcher) { 327 final Launcher launcher = (Launcher) mActivity; 328 DragLayer dragLayer = launcher.getDragLayer(); 329 Rect to = finalRect; 330 if (to == null) { 331 to = new Rect(); 332 Workspace<?> workspace = launcher.getWorkspace(); 333 // Set cellLayout and this to it's final state to compute final animation locations 334 workspace.setFinalTransitionTransform(); 335 float scaleX = getScaleX(); 336 float scaleY = getScaleY(); 337 setScaleX(1.0f); 338 setScaleY(1.0f); 339 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); 340 // Finished computing final animation locations, restore current state 341 setScaleX(scaleX); 342 setScaleY(scaleY); 343 workspace.resetTransitionTransform(); 344 } 345 346 int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1); 347 boolean itemAdded = false; 348 if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) { 349 List<ItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); 350 getFolder().addFolderContent(item, index, false); 351 mCurrentPreviewItems.clear(); 352 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 353 354 if (!oldPreviewItems.equals(mCurrentPreviewItems)) { 355 int newIndex = mCurrentPreviewItems.indexOf(item); 356 if (newIndex >= 0) { 357 // If the item dropped is going to be in the preview, we update the 358 // index here to reflect its position in the preview. 359 index = newIndex; 360 } 361 362 mPreviewItemManager.hidePreviewItem(index, true); 363 mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item); 364 itemAdded = true; 365 } else { 366 getFolder().removeFolderContent(false, item); 367 } 368 } 369 370 if (!itemAdded) { 371 getFolder().addFolderContent(item, index, true); 372 } 373 374 int[] center = new int[2]; 375 float scale = getLocalCenterForIndex(index, numItemsInPreview, center); 376 center[0] = Math.round(scaleRelativeToDragLayer * center[0]); 377 center[1] = Math.round(scaleRelativeToDragLayer * center[1]); 378 379 to.offset(center[0] - animateView.getMeasuredWidth() / 2, 380 center[1] - animateView.getMeasuredHeight() / 2); 381 382 float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f; 383 384 float finalScale = scale * scaleRelativeToDragLayer; 385 386 // Account for potentially different icon sizes with non-default grid settings 387 if (d.dragSource instanceof ActivityAllAppsContainerView) { 388 DeviceProfile grid = mActivity.getDeviceProfile(); 389 float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx); 390 finalScale *= containerScale; 391 } 392 393 final int finalIndex = index; 394 dragLayer.animateView(animateView, to, finalAlpha, 395 finalScale, finalScale, DROP_IN_ANIMATION_DURATION, 396 Interpolators.DECELERATE_2, 397 () -> { 398 mPreviewItemManager.hidePreviewItem(finalIndex, false); 399 mFolder.showItem(item); 400 }, 401 DragLayer.ANIMATION_END_DISAPPEAR, null); 402 403 mFolder.hideItem(item); 404 405 if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true); 406 407 FolderNameInfos nameInfos = new FolderNameInfos(); 408 Executors.MODEL_EXECUTOR.post(() -> { 409 d.folderNameProvider.getSuggestedFolderName( 410 getContext(), mInfo.getAppContents(), nameInfos); 411 postDelayed(() -> { 412 setLabelSuggestion(nameInfos, d.logInstanceId); 413 invalidate(); 414 }, DROP_IN_ANIMATION_DURATION); 415 }); 416 } else { 417 getFolder().addFolderContent(item); 418 } 419 } 420 421 /** 422 * Set the suggested folder name. 423 */ setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId)424 public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) { 425 if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) { 426 return; 427 } 428 if (nameInfos == null || !nameInfos.hasSuggestions()) { 429 StatsLogManager.newInstance(getContext()).logger() 430 .withInstanceId(instanceId) 431 .withItemInfo(mInfo) 432 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS); 433 return; 434 } 435 if (!nameInfos.hasPrimary()) { 436 StatsLogManager.newInstance(getContext()).logger() 437 .withInstanceId(instanceId) 438 .withItemInfo(mInfo) 439 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY); 440 return; 441 } 442 CharSequence newTitle = nameInfos.getLabels()[0]; 443 FromState fromState = mInfo.getFromLabelState(); 444 445 mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter()); 446 onTitleChanged(mInfo.title); 447 mFolder.getFolderName().setText(mInfo.title); 448 449 // Logging for folder creation flow 450 StatsLogManager.newInstance(getContext()).logger() 451 .withInstanceId(instanceId) 452 .withItemInfo(mInfo) 453 .withFromState(fromState) 454 .withToState(ToState.TO_SUGGESTION0) 455 // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter, 456 // event is assumed to be folder creation on the server side. 457 .withEditText(newTitle.toString()) 458 .log(LAUNCHER_FOLDER_AUTO_LABELED); 459 } 460 461 onDrop(DragObject d, boolean itemReturnedOnFailedDrop)462 public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { 463 ItemInfo item; 464 if (d.dragInfo instanceof WorkspaceItemFactory) { 465 // Came from all apps -- make a copy 466 item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext()); 467 } else if (d.dragSource instanceof BaseItemDragListener){ 468 // Came from a different window -- make a copy 469 if (d.dragInfo instanceof AppPairInfo) { 470 // dragged item is app pair 471 item = new AppPairInfo((AppPairInfo) d.dragInfo); 472 } else { 473 // dragged item is WorkspaceItemInfo 474 item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); 475 } 476 } else { 477 item = d.dragInfo; 478 } 479 mFolder.notifyDrop(); 480 onDrop(item, d, null, 1.0f, 481 itemReturnedOnFailedDrop ? item.rank : mInfo.getContents().size(), 482 itemReturnedOnFailedDrop 483 ); 484 } 485 486 /** Keep the notification dot up to date with the sum of all the content's dots. */ updateDotInfo()487 public void updateDotInfo() { 488 boolean hadDot = mDotInfo.hasDot(); 489 mDotInfo.reset(); 490 for (ItemInfo si : mInfo.getContents()) { 491 mDotInfo.addDotInfo(mActivity.getDotInfoForItem(si)); 492 } 493 boolean isDotted = mDotInfo.hasDot(); 494 float newDotScale = isDotted ? 1f : 0f; 495 // Animate when a dot is first added or when it is removed. 496 if ((hadDot ^ isDotted) && isShown()) { 497 animateDotScale(newDotScale); 498 } else { 499 cancelDotScaleAnim(); 500 mDotScale = newDotScale; 501 invalidate(); 502 } 503 } 504 getLayoutRule()505 public ClippedFolderIconLayoutRule getLayoutRule() { 506 return mPreviewLayoutRule; 507 } 508 509 @Override setForceHideDot(boolean forceHideDot)510 public void setForceHideDot(boolean forceHideDot) { 511 if (mForceHideDot == forceHideDot) { 512 return; 513 } 514 mForceHideDot = forceHideDot; 515 516 if (forceHideDot) { 517 invalidate(); 518 } else if (hasDot()) { 519 animateDotScale(0, 1); 520 } 521 } 522 cancelDotScaleAnim()523 private void cancelDotScaleAnim() { 524 if (mDotScaleAnim != null) { 525 mDotScaleAnim.cancel(); 526 } 527 } 528 animateDotScale(float... dotScales)529 public void animateDotScale(float... dotScales) { 530 cancelDotScaleAnim(); 531 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 532 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 533 @Override 534 public void onAnimationEnd(Animator animation) { 535 mDotScaleAnim = null; 536 } 537 }); 538 mDotScaleAnim.start(); 539 } 540 hasDot()541 public boolean hasDot() { 542 return mDotInfo != null && mDotInfo.hasDot(); 543 } 544 getLocalCenterForIndex(int index, int curNumItems, int[] center)545 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { 546 mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams( 547 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams); 548 549 mTmpParams.transX += mBackground.basePreviewOffsetX; 550 mTmpParams.transY += mBackground.basePreviewOffsetY; 551 552 float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize(); 553 float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2; 554 float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2; 555 556 center[0] = Math.round(offsetX); 557 center[1] = Math.round(offsetY); 558 return mTmpParams.scale; 559 } 560 setFolderBackground(PreviewBackground bg)561 public void setFolderBackground(PreviewBackground bg) { 562 mBackground = bg; 563 mBackground.setInvalidateDelegate(this); 564 } 565 566 @Override setIconVisible(boolean visible)567 public void setIconVisible(boolean visible) { 568 mBackgroundIsVisible = visible; 569 invalidate(); 570 } 571 getIconVisible()572 public boolean getIconVisible() { 573 return mBackgroundIsVisible; 574 } 575 getFolderBackground()576 public PreviewBackground getFolderBackground() { 577 return mBackground; 578 } 579 getPreviewItemManager()580 public PreviewItemManager getPreviewItemManager() { 581 return mPreviewItemManager; 582 } 583 584 @Override dispatchDraw(Canvas canvas)585 protected void dispatchDraw(Canvas canvas) { 586 super.dispatchDraw(canvas); 587 588 if (!mBackgroundIsVisible) return; 589 590 mPreviewItemManager.recomputePreviewDrawingParams(); 591 592 if (!mBackground.drawingDelegated()) { 593 mBackground.drawBackground(canvas); 594 } 595 596 if (mCurrentPreviewItems.isEmpty() && !mAnimating) return; 597 598 mPreviewItemManager.draw(canvas); 599 600 if (!mBackground.drawingDelegated()) { 601 mBackground.drawBackgroundStroke(canvas); 602 } 603 604 drawDot(canvas); 605 } 606 drawDot(Canvas canvas)607 public void drawDot(Canvas canvas) { 608 if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) { 609 Rect iconBounds = mDotParams.iconBounds; 610 // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered 611 int iconSize = mActivity.getDeviceProfile().iconSizePx; 612 iconBounds.left = (getWidth() - iconSize) / 2; 613 iconBounds.right = iconBounds.left + iconSize; 614 iconBounds.top = getPaddingTop(); 615 iconBounds.bottom = iconBounds.top + iconSize; 616 617 float iconScale = (float) mBackground.previewSize / iconSize; 618 Utilities.scaleRectAboutCenter(iconBounds, iconScale); 619 620 // If we are animating to the accepting state, animate the dot out. 621 mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); 622 mDotParams.dotColor = mBackground.getDotColor(); 623 mDotRenderer.draw(canvas, mDotParams); 624 } 625 } 626 627 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)628 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 629 boolean shouldCenterIcon = mActivity.getDeviceProfile().iconCenterVertically; 630 if (shouldCenterIcon) { 631 int iconSize = mActivity.getDeviceProfile().iconSizePx; 632 Paint.FontMetrics fm = mFolderName.getPaint().getFontMetrics(); 633 int cellHeightPx = iconSize + mFolderName.getCompoundDrawablePadding() 634 + (int) Math.ceil(fm.bottom - fm.top); 635 setPadding(getPaddingLeft(), (MeasureSpec.getSize(heightMeasureSpec) 636 - cellHeightPx) / 2, getPaddingRight(), getPaddingBottom()); 637 } 638 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 639 } 640 641 /** Sets the visibility of the icon's title text */ setTextVisible(boolean visible)642 public void setTextVisible(boolean visible) { 643 if (visible) { 644 mFolderName.setVisibility(VISIBLE); 645 } else { 646 mFolderName.setVisibility(INVISIBLE); 647 } 648 } 649 getTextVisible()650 public boolean getTextVisible() { 651 return mFolderName.getVisibility() == VISIBLE; 652 } 653 654 /** 655 * Returns the list of items which should be visible in the preview 656 */ getPreviewItemsOnPage(int page)657 public List<ItemInfo> getPreviewItemsOnPage(int page) { 658 return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents()); 659 } 660 661 @Override verifyDrawable(@onNull Drawable who)662 protected boolean verifyDrawable(@NonNull Drawable who) { 663 return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who); 664 } 665 updatePreviewItems(boolean animate)666 private void updatePreviewItems(boolean animate) { 667 mPreviewItemManager.updatePreviewItems(animate); 668 mCurrentPreviewItems.clear(); 669 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 670 } 671 672 /** 673 * Updates the preview items which match the provided condition 674 */ updatePreviewItems(Predicate<ItemInfo> itemCheck)675 public void updatePreviewItems(Predicate<ItemInfo> itemCheck) { 676 mPreviewItemManager.updatePreviewItems(itemCheck); 677 } 678 onItemsChanged(boolean animate)679 public void onItemsChanged(boolean animate) { 680 updatePreviewItems(false); 681 updateDotInfo(); 682 setContentDescription(getAccessiblityTitle(mInfo.title)); 683 updatePreviewItems(animate); 684 invalidate(); 685 requestLayout(); 686 } 687 onTitleChanged(CharSequence title)688 public void onTitleChanged(CharSequence title) { 689 mFolderName.setText(title); 690 setContentDescription(getAccessiblityTitle(title)); 691 } 692 693 @Override onTouchEvent(MotionEvent event)694 public boolean onTouchEvent(MotionEvent event) { 695 if (event.getAction() == MotionEvent.ACTION_DOWN 696 && shouldIgnoreTouchDown(event.getX(), event.getY())) { 697 return false; 698 } 699 700 // Call the superclass onTouchEvent first, because sometimes it changes the state to 701 // isPressed() on an ACTION_UP 702 super.onTouchEvent(event); 703 mLongPressHelper.onTouchEvent(event); 704 // Keep receiving the rest of the events 705 return true; 706 } 707 708 /** 709 * Returns true if the touch down at the provided position be ignored 710 */ shouldIgnoreTouchDown(float x, float y)711 protected boolean shouldIgnoreTouchDown(float x, float y) { 712 mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), 713 getHeight() - getPaddingBottom()); 714 return !mTouchArea.contains((int) x, (int) y); 715 } 716 717 @Override cancelLongPress()718 public void cancelLongPress() { 719 super.cancelLongPress(); 720 mLongPressHelper.cancelLongPress(); 721 } 722 isInHotseat()723 private boolean isInHotseat() { 724 return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; 725 } 726 clearLeaveBehindIfExists()727 public void clearLeaveBehindIfExists() { 728 if (getParent() instanceof FolderIconParent) { 729 ((FolderIconParent) getParent()).clearFolderLeaveBehind(this); 730 } 731 } 732 drawLeaveBehindIfExists()733 public void drawLeaveBehindIfExists() { 734 if (getParent() instanceof FolderIconParent) { 735 ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this); 736 } 737 } 738 onFolderClose(int currentPage)739 public void onFolderClose(int currentPage) { 740 mPreviewItemManager.onFolderClose(currentPage); 741 } 742 743 @Override getTranslateDelegate()744 public MultiTranslateDelegate getTranslateDelegate() { 745 return mTranslateDelegate; 746 } 747 748 @Override setReorderBounceScale(float scale)749 public void setReorderBounceScale(float scale) { 750 mScaleForReorderBounce = scale; 751 super.setScaleX(scale); 752 super.setScaleY(scale); 753 } 754 755 @Override getReorderBounceScale()756 public float getReorderBounceScale() { 757 return mScaleForReorderBounce; 758 } 759 760 @Override getViewType()761 public int getViewType() { 762 return DRAGGABLE_ICON; 763 } 764 765 @Override getWorkspaceVisualDragBounds(Rect bounds)766 public void getWorkspaceVisualDragBounds(Rect bounds) { 767 getPreviewBounds(bounds); 768 } 769 770 /** 771 * Returns a formatted accessibility title for folder 772 */ getAccessiblityTitle(CharSequence title)773 public String getAccessiblityTitle(CharSequence title) { 774 if (title == null) { 775 // Avoids "Talkback -> Folder: null" announcement. 776 title = getContext().getString(R.string.unnamed_folder); 777 } 778 int size = mInfo.getContents().size(); 779 if (size < MAX_NUM_ITEMS_IN_PREVIEW) { 780 return getContext().getString(R.string.folder_name_format_exact, title, size); 781 } else { 782 return getContext().getString(R.string.folder_name_format_overflow, title, 783 MAX_NUM_ITEMS_IN_PREVIEW); 784 } 785 } 786 787 @Override onHoverChanged(boolean hovered)788 public void onHoverChanged(boolean hovered) { 789 super.onHoverChanged(hovered); 790 if (enableCursorHoverStates()) { 791 mBackground.setHovered(hovered); 792 } 793 } 794 795 /** 796 * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. 797 */ 798 public interface FolderIconParent { 799 /** 800 * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a 801 * gap where the FolderIcon would be when the Folder is closed. 802 */ drawFolderLeaveBehindForIcon(FolderIcon child)803 void drawFolderLeaveBehindForIcon(FolderIcon child); 804 /** 805 * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed. 806 */ clearFolderLeaveBehind(FolderIcon child)807 void clearFolderLeaveBehind(FolderIcon child); 808 } 809 } 810