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.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES; 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.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.util.AttributeSet; 35 import android.util.Property; 36 import android.view.LayoutInflater; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewDebug; 40 import android.view.ViewGroup; 41 import android.widget.FrameLayout; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 46 import com.android.app.animation.Interpolators; 47 import com.android.launcher3.Alarm; 48 import com.android.launcher3.BubbleTextView; 49 import com.android.launcher3.CellLayout; 50 import com.android.launcher3.CheckLongPressHelper; 51 import com.android.launcher3.DeviceProfile; 52 import com.android.launcher3.DropTarget.DragObject; 53 import com.android.launcher3.Launcher; 54 import com.android.launcher3.LauncherSettings; 55 import com.android.launcher3.OnAlarmListener; 56 import com.android.launcher3.R; 57 import com.android.launcher3.Reorderable; 58 import com.android.launcher3.Utilities; 59 import com.android.launcher3.Workspace; 60 import com.android.launcher3.allapps.ActivityAllAppsContainerView; 61 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 62 import com.android.launcher3.dot.FolderDotInfo; 63 import com.android.launcher3.dragndrop.BaseItemDragListener; 64 import com.android.launcher3.dragndrop.DragLayer; 65 import com.android.launcher3.dragndrop.DragView; 66 import com.android.launcher3.dragndrop.DraggableView; 67 import com.android.launcher3.icons.DotRenderer; 68 import com.android.launcher3.logger.LauncherAtom.FromState; 69 import com.android.launcher3.logger.LauncherAtom.ToState; 70 import com.android.launcher3.logging.InstanceId; 71 import com.android.launcher3.logging.StatsLogManager; 72 import com.android.launcher3.model.data.FolderInfo; 73 import com.android.launcher3.model.data.FolderInfo.FolderListener; 74 import com.android.launcher3.model.data.FolderInfo.LabelState; 75 import com.android.launcher3.model.data.ItemInfo; 76 import com.android.launcher3.model.data.WorkspaceItemFactory; 77 import com.android.launcher3.model.data.WorkspaceItemInfo; 78 import com.android.launcher3.util.Executors; 79 import com.android.launcher3.util.MultiTranslateDelegate; 80 import com.android.launcher3.util.Thunk; 81 import com.android.launcher3.views.ActivityContext; 82 import com.android.launcher3.views.IconLabelDotView; 83 import com.android.launcher3.widget.PendingAddShortcutInfo; 84 85 import java.util.ArrayList; 86 import java.util.List; 87 import java.util.function.Predicate; 88 89 90 /** 91 * An icon that can appear on in the workspace representing an {@link Folder}. 92 */ 93 public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView, 94 DraggableView, Reorderable { 95 96 private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); 97 @Thunk ActivityContext mActivity; 98 @Thunk Folder mFolder; 99 public FolderInfo mInfo; 100 101 private CheckLongPressHelper mLongPressHelper; 102 103 static final int DROP_IN_ANIMATION_DURATION = 400; 104 105 // Flag whether the folder should open itself when an item is dragged over is enabled. 106 public static final boolean SPRING_LOADING_ENABLED = true; 107 108 // Delay when drag enters until the folder opens, in miliseconds. 109 private static final int ON_OPEN_DELAY = 800; 110 111 @Thunk BubbleTextView mFolderName; 112 113 PreviewBackground mBackground = new PreviewBackground(); 114 private boolean mBackgroundIsVisible = true; 115 116 FolderGridOrganizer mPreviewVerifier; 117 ClippedFolderIconLayoutRule mPreviewLayoutRule; 118 private PreviewItemManager mPreviewItemManager; 119 private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); 120 private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>(); 121 122 boolean mAnimating = false; 123 124 private Alarm mOpenAlarm = new Alarm(); 125 126 private boolean mForceHideDot; 127 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 128 private FolderDotInfo mDotInfo = new FolderDotInfo(); 129 private DotRenderer mDotRenderer; 130 @ViewDebug.ExportedProperty(category = "launcher", deepExport = true) 131 private DotRenderer.DrawParams mDotParams; 132 private float mDotScale; 133 private Animator mDotScaleAnim; 134 135 private Rect mTouchArea = new Rect(); 136 137 private float mScaleForReorderBounce = 1f; 138 139 private static final Property<FolderIcon, Float> DOT_SCALE_PROPERTY 140 = new Property<FolderIcon, Float>(Float.TYPE, "dotScale") { 141 @Override 142 public Float get(FolderIcon folderIcon) { 143 return folderIcon.mDotScale; 144 } 145 146 @Override 147 public void set(FolderIcon folderIcon, Float value) { 148 folderIcon.mDotScale = value; 149 folderIcon.invalidate(); 150 } 151 }; 152 FolderIcon(Context context, AttributeSet attrs)153 public FolderIcon(Context context, AttributeSet attrs) { 154 super(context, attrs); 155 init(); 156 } 157 FolderIcon(Context context)158 public FolderIcon(Context context) { 159 super(context); 160 init(); 161 } 162 init()163 private void init() { 164 mLongPressHelper = new CheckLongPressHelper(this); 165 mPreviewLayoutRule = new ClippedFolderIconLayoutRule(); 166 mPreviewItemManager = new PreviewItemManager(this); 167 mDotParams = new DotRenderer.DrawParams(); 168 } 169 inflateFolderAndIcon(int resId, T activityContext, ViewGroup group, FolderInfo folderInfo)170 public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId, 171 T activityContext, ViewGroup group, FolderInfo folderInfo) { 172 Folder folder = Folder.fromXml(activityContext); 173 174 FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo); 175 folder.setFolderIcon(icon); 176 folder.bind(folderInfo); 177 icon.setFolder(folder); 178 return icon; 179 } 180 181 /** 182 * Builds a FolderIcon to be added to the Launcher 183 */ inflateIcon(int resId, ActivityContext activity, @Nullable ViewGroup group, FolderInfo folderInfo)184 public static FolderIcon inflateIcon(int resId, ActivityContext activity, 185 @Nullable ViewGroup group, FolderInfo folderInfo) { 186 @SuppressWarnings("all") // suppress dead code warning 187 final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; 188 if (error) { 189 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + 190 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + 191 "is dependent on this"); 192 } 193 194 DeviceProfile grid = activity.getDeviceProfile(); 195 LayoutInflater inflater = (group != null) 196 ? LayoutInflater.from(group.getContext()) 197 : activity.getLayoutInflater(); 198 FolderIcon icon = (FolderIcon) inflater.inflate(resId, group, false); 199 200 icon.setClipToPadding(false); 201 icon.mFolderName = icon.findViewById(R.id.folder_icon_name); 202 icon.mFolderName.setText(folderInfo.title); 203 icon.mFolderName.setCompoundDrawablePadding(0); 204 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); 205 lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; 206 207 icon.setTag(folderInfo); 208 icon.setOnClickListener(activity.getItemOnClickListener()); 209 icon.mInfo = folderInfo; 210 icon.mActivity = activity; 211 icon.mDotRenderer = grid.mDotRendererWorkSpace; 212 213 icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title)); 214 215 // Keep the notification dot up to date with the sum of all the content's dots. 216 FolderDotInfo folderDotInfo = new FolderDotInfo(); 217 for (WorkspaceItemInfo si : folderInfo.contents) { 218 folderDotInfo.addDotInfo(activity.getDotInfoForItem(si)); 219 } 220 icon.setDotInfo(folderDotInfo); 221 222 icon.setAccessibilityDelegate(activity.getAccessibilityDelegate()); 223 224 icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv); 225 icon.mPreviewVerifier.setFolderInfo(folderInfo); 226 icon.updatePreviewItems(false); 227 228 folderInfo.addListener(icon); 229 230 return icon; 231 } 232 animateBgShadowAndStroke()233 public void animateBgShadowAndStroke() { 234 mBackground.fadeInBackgroundShadow(); 235 mBackground.animateBackgroundStroke(); 236 } 237 getFolderName()238 public BubbleTextView getFolderName() { 239 return mFolderName; 240 } 241 getPreviewBounds(Rect outBounds)242 public void getPreviewBounds(Rect outBounds) { 243 mPreviewItemManager.recomputePreviewDrawingParams(); 244 mBackground.getBounds(outBounds); 245 // The preview items go outside of the bounds of the background. 246 Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR); 247 } 248 getBackgroundStrokeWidth()249 public float getBackgroundStrokeWidth() { 250 return mBackground.getStrokeWidth(); 251 } 252 getFolder()253 public Folder getFolder() { 254 return mFolder; 255 } 256 setFolder(Folder folder)257 private void setFolder(Folder folder) { 258 mFolder = folder; 259 } 260 willAcceptItem(ItemInfo item)261 private boolean willAcceptItem(ItemInfo item) { 262 final int itemType = item.itemType; 263 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 264 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && 265 item != mInfo && !mFolder.isOpen()); 266 } 267 acceptDrop(ItemInfo dragInfo)268 public boolean acceptDrop(ItemInfo dragInfo) { 269 return !mFolder.isDestroyed() && willAcceptItem(dragInfo); 270 } 271 addItem(WorkspaceItemInfo item)272 public void addItem(WorkspaceItemInfo item) { 273 mInfo.add(item, true); 274 } 275 removeItem(WorkspaceItemInfo item, boolean animate)276 public void removeItem(WorkspaceItemInfo 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 WorkspaceItemInfo) 290 || (dragInfo instanceof PendingAddShortcutInfo))) { 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 WorkspaceItemInfo destInfo, final View destView, final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)305 public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, 306 final WorkspaceItemInfo 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 WorkspaceItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)332 private void onDrop(final WorkspaceItemInfo 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<WorkspaceItemInfo> 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.contents, 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 WorkspaceItemInfo 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 item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); 484 } else { 485 item = (WorkspaceItemInfo) d.dragInfo; 486 } 487 mFolder.notifyDrop(); 488 onDrop(item, d, null, 1.0f, 489 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(), 490 itemReturnedOnFailedDrop 491 ); 492 } 493 setDotInfo(FolderDotInfo dotInfo)494 public void setDotInfo(FolderDotInfo dotInfo) { 495 updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot()); 496 mDotInfo = dotInfo; 497 } 498 getLayoutRule()499 public ClippedFolderIconLayoutRule getLayoutRule() { 500 return mPreviewLayoutRule; 501 } 502 503 @Override setForceHideDot(boolean forceHideDot)504 public void setForceHideDot(boolean forceHideDot) { 505 if (mForceHideDot == forceHideDot) { 506 return; 507 } 508 mForceHideDot = forceHideDot; 509 510 if (forceHideDot) { 511 invalidate(); 512 } else if (hasDot()) { 513 animateDotScale(0, 1); 514 } 515 } 516 517 /** 518 * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false 519 * (the dot is being added or removed). 520 */ updateDotScale(boolean wasDotted, boolean isDotted)521 private void updateDotScale(boolean wasDotted, boolean isDotted) { 522 float newDotScale = isDotted ? 1f : 0f; 523 // Animate when a dot is first added or when it is removed. 524 if ((wasDotted ^ isDotted) && isShown()) { 525 animateDotScale(newDotScale); 526 } else { 527 cancelDotScaleAnim(); 528 mDotScale = newDotScale; 529 invalidate(); 530 } 531 } 532 cancelDotScaleAnim()533 private void cancelDotScaleAnim() { 534 if (mDotScaleAnim != null) { 535 mDotScaleAnim.cancel(); 536 } 537 } 538 animateDotScale(float... dotScales)539 public void animateDotScale(float... dotScales) { 540 cancelDotScaleAnim(); 541 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 542 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 543 @Override 544 public void onAnimationEnd(Animator animation) { 545 mDotScaleAnim = null; 546 } 547 }); 548 mDotScaleAnim.start(); 549 } 550 hasDot()551 public boolean hasDot() { 552 return mDotInfo != null && mDotInfo.hasDot(); 553 } 554 getLocalCenterForIndex(int index, int curNumItems, int[] center)555 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { 556 mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams( 557 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams); 558 559 mTmpParams.transX += mBackground.basePreviewOffsetX; 560 mTmpParams.transY += mBackground.basePreviewOffsetY; 561 562 float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize(); 563 float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2; 564 float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2; 565 566 center[0] = Math.round(offsetX); 567 center[1] = Math.round(offsetY); 568 return mTmpParams.scale; 569 } 570 setFolderBackground(PreviewBackground bg)571 public void setFolderBackground(PreviewBackground bg) { 572 mBackground = bg; 573 mBackground.setInvalidateDelegate(this); 574 } 575 576 @Override setIconVisible(boolean visible)577 public void setIconVisible(boolean visible) { 578 mBackgroundIsVisible = visible; 579 invalidate(); 580 } 581 getIconVisible()582 public boolean getIconVisible() { 583 return mBackgroundIsVisible; 584 } 585 getFolderBackground()586 public PreviewBackground getFolderBackground() { 587 return mBackground; 588 } 589 getPreviewItemManager()590 public PreviewItemManager getPreviewItemManager() { 591 return mPreviewItemManager; 592 } 593 594 @Override dispatchDraw(Canvas canvas)595 protected void dispatchDraw(Canvas canvas) { 596 super.dispatchDraw(canvas); 597 598 if (!mBackgroundIsVisible) return; 599 600 mPreviewItemManager.recomputePreviewDrawingParams(); 601 602 if (!mBackground.drawingDelegated()) { 603 mBackground.drawBackground(canvas); 604 } 605 606 if (mCurrentPreviewItems.isEmpty() && !mAnimating) return; 607 608 mPreviewItemManager.draw(canvas); 609 610 if (!mBackground.drawingDelegated()) { 611 mBackground.drawBackgroundStroke(canvas); 612 } 613 614 drawDot(canvas); 615 } 616 drawDot(Canvas canvas)617 public void drawDot(Canvas canvas) { 618 if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) { 619 Rect iconBounds = mDotParams.iconBounds; 620 // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered 621 int iconSize = mActivity.getDeviceProfile().iconSizePx; 622 iconBounds.left = (getWidth() - iconSize) / 2; 623 iconBounds.right = iconBounds.left + iconSize; 624 iconBounds.top = getPaddingTop(); 625 iconBounds.bottom = iconBounds.top + iconSize; 626 627 float iconScale = (float) mBackground.previewSize / iconSize; 628 Utilities.scaleRectAboutCenter(iconBounds, iconScale); 629 630 // If we are animating to the accepting state, animate the dot out. 631 mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); 632 mDotParams.dotColor = mBackground.getDotColor(); 633 mDotRenderer.draw(canvas, mDotParams); 634 } 635 } 636 setTextVisible(boolean visible)637 public void setTextVisible(boolean visible) { 638 if (visible) { 639 mFolderName.setVisibility(VISIBLE); 640 } else { 641 mFolderName.setVisibility(INVISIBLE); 642 } 643 } 644 getTextVisible()645 public boolean getTextVisible() { 646 return mFolderName.getVisibility() == VISIBLE; 647 } 648 649 /** 650 * Returns the list of items which should be visible in the preview 651 */ getPreviewItemsOnPage(int page)652 public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) { 653 return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents); 654 } 655 656 @Override verifyDrawable(@onNull Drawable who)657 protected boolean verifyDrawable(@NonNull Drawable who) { 658 return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who); 659 } 660 661 @Override onItemsChanged(boolean animate)662 public void onItemsChanged(boolean animate) { 663 updatePreviewItems(animate); 664 invalidate(); 665 requestLayout(); 666 } 667 updatePreviewItems(boolean animate)668 private void updatePreviewItems(boolean animate) { 669 mPreviewItemManager.updatePreviewItems(animate); 670 mCurrentPreviewItems.clear(); 671 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 672 } 673 674 /** 675 * Updates the preview items which match the provided condition 676 */ updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck)677 public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) { 678 mPreviewItemManager.updatePreviewItems(itemCheck); 679 } 680 681 @Override onAdd(WorkspaceItemInfo item, int rank)682 public void onAdd(WorkspaceItemInfo item, int rank) { 683 updatePreviewItems(false); 684 boolean wasDotted = mDotInfo.hasDot(); 685 mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item)); 686 boolean isDotted = mDotInfo.hasDot(); 687 updateDotScale(wasDotted, isDotted); 688 setContentDescription(getAccessiblityTitle(mInfo.title)); 689 invalidate(); 690 requestLayout(); 691 } 692 693 @Override onRemove(List<WorkspaceItemInfo> items)694 public void onRemove(List<WorkspaceItemInfo> items) { 695 updatePreviewItems(false); 696 boolean wasDotted = mDotInfo.hasDot(); 697 items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo); 698 boolean isDotted = mDotInfo.hasDot(); 699 updateDotScale(wasDotted, isDotted); 700 setContentDescription(getAccessiblityTitle(mInfo.title)); 701 invalidate(); 702 requestLayout(); 703 } 704 onTitleChanged(CharSequence title)705 public void onTitleChanged(CharSequence title) { 706 mFolderName.setText(title); 707 setContentDescription(getAccessiblityTitle(title)); 708 } 709 710 @Override onTouchEvent(MotionEvent event)711 public boolean onTouchEvent(MotionEvent event) { 712 if (event.getAction() == MotionEvent.ACTION_DOWN 713 && shouldIgnoreTouchDown(event.getX(), event.getY())) { 714 return false; 715 } 716 717 // Call the superclass onTouchEvent first, because sometimes it changes the state to 718 // isPressed() on an ACTION_UP 719 super.onTouchEvent(event); 720 mLongPressHelper.onTouchEvent(event); 721 // Keep receiving the rest of the events 722 return true; 723 } 724 725 /** 726 * Returns true if the touch down at the provided position be ignored 727 */ shouldIgnoreTouchDown(float x, float y)728 protected boolean shouldIgnoreTouchDown(float x, float y) { 729 mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), 730 getHeight() - getPaddingBottom()); 731 return !mTouchArea.contains((int) x, (int) y); 732 } 733 734 @Override cancelLongPress()735 public void cancelLongPress() { 736 super.cancelLongPress(); 737 mLongPressHelper.cancelLongPress(); 738 } 739 removeListeners()740 public void removeListeners() { 741 mInfo.removeListener(this); 742 mInfo.removeListener(mFolder); 743 } 744 isInHotseat()745 private boolean isInHotseat() { 746 return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; 747 } 748 clearLeaveBehindIfExists()749 public void clearLeaveBehindIfExists() { 750 if (getParent() instanceof FolderIconParent) { 751 ((FolderIconParent) getParent()).clearFolderLeaveBehind(this); 752 } 753 } 754 drawLeaveBehindIfExists()755 public void drawLeaveBehindIfExists() { 756 if (getParent() instanceof FolderIconParent) { 757 ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this); 758 } 759 } 760 onFolderClose(int currentPage)761 public void onFolderClose(int currentPage) { 762 mPreviewItemManager.onFolderClose(currentPage); 763 } 764 765 @Override getTranslateDelegate()766 public MultiTranslateDelegate getTranslateDelegate() { 767 return mTranslateDelegate; 768 } 769 770 @Override setReorderBounceScale(float scale)771 public void setReorderBounceScale(float scale) { 772 mScaleForReorderBounce = scale; 773 super.setScaleX(scale); 774 super.setScaleY(scale); 775 } 776 777 @Override getReorderBounceScale()778 public float getReorderBounceScale() { 779 return mScaleForReorderBounce; 780 } 781 782 @Override getViewType()783 public int getViewType() { 784 return DRAGGABLE_ICON; 785 } 786 787 @Override getWorkspaceVisualDragBounds(Rect bounds)788 public void getWorkspaceVisualDragBounds(Rect bounds) { 789 getPreviewBounds(bounds); 790 } 791 792 /** 793 * Returns a formatted accessibility title for folder 794 */ getAccessiblityTitle(CharSequence title)795 public String getAccessiblityTitle(CharSequence title) { 796 int size = mInfo.contents.size(); 797 if (size < MAX_NUM_ITEMS_IN_PREVIEW) { 798 return getContext().getString(R.string.folder_name_format_exact, title, size); 799 } else { 800 return getContext().getString(R.string.folder_name_format_overflow, title, 801 MAX_NUM_ITEMS_IN_PREVIEW); 802 } 803 } 804 805 @Override onHoverChanged(boolean hovered)806 public void onHoverChanged(boolean hovered) { 807 super.onHoverChanged(hovered); 808 if (ENABLE_CURSOR_HOVER_STATES.get()) { 809 mBackground.setHovered(hovered); 810 } 811 } 812 813 /** 814 * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. 815 */ 816 public interface FolderIconParent { 817 /** 818 * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a 819 * gap where the FolderIcon would be when the Folder is closed. 820 */ drawFolderLeaveBehindForIcon(FolderIcon child)821 void drawFolderLeaveBehindForIcon(FolderIcon child); 822 /** 823 * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed. 824 */ clearFolderLeaveBehind(FolderIcon child)825 void clearFolderLeaveBehind(FolderIcon child); 826 } 827 } 828