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