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.ICON_OVERLAP_FACTOR; 21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; 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 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.ObjectAnimator; 30 import android.content.Context; 31 import android.graphics.Canvas; 32 import android.graphics.Paint; 33 import android.graphics.Rect; 34 import android.graphics.drawable.Drawable; 35 import android.os.Looper; 36 import android.util.AttributeSet; 37 import android.util.Property; 38 import android.view.LayoutInflater; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.ViewDebug; 42 import android.view.ViewGroup; 43 import android.widget.FrameLayout; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 48 import com.android.app.animation.Interpolators; 49 import com.android.launcher3.Alarm; 50 import com.android.launcher3.BubbleTextView; 51 import com.android.launcher3.CellLayout; 52 import com.android.launcher3.CheckLongPressHelper; 53 import com.android.launcher3.DeviceProfile; 54 import com.android.launcher3.DropTarget.DragObject; 55 import com.android.launcher3.Launcher; 56 import com.android.launcher3.LauncherSettings; 57 import com.android.launcher3.OnAlarmListener; 58 import com.android.launcher3.R; 59 import com.android.launcher3.Reorderable; 60 import com.android.launcher3.Utilities; 61 import com.android.launcher3.Workspace; 62 import com.android.launcher3.allapps.ActivityAllAppsContainerView; 63 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 64 import com.android.launcher3.dot.FolderDotInfo; 65 import com.android.launcher3.dragndrop.BaseItemDragListener; 66 import com.android.launcher3.dragndrop.DragLayer; 67 import com.android.launcher3.dragndrop.DragView; 68 import com.android.launcher3.dragndrop.DraggableView; 69 import com.android.launcher3.icons.DotRenderer; 70 import com.android.launcher3.logger.LauncherAtom.FromState; 71 import com.android.launcher3.logger.LauncherAtom.ToState; 72 import com.android.launcher3.logging.InstanceId; 73 import com.android.launcher3.logging.StatsLogManager; 74 import com.android.launcher3.model.data.AppPairInfo; 75 import com.android.launcher3.model.data.FolderInfo; 76 import com.android.launcher3.model.data.FolderInfo.FolderListener; 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.IconLabelDotView; 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 FolderListener, IconLabelDotView, 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 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 icon.setFolder(folder); 180 return icon; 181 } 182 183 /** 184 * Builds a FolderIcon to be added to the Launcher 185 */ inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, FolderInfo folderInfo)186 public static FolderIcon inflateIcon(int resId, ActivityContext activity, 187 @Nullable ViewGroup group, FolderInfo folderInfo) { 188 @SuppressWarnings("all") // suppress dead code warning 189 final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; 190 if (error) { 191 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + 192 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + 193 "is dependent on this"); 194 } 195 196 DeviceProfile grid = activity.getDeviceProfile(); 197 LayoutInflater inflater = (group != null) 198 ? LayoutInflater.from(group.getContext()) 199 : activity.getLayoutInflater(); 200 FolderIcon icon = (FolderIcon) inflater.inflate(resId, group, false); 201 202 icon.setClipToPadding(false); 203 icon.mFolderName = icon.findViewById(R.id.folder_icon_name); 204 icon.mFolderName.setText(folderInfo.title); 205 icon.mFolderName.setCompoundDrawablePadding(0); 206 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); 207 lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; 208 209 icon.setTag(folderInfo); 210 icon.setOnClickListener(activity.getItemOnClickListener()); 211 icon.mInfo = folderInfo; 212 icon.mActivity = activity; 213 icon.mDotRenderer = grid.mDotRendererWorkSpace; 214 215 icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title)); 216 217 // Keep the notification dot up to date with the sum of all the content's dots. 218 FolderDotInfo folderDotInfo = new FolderDotInfo(); 219 for (ItemInfo si : folderInfo.getContents()) { 220 folderDotInfo.addDotInfo(activity.getDotInfoForItem(si)); 221 } 222 icon.setDotInfo(folderDotInfo); 223 224 icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); 225 226 icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile()); 227 icon.mPreviewVerifier.setFolderInfo(folderInfo); 228 icon.updatePreviewItems(false); 229 230 folderInfo.addListener(icon); 231 232 return icon; 233 } 234 animateBgShadowAndStroke()235 public void animateBgShadowAndStroke() { 236 mBackground.fadeInBackgroundShadow(); 237 mBackground.animateBackgroundStroke(); 238 } 239 getFolderName()240 public BubbleTextView getFolderName() { 241 return mFolderName; 242 } 243 getPreviewBounds(Rect outBounds)244 public void getPreviewBounds(Rect outBounds) { 245 mPreviewItemManager.recomputePreviewDrawingParams(); 246 mBackground.getBounds(outBounds); 247 // The preview items go outside of the bounds of the background. 248 Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR); 249 } 250 getBackgroundStrokeWidth()251 public float getBackgroundStrokeWidth() { 252 return mBackground.getStrokeWidth(); 253 } 254 getFolder()255 public Folder getFolder() { 256 return mFolder; 257 } 258 setFolder(Folder folder)259 private void setFolder(Folder folder) { 260 mFolder = folder; 261 } 262 willAcceptItem(ItemInfo item)263 private boolean willAcceptItem(ItemInfo item) { 264 final int itemType = item.itemType; 265 return (Folder.willAcceptItemType(itemType) && item != mInfo && !mFolder.isOpen()); 266 } 267 acceptDrop(ItemInfo dragInfo)268 public boolean acceptDrop(ItemInfo dragInfo) { 269 return !mFolder.isDestroyed() && willAcceptItem(dragInfo); 270 } 271 addItem(ItemInfo item)272 public void addItem(ItemInfo item) { 273 mInfo.add(item, true); 274 } 275 removeItem(ItemInfo item, boolean animate)276 public void removeItem(ItemInfo item, boolean animate) { 277 mInfo.remove(item, animate); 278 } 279 onDragEnter(ItemInfo dragInfo)280 public void onDragEnter(ItemInfo dragInfo) { 281 if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return; 282 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) getLayoutParams(); 283 CellLayout cl = (CellLayout) getParent().getParent(); 284 285 mBackground.animateToAccept(cl, lp.getCellX(), lp.getCellY()); 286 mOpenAlarm.setOnAlarmListener(mOnOpenListener); 287 if (SPRING_LOADING_ENABLED && 288 ((dragInfo instanceof WorkspaceItemFactory) 289 || (dragInfo instanceof PendingAddShortcutInfo) 290 || Folder.willAccept(dragInfo))) { 291 mOpenAlarm.setAlarm(ON_OPEN_DELAY); 292 } 293 } 294 295 OnAlarmListener mOnOpenListener = new OnAlarmListener() { 296 public void onAlarm(Alarm alarm) { 297 mFolder.beginExternalDrag(); 298 } 299 }; 300 prepareCreateAnimation(final View destView)301 public Drawable prepareCreateAnimation(final View destView) { 302 return mPreviewItemManager.prepareCreateAnimation(destView); 303 } 304 performCreateAnimation(final ItemInfo destInfo, final View destView, final ItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)305 public void performCreateAnimation(final ItemInfo destInfo, final View destView, 306 final ItemInfo srcInfo, final DragObject d, Rect dstRect, 307 float scaleRelativeToDragLayer) { 308 final DragView srcView = d.dragView; 309 prepareCreateAnimation(destView); 310 addItem(destInfo); 311 // This will animate the first item from it's position as an icon into its 312 // position as the first item in the preview 313 mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null) 314 .start(); 315 316 // This will animate the dragView (srcView) into the new folder 317 onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1, 318 false /* itemReturnedOnFailedDrop */); 319 } 320 performDestroyAnimation(Runnable onCompleteRunnable)321 public void performDestroyAnimation(Runnable onCompleteRunnable) { 322 // This will animate the final item in the preview to be full size. 323 mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable) 324 .start(); 325 } 326 onDragExit()327 public void onDragExit() { 328 mBackground.animateToRest(); 329 mOpenAlarm.cancelAlarm(); 330 } 331 onDrop(final ItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)332 private void onDrop(final ItemInfo item, DragObject d, Rect finalRect, 333 float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) { 334 item.cellX = -1; 335 item.cellY = -1; 336 DragView animateView = d.dragView; 337 // Typically, the animateView corresponds to the DragView; however, if this is being done 338 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we 339 // will not have a view to animate 340 if (animateView != null && mActivity instanceof Launcher) { 341 final Launcher launcher = (Launcher) mActivity; 342 DragLayer dragLayer = launcher.getDragLayer(); 343 Rect to = finalRect; 344 if (to == null) { 345 to = new Rect(); 346 Workspace<?> workspace = launcher.getWorkspace(); 347 // Set cellLayout and this to it's final state to compute final animation locations 348 workspace.setFinalTransitionTransform(); 349 float scaleX = getScaleX(); 350 float scaleY = getScaleY(); 351 setScaleX(1.0f); 352 setScaleY(1.0f); 353 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); 354 // Finished computing final animation locations, restore current state 355 setScaleX(scaleX); 356 setScaleY(scaleY); 357 workspace.resetTransitionTransform(); 358 } 359 360 int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1); 361 boolean itemAdded = false; 362 if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) { 363 List<ItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); 364 mInfo.add(item, index, false); 365 mCurrentPreviewItems.clear(); 366 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 367 368 if (!oldPreviewItems.equals(mCurrentPreviewItems)) { 369 int newIndex = mCurrentPreviewItems.indexOf(item); 370 if (newIndex >= 0) { 371 // If the item dropped is going to be in the preview, we update the 372 // index here to reflect its position in the preview. 373 index = newIndex; 374 } 375 376 mPreviewItemManager.hidePreviewItem(index, true); 377 mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item); 378 itemAdded = true; 379 } else { 380 removeItem(item, false); 381 } 382 } 383 384 if (!itemAdded) { 385 mInfo.add(item, index, true); 386 } 387 388 int[] center = new int[2]; 389 float scale = getLocalCenterForIndex(index, numItemsInPreview, center); 390 center[0] = Math.round(scaleRelativeToDragLayer * center[0]); 391 center[1] = Math.round(scaleRelativeToDragLayer * center[1]); 392 393 to.offset(center[0] - animateView.getMeasuredWidth() / 2, 394 center[1] - animateView.getMeasuredHeight() / 2); 395 396 float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f; 397 398 float finalScale = scale * scaleRelativeToDragLayer; 399 400 // Account for potentially different icon sizes with non-default grid settings 401 if (d.dragSource instanceof ActivityAllAppsContainerView) { 402 DeviceProfile grid = mActivity.getDeviceProfile(); 403 float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx); 404 finalScale *= containerScale; 405 } 406 407 final int finalIndex = index; 408 dragLayer.animateView(animateView, to, finalAlpha, 409 finalScale, finalScale, DROP_IN_ANIMATION_DURATION, 410 Interpolators.DECELERATE_2, 411 () -> { 412 mPreviewItemManager.hidePreviewItem(finalIndex, false); 413 mFolder.showItem(item); 414 }, 415 DragLayer.ANIMATION_END_DISAPPEAR, null); 416 417 mFolder.hideItem(item); 418 419 if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true); 420 421 FolderNameInfos nameInfos = new FolderNameInfos(); 422 Executors.MODEL_EXECUTOR.post(() -> { 423 d.folderNameProvider.getSuggestedFolderName( 424 getContext(), mInfo.getAppContents(), nameInfos); 425 postDelayed(() -> { 426 setLabelSuggestion(nameInfos, d.logInstanceId); 427 invalidate(); 428 }, DROP_IN_ANIMATION_DURATION); 429 }); 430 } else { 431 addItem(item); 432 } 433 } 434 435 /** 436 * Set the suggested folder name. 437 */ setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId)438 public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) { 439 if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) { 440 return; 441 } 442 if (nameInfos == null || !nameInfos.hasSuggestions()) { 443 StatsLogManager.newInstance(getContext()).logger() 444 .withInstanceId(instanceId) 445 .withItemInfo(mInfo) 446 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS); 447 return; 448 } 449 if (!nameInfos.hasPrimary()) { 450 StatsLogManager.newInstance(getContext()).logger() 451 .withInstanceId(instanceId) 452 .withItemInfo(mInfo) 453 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY); 454 return; 455 } 456 CharSequence newTitle = nameInfos.getLabels()[0]; 457 FromState fromState = mInfo.getFromLabelState(); 458 459 mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter()); 460 onTitleChanged(mInfo.title); 461 mFolder.mFolderName.setText(mInfo.title); 462 463 // Logging for folder creation flow 464 StatsLogManager.newInstance(getContext()).logger() 465 .withInstanceId(instanceId) 466 .withItemInfo(mInfo) 467 .withFromState(fromState) 468 .withToState(ToState.TO_SUGGESTION0) 469 // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter, 470 // event is assumed to be folder creation on the server side. 471 .withEditText(newTitle.toString()) 472 .log(LAUNCHER_FOLDER_AUTO_LABELED); 473 } 474 475 onDrop(DragObject d, boolean itemReturnedOnFailedDrop)476 public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { 477 ItemInfo item; 478 if (d.dragInfo instanceof WorkspaceItemFactory) { 479 // Came from all apps -- make a copy 480 item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext()); 481 } else if (d.dragSource instanceof BaseItemDragListener){ 482 // Came from a different window -- make a copy 483 if (d.dragInfo instanceof AppPairInfo) { 484 // dragged item is app pair 485 item = new AppPairInfo((AppPairInfo) d.dragInfo); 486 } else { 487 // dragged item is WorkspaceItemInfo 488 item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); 489 } 490 } else { 491 item = d.dragInfo; 492 } 493 mFolder.notifyDrop(); 494 onDrop(item, d, null, 1.0f, 495 itemReturnedOnFailedDrop ? item.rank : mInfo.getContents().size(), 496 itemReturnedOnFailedDrop 497 ); 498 } 499 setDotInfo(FolderDotInfo dotInfo)500 public void setDotInfo(FolderDotInfo dotInfo) { 501 updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot()); 502 mDotInfo = dotInfo; 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 523 /** 524 * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false 525 * (the dot is being added or removed). 526 */ updateDotScale(boolean wasDotted, boolean isDotted)527 private void updateDotScale(boolean wasDotted, boolean isDotted) { 528 float newDotScale = isDotted ? 1f : 0f; 529 // Animate when a dot is first added or when it is removed. 530 if ((wasDotted ^ isDotted) && isShown()) { 531 animateDotScale(newDotScale); 532 } else { 533 cancelDotScaleAnim(); 534 mDotScale = newDotScale; 535 invalidate(); 536 } 537 } 538 cancelDotScaleAnim()539 private void cancelDotScaleAnim() { 540 if (mDotScaleAnim != null) { 541 mDotScaleAnim.cancel(); 542 } 543 } 544 animateDotScale(float... dotScales)545 public void animateDotScale(float... dotScales) { 546 cancelDotScaleAnim(); 547 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 548 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 549 @Override 550 public void onAnimationEnd(Animator animation) { 551 mDotScaleAnim = null; 552 } 553 }); 554 mDotScaleAnim.start(); 555 } 556 hasDot()557 public boolean hasDot() { 558 return mDotInfo != null && mDotInfo.hasDot(); 559 } 560 getLocalCenterForIndex(int index, int curNumItems, int[] center)561 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { 562 mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams( 563 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams); 564 565 mTmpParams.transX += mBackground.basePreviewOffsetX; 566 mTmpParams.transY += mBackground.basePreviewOffsetY; 567 568 float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize(); 569 float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2; 570 float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2; 571 572 center[0] = Math.round(offsetX); 573 center[1] = Math.round(offsetY); 574 return mTmpParams.scale; 575 } 576 setFolderBackground(PreviewBackground bg)577 public void setFolderBackground(PreviewBackground bg) { 578 mBackground = bg; 579 mBackground.setInvalidateDelegate(this); 580 } 581 582 @Override setIconVisible(boolean visible)583 public void setIconVisible(boolean visible) { 584 mBackgroundIsVisible = visible; 585 invalidate(); 586 } 587 getIconVisible()588 public boolean getIconVisible() { 589 return mBackgroundIsVisible; 590 } 591 getFolderBackground()592 public PreviewBackground getFolderBackground() { 593 return mBackground; 594 } 595 getPreviewItemManager()596 public PreviewItemManager getPreviewItemManager() { 597 return mPreviewItemManager; 598 } 599 600 @Override dispatchDraw(Canvas canvas)601 protected void dispatchDraw(Canvas canvas) { 602 super.dispatchDraw(canvas); 603 604 if (!mBackgroundIsVisible) return; 605 606 mPreviewItemManager.recomputePreviewDrawingParams(); 607 608 if (!mBackground.drawingDelegated()) { 609 mBackground.drawBackground(canvas); 610 } 611 612 if (mCurrentPreviewItems.isEmpty() && !mAnimating) return; 613 614 mPreviewItemManager.draw(canvas); 615 616 if (!mBackground.drawingDelegated()) { 617 mBackground.drawBackgroundStroke(canvas); 618 } 619 620 drawDot(canvas); 621 } 622 drawDot(Canvas canvas)623 public void drawDot(Canvas canvas) { 624 if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) { 625 Rect iconBounds = mDotParams.iconBounds; 626 // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered 627 int iconSize = mActivity.getDeviceProfile().iconSizePx; 628 iconBounds.left = (getWidth() - iconSize) / 2; 629 iconBounds.right = iconBounds.left + iconSize; 630 iconBounds.top = getPaddingTop(); 631 iconBounds.bottom = iconBounds.top + iconSize; 632 633 float iconScale = (float) mBackground.previewSize / iconSize; 634 Utilities.scaleRectAboutCenter(iconBounds, iconScale); 635 636 // If we are animating to the accepting state, animate the dot out. 637 mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); 638 mDotParams.dotColor = mBackground.getDotColor(); 639 mDotRenderer.draw(canvas, mDotParams); 640 } 641 } 642 643 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)644 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 645 boolean shouldCenterIcon = mActivity.getDeviceProfile().iconCenterVertically; 646 if (shouldCenterIcon) { 647 int iconSize = mActivity.getDeviceProfile().iconSizePx; 648 Paint.FontMetrics fm = mFolderName.getPaint().getFontMetrics(); 649 int cellHeightPx = iconSize + mFolderName.getCompoundDrawablePadding() 650 + (int) Math.ceil(fm.bottom - fm.top); 651 setPadding(getPaddingLeft(), (MeasureSpec.getSize(heightMeasureSpec) 652 - cellHeightPx) / 2, getPaddingRight(), getPaddingBottom()); 653 } 654 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 655 } 656 657 /** Sets the visibility of the icon's title text */ setTextVisible(boolean visible)658 public void setTextVisible(boolean visible) { 659 if (visible) { 660 mFolderName.setVisibility(VISIBLE); 661 } else { 662 mFolderName.setVisibility(INVISIBLE); 663 } 664 } 665 getTextVisible()666 public boolean getTextVisible() { 667 return mFolderName.getVisibility() == VISIBLE; 668 } 669 670 /** 671 * Returns the list of items which should be visible in the preview 672 */ getPreviewItemsOnPage(int page)673 public List<ItemInfo> getPreviewItemsOnPage(int page) { 674 return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents()); 675 } 676 677 @Override verifyDrawable(@onNull Drawable who)678 protected boolean verifyDrawable(@NonNull Drawable who) { 679 return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who); 680 } 681 682 @Override onItemsChanged(boolean animate)683 public void onItemsChanged(boolean animate) { 684 updatePreviewItems(animate); 685 invalidate(); 686 requestLayout(); 687 } 688 updatePreviewItems(boolean animate)689 private void updatePreviewItems(boolean animate) { 690 mPreviewItemManager.updatePreviewItems(animate); 691 mCurrentPreviewItems.clear(); 692 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 693 } 694 695 /** 696 * Updates the preview items which match the provided condition 697 */ updatePreviewItems(Predicate<ItemInfo> itemCheck)698 public void updatePreviewItems(Predicate<ItemInfo> itemCheck) { 699 mPreviewItemManager.updatePreviewItems(itemCheck); 700 } 701 702 @Override onAdd(ItemInfo item, int rank)703 public void onAdd(ItemInfo item, int rank) { 704 updatePreviewItems(false); 705 boolean wasDotted = mDotInfo.hasDot(); 706 mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item)); 707 boolean isDotted = mDotInfo.hasDot(); 708 updateDotScale(wasDotted, isDotted); 709 setContentDescription(getAccessiblityTitle(mInfo.title)); 710 invalidate(); 711 requestLayout(); 712 } 713 714 @Override onRemove(List<ItemInfo> items)715 public void onRemove(List<ItemInfo> items) { 716 updatePreviewItems(false); 717 boolean wasDotted = mDotInfo.hasDot(); 718 items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo); 719 boolean isDotted = mDotInfo.hasDot(); 720 updateDotScale(wasDotted, isDotted); 721 setContentDescription(getAccessiblityTitle(mInfo.title)); 722 invalidate(); 723 requestLayout(); 724 } 725 726 @Override onTitleChanged(CharSequence title)727 public void onTitleChanged(CharSequence title) { 728 mFolderName.setText(title); 729 setContentDescription(getAccessiblityTitle(title)); 730 } 731 732 @Override onTouchEvent(MotionEvent event)733 public boolean onTouchEvent(MotionEvent event) { 734 if (event.getAction() == MotionEvent.ACTION_DOWN 735 && shouldIgnoreTouchDown(event.getX(), event.getY())) { 736 return false; 737 } 738 739 // Call the superclass onTouchEvent first, because sometimes it changes the state to 740 // isPressed() on an ACTION_UP 741 super.onTouchEvent(event); 742 mLongPressHelper.onTouchEvent(event); 743 // Keep receiving the rest of the events 744 return true; 745 } 746 747 /** 748 * Returns true if the touch down at the provided position be ignored 749 */ shouldIgnoreTouchDown(float x, float y)750 protected boolean shouldIgnoreTouchDown(float x, float y) { 751 mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), 752 getHeight() - getPaddingBottom()); 753 return !mTouchArea.contains((int) x, (int) y); 754 } 755 756 @Override cancelLongPress()757 public void cancelLongPress() { 758 super.cancelLongPress(); 759 mLongPressHelper.cancelLongPress(); 760 } 761 removeListeners()762 public void removeListeners() { 763 mInfo.removeListener(this); 764 mInfo.removeListener(mFolder); 765 } 766 isInHotseat()767 private boolean isInHotseat() { 768 return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; 769 } 770 clearLeaveBehindIfExists()771 public void clearLeaveBehindIfExists() { 772 if (getParent() instanceof FolderIconParent) { 773 ((FolderIconParent) getParent()).clearFolderLeaveBehind(this); 774 } 775 } 776 drawLeaveBehindIfExists()777 public void drawLeaveBehindIfExists() { 778 if (getParent() instanceof FolderIconParent) { 779 ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this); 780 } 781 } 782 onFolderClose(int currentPage)783 public void onFolderClose(int currentPage) { 784 mPreviewItemManager.onFolderClose(currentPage); 785 } 786 787 @Override getTranslateDelegate()788 public MultiTranslateDelegate getTranslateDelegate() { 789 return mTranslateDelegate; 790 } 791 792 @Override setReorderBounceScale(float scale)793 public void setReorderBounceScale(float scale) { 794 mScaleForReorderBounce = scale; 795 super.setScaleX(scale); 796 super.setScaleY(scale); 797 } 798 799 @Override getReorderBounceScale()800 public float getReorderBounceScale() { 801 return mScaleForReorderBounce; 802 } 803 804 @Override getViewType()805 public int getViewType() { 806 return DRAGGABLE_ICON; 807 } 808 809 @Override getWorkspaceVisualDragBounds(Rect bounds)810 public void getWorkspaceVisualDragBounds(Rect bounds) { 811 getPreviewBounds(bounds); 812 } 813 814 /** 815 * Returns a formatted accessibility title for folder 816 */ getAccessiblityTitle(CharSequence title)817 public String getAccessiblityTitle(CharSequence title) { 818 int size = mInfo.getContents().size(); 819 if (size < MAX_NUM_ITEMS_IN_PREVIEW) { 820 return getContext().getString(R.string.folder_name_format_exact, title, size); 821 } else { 822 return getContext().getString(R.string.folder_name_format_overflow, title, 823 MAX_NUM_ITEMS_IN_PREVIEW); 824 } 825 } 826 827 @Override onHoverChanged(boolean hovered)828 public void onHoverChanged(boolean hovered) { 829 super.onHoverChanged(hovered); 830 if (enableCursorHoverStates()) { 831 mBackground.setHovered(hovered); 832 } 833 } 834 835 /** 836 * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. 837 */ 838 public interface FolderIconParent { 839 /** 840 * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a 841 * gap where the FolderIcon would be when the Folder is closed. 842 */ drawFolderLeaveBehindForIcon(FolderIcon child)843 void drawFolderLeaveBehindForIcon(FolderIcon child); 844 /** 845 * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed. 846 */ clearFolderLeaveBehind(FolderIcon child)847 void clearFolderLeaveBehind(FolderIcon child); 848 } 849 } 850