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