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