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 mTranslationForReorderBounce = new PointF(0, 0); 136 private final PointF mTranslationForReorderPreview = new PointF(0, 0); 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 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 // The preview items go outside of the bounds of the background. 241 Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR); 242 } 243 getBackgroundStrokeWidth()244 public float getBackgroundStrokeWidth() { 245 return mBackground.getStrokeWidth(); 246 } 247 getFolder()248 public Folder getFolder() { 249 return mFolder; 250 } 251 setFolder(Folder folder)252 private void setFolder(Folder folder) { 253 mFolder = folder; 254 } 255 willAcceptItem(ItemInfo item)256 private boolean willAcceptItem(ItemInfo item) { 257 final int itemType = item.itemType; 258 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 259 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT || 260 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && 261 item != mInfo && !mFolder.isOpen()); 262 } 263 acceptDrop(ItemInfo dragInfo)264 public boolean acceptDrop(ItemInfo dragInfo) { 265 return !mFolder.isDestroyed() && willAcceptItem(dragInfo); 266 } 267 addItem(WorkspaceItemInfo item)268 public void addItem(WorkspaceItemInfo item) { 269 mInfo.add(item, true); 270 } 271 removeItem(WorkspaceItemInfo item, boolean animate)272 public void removeItem(WorkspaceItemInfo item, boolean animate) { 273 mInfo.remove(item, animate); 274 } 275 onDragEnter(ItemInfo dragInfo)276 public void onDragEnter(ItemInfo dragInfo) { 277 if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return; 278 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 279 CellLayout cl = (CellLayout) getParent().getParent(); 280 281 mBackground.animateToAccept(cl, lp.cellX, lp.cellY); 282 mOpenAlarm.setOnAlarmListener(mOnOpenListener); 283 if (SPRING_LOADING_ENABLED && 284 ((dragInfo instanceof AppInfo) 285 || (dragInfo instanceof WorkspaceItemInfo) 286 || (dragInfo instanceof PendingAddShortcutInfo))) { 287 mOpenAlarm.setAlarm(ON_OPEN_DELAY); 288 } 289 } 290 291 OnAlarmListener mOnOpenListener = new OnAlarmListener() { 292 public void onAlarm(Alarm alarm) { 293 mFolder.beginExternalDrag(); 294 } 295 }; 296 prepareCreateAnimation(final View destView)297 public Drawable prepareCreateAnimation(final View destView) { 298 return mPreviewItemManager.prepareCreateAnimation(destView); 299 } 300 performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer)301 public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, 302 final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, 303 float scaleRelativeToDragLayer) { 304 final DragView srcView = d.dragView; 305 prepareCreateAnimation(destView); 306 addItem(destInfo); 307 // This will animate the first item from it's position as an icon into its 308 // position as the first item in the preview 309 mPreviewItemManager.createFirstItemAnimation(false /* reverse */, null) 310 .start(); 311 312 // This will animate the dragView (srcView) into the new folder 313 onDrop(srcInfo, d, dstRect, scaleRelativeToDragLayer, 1, 314 false /* itemReturnedOnFailedDrop */); 315 } 316 performDestroyAnimation(Runnable onCompleteRunnable)317 public void performDestroyAnimation(Runnable onCompleteRunnable) { 318 // This will animate the final item in the preview to be full size. 319 mPreviewItemManager.createFirstItemAnimation(true /* reverse */, onCompleteRunnable) 320 .start(); 321 } 322 onDragExit()323 public void onDragExit() { 324 mBackground.animateToRest(); 325 mOpenAlarm.cancelAlarm(); 326 } 327 onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop)328 private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, 329 float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) { 330 item.cellX = -1; 331 item.cellY = -1; 332 DragView animateView = d.dragView; 333 // Typically, the animateView corresponds to the DragView; however, if this is being done 334 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we 335 // will not have a view to animate 336 if (animateView != null && mActivity instanceof Launcher) { 337 final Launcher launcher = (Launcher) mActivity; 338 DragLayer dragLayer = launcher.getDragLayer(); 339 Rect from = new Rect(); 340 dragLayer.getViewRectRelativeToSelf(animateView, from); 341 Rect to = finalRect; 342 if (to == null) { 343 to = new Rect(); 344 Workspace workspace = launcher.getWorkspace(); 345 // Set cellLayout and this to it's final state to compute final animation locations 346 workspace.setFinalTransitionTransform(); 347 float scaleX = getScaleX(); 348 float scaleY = getScaleY(); 349 setScaleX(1.0f); 350 setScaleY(1.0f); 351 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); 352 // Finished computing final animation locations, restore current state 353 setScaleX(scaleX); 354 setScaleY(scaleY); 355 workspace.resetTransitionTransform(); 356 } 357 358 int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1); 359 boolean itemAdded = false; 360 if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) { 361 List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); 362 mInfo.add(item, index, false); 363 mCurrentPreviewItems.clear(); 364 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 365 366 if (!oldPreviewItems.equals(mCurrentPreviewItems)) { 367 int newIndex = mCurrentPreviewItems.indexOf(item); 368 if (newIndex >= 0) { 369 // If the item dropped is going to be in the preview, we update the 370 // index here to reflect its position in the preview. 371 index = newIndex; 372 } 373 374 mPreviewItemManager.hidePreviewItem(index, true); 375 mPreviewItemManager.onDrop(oldPreviewItems, mCurrentPreviewItems, item); 376 itemAdded = true; 377 } else { 378 removeItem(item, false); 379 } 380 } 381 382 if (!itemAdded) { 383 mInfo.add(item, index, true); 384 } 385 386 int[] center = new int[2]; 387 float scale = getLocalCenterForIndex(index, numItemsInPreview, center); 388 center[0] = Math.round(scaleRelativeToDragLayer * center[0]); 389 center[1] = Math.round(scaleRelativeToDragLayer * center[1]); 390 391 to.offset(center[0] - animateView.getMeasuredWidth() / 2, 392 center[1] - animateView.getMeasuredHeight() / 2); 393 394 float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f; 395 396 float finalScale = scale * scaleRelativeToDragLayer; 397 398 // Account for potentially different icon sizes with non-default grid settings 399 if (d.dragSource instanceof AllAppsContainerView) { 400 DeviceProfile grid = mActivity.getDeviceProfile(); 401 float containerScale = (1f * grid.iconSizePx / grid.allAppsIconSizePx); 402 finalScale *= containerScale; 403 } 404 405 final int finalIndex = index; 406 dragLayer.animateView(animateView, from, to, finalAlpha, 407 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION, 408 Interpolators.DEACCEL_2, Interpolators.ACCEL_2, 409 () -> { 410 mPreviewItemManager.hidePreviewItem(finalIndex, false); 411 mFolder.showItem(item); 412 }, DragLayer.ANIMATION_END_DISAPPEAR, null); 413 414 mFolder.hideItem(item); 415 416 if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true); 417 418 FolderNameInfos nameInfos = new FolderNameInfos(); 419 if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) { 420 Executors.MODEL_EXECUTOR.post(() -> { 421 d.folderNameProvider.getSuggestedFolderName( 422 getContext(), mInfo.contents, nameInfos); 423 showFinalView(finalIndex, item, nameInfos, d.logInstanceId); 424 }); 425 } else { 426 showFinalView(finalIndex, item, nameInfos, d.logInstanceId); 427 } 428 } else { 429 addItem(item); 430 } 431 } 432 showFinalView(int finalIndex, final WorkspaceItemInfo item, FolderNameInfos nameInfos, InstanceId instanceId)433 private void showFinalView(int finalIndex, final WorkspaceItemInfo item, 434 FolderNameInfos nameInfos, InstanceId instanceId) { 435 postDelayed(() -> { 436 setLabelSuggestion(nameInfos, instanceId); 437 invalidate(); 438 }, DROP_IN_ANIMATION_DURATION); 439 } 440 441 /** 442 * Set the suggested folder name. 443 */ setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId)444 public void setLabelSuggestion(FolderNameInfos nameInfos, InstanceId instanceId) { 445 if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) { 446 return; 447 } 448 if (!mInfo.getLabelState().equals(LabelState.UNLABELED)) { 449 return; 450 } 451 if (nameInfos == null || !nameInfos.hasSuggestions()) { 452 StatsLogManager.newInstance(getContext()).logger() 453 .withInstanceId(instanceId) 454 .withItemInfo(mInfo) 455 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_SUGGESTIONS); 456 return; 457 } 458 if (!nameInfos.hasPrimary()) { 459 StatsLogManager.newInstance(getContext()).logger() 460 .withInstanceId(instanceId) 461 .withItemInfo(mInfo) 462 .log(LAUNCHER_FOLDER_AUTO_LABELING_SKIPPED_EMPTY_PRIMARY); 463 return; 464 } 465 CharSequence newTitle = nameInfos.getLabels()[0]; 466 FromState fromState = mInfo.getFromLabelState(); 467 468 mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter()); 469 onTitleChanged(mInfo.title); 470 mFolder.mFolderName.setText(mInfo.title); 471 472 // Logging for folder creation flow 473 StatsLogManager.newInstance(getContext()).logger() 474 .withInstanceId(instanceId) 475 .withItemInfo(mInfo) 476 .withFromState(fromState) 477 .withToState(ToState.TO_SUGGESTION0) 478 // When LAUNCHER_FOLDER_LABEL_UPDATED event.edit_text does not have delimiter, 479 // event is assumed to be folder creation on the server side. 480 .withEditText(newTitle.toString()) 481 .log(LAUNCHER_FOLDER_AUTO_LABELED); 482 } 483 484 onDrop(DragObject d, boolean itemReturnedOnFailedDrop)485 public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { 486 WorkspaceItemInfo item; 487 if (d.dragInfo instanceof AppInfo) { 488 // Came from all apps -- make a copy 489 item = ((AppInfo) d.dragInfo).makeWorkspaceItem(); 490 } else if (d.dragSource instanceof BaseItemDragListener){ 491 // Came from a different window -- make a copy 492 item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); 493 } else { 494 item = (WorkspaceItemInfo) d.dragInfo; 495 } 496 mFolder.notifyDrop(); 497 onDrop(item, d, null, 1.0f, 498 itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(), 499 itemReturnedOnFailedDrop 500 ); 501 } 502 setDotInfo(FolderDotInfo dotInfo)503 public void setDotInfo(FolderDotInfo dotInfo) { 504 updateDotScale(mDotInfo.hasDot(), dotInfo.hasDot()); 505 mDotInfo = dotInfo; 506 } 507 getLayoutRule()508 public ClippedFolderIconLayoutRule getLayoutRule() { 509 return mPreviewLayoutRule; 510 } 511 512 @Override setForceHideDot(boolean forceHideDot)513 public void setForceHideDot(boolean forceHideDot) { 514 if (mForceHideDot == forceHideDot) { 515 return; 516 } 517 mForceHideDot = forceHideDot; 518 519 if (forceHideDot) { 520 invalidate(); 521 } else if (hasDot()) { 522 animateDotScale(0, 1); 523 } 524 } 525 526 /** 527 * Sets mDotScale to 1 or 0, animating if wasDotted or isDotted is false 528 * (the dot is being added or removed). 529 */ updateDotScale(boolean wasDotted, boolean isDotted)530 private void updateDotScale(boolean wasDotted, boolean isDotted) { 531 float newDotScale = isDotted ? 1f : 0f; 532 // Animate when a dot is first added or when it is removed. 533 if ((wasDotted ^ isDotted) && isShown()) { 534 animateDotScale(newDotScale); 535 } else { 536 cancelDotScaleAnim(); 537 mDotScale = newDotScale; 538 invalidate(); 539 } 540 } 541 cancelDotScaleAnim()542 private void cancelDotScaleAnim() { 543 if (mDotScaleAnim != null) { 544 mDotScaleAnim.cancel(); 545 } 546 } 547 animateDotScale(float... dotScales)548 public void animateDotScale(float... dotScales) { 549 cancelDotScaleAnim(); 550 mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales); 551 mDotScaleAnim.addListener(new AnimatorListenerAdapter() { 552 @Override 553 public void onAnimationEnd(Animator animation) { 554 mDotScaleAnim = null; 555 } 556 }); 557 mDotScaleAnim.start(); 558 } 559 hasDot()560 public boolean hasDot() { 561 return mDotInfo != null && mDotInfo.hasDot(); 562 } 563 getLocalCenterForIndex(int index, int curNumItems, int[] center)564 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { 565 mTmpParams = mPreviewItemManager.computePreviewItemDrawingParams( 566 Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index), curNumItems, mTmpParams); 567 568 mTmpParams.transX += mBackground.basePreviewOffsetX; 569 mTmpParams.transY += mBackground.basePreviewOffsetY; 570 571 float intrinsicIconSize = mPreviewItemManager.getIntrinsicIconSize(); 572 float offsetX = mTmpParams.transX + (mTmpParams.scale * intrinsicIconSize) / 2; 573 float offsetY = mTmpParams.transY + (mTmpParams.scale * intrinsicIconSize) / 2; 574 575 center[0] = Math.round(offsetX); 576 center[1] = Math.round(offsetY); 577 return mTmpParams.scale; 578 } 579 setFolderBackground(PreviewBackground bg)580 public void setFolderBackground(PreviewBackground bg) { 581 mBackground = bg; 582 mBackground.setInvalidateDelegate(this); 583 } 584 585 @Override setIconVisible(boolean visible)586 public void setIconVisible(boolean visible) { 587 mBackgroundIsVisible = visible; 588 invalidate(); 589 } 590 getIconVisible()591 public boolean getIconVisible() { 592 return mBackgroundIsVisible; 593 } 594 getFolderBackground()595 public PreviewBackground getFolderBackground() { 596 return mBackground; 597 } 598 getPreviewItemManager()599 public PreviewItemManager getPreviewItemManager() { 600 return mPreviewItemManager; 601 } 602 603 @Override dispatchDraw(Canvas canvas)604 protected void dispatchDraw(Canvas canvas) { 605 super.dispatchDraw(canvas); 606 607 if (!mBackgroundIsVisible) return; 608 609 mPreviewItemManager.recomputePreviewDrawingParams(); 610 611 if (!mBackground.drawingDelegated()) { 612 mBackground.drawBackground(canvas); 613 } 614 615 if (mCurrentPreviewItems.isEmpty() && !mAnimating) return; 616 617 mPreviewItemManager.draw(canvas); 618 619 if (!mBackground.drawingDelegated()) { 620 mBackground.drawBackgroundStroke(canvas); 621 } 622 623 drawDot(canvas); 624 } 625 drawDot(Canvas canvas)626 public void drawDot(Canvas canvas) { 627 if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) { 628 Rect iconBounds = mDotParams.iconBounds; 629 BubbleTextView.getIconBounds(this, iconBounds, mActivity.getDeviceProfile().iconSizePx); 630 float iconScale = (float) mBackground.previewSize / iconBounds.width(); 631 Utilities.scaleRectAboutCenter(iconBounds, iconScale); 632 633 // If we are animating to the accepting state, animate the dot out. 634 mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress()); 635 mDotParams.color = mBackground.getDotColor(); 636 mDotRenderer.draw(canvas, mDotParams); 637 } 638 } 639 setTextVisible(boolean visible)640 public void setTextVisible(boolean visible) { 641 if (visible) { 642 mFolderName.setVisibility(VISIBLE); 643 } else { 644 mFolderName.setVisibility(INVISIBLE); 645 } 646 } 647 getTextVisible()648 public boolean getTextVisible() { 649 return mFolderName.getVisibility() == VISIBLE; 650 } 651 652 /** 653 * Returns the list of items which should be visible in the preview 654 */ getPreviewItemsOnPage(int page)655 public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) { 656 return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents); 657 } 658 659 @Override verifyDrawable(@onNull Drawable who)660 protected boolean verifyDrawable(@NonNull Drawable who) { 661 return mPreviewItemManager.verifyDrawable(who) || super.verifyDrawable(who); 662 } 663 664 @Override onItemsChanged(boolean animate)665 public void onItemsChanged(boolean animate) { 666 updatePreviewItems(animate); 667 invalidate(); 668 requestLayout(); 669 } 670 updatePreviewItems(boolean animate)671 private void updatePreviewItems(boolean animate) { 672 mPreviewItemManager.updatePreviewItems(animate); 673 mCurrentPreviewItems.clear(); 674 mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); 675 } 676 677 /** 678 * Updates the preview items which match the provided condition 679 */ updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck)680 public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) { 681 mPreviewItemManager.updatePreviewItems(itemCheck); 682 } 683 684 @Override onAdd(WorkspaceItemInfo item, int rank)685 public void onAdd(WorkspaceItemInfo item, int rank) { 686 boolean wasDotted = mDotInfo.hasDot(); 687 mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item)); 688 boolean isDotted = mDotInfo.hasDot(); 689 updateDotScale(wasDotted, isDotted); 690 setContentDescription(getAccessiblityTitle(mInfo.title)); 691 invalidate(); 692 requestLayout(); 693 } 694 695 @Override onRemove(List<WorkspaceItemInfo> items)696 public void onRemove(List<WorkspaceItemInfo> items) { 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 updateTranslation()766 private void updateTranslation() { 767 super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x); 768 super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y); 769 } 770 setReorderBounceOffset(float x, float y)771 public void setReorderBounceOffset(float x, float y) { 772 mTranslationForReorderBounce.set(x, y); 773 updateTranslation(); 774 } 775 getReorderBounceOffset(PointF offset)776 public void getReorderBounceOffset(PointF offset) { 777 offset.set(mTranslationForReorderBounce); 778 } 779 780 @Override setReorderPreviewOffset(float x, float y)781 public void setReorderPreviewOffset(float x, float y) { 782 mTranslationForReorderPreview.set(x, y); 783 updateTranslation(); 784 } 785 786 @Override getReorderPreviewOffset(PointF offset)787 public void getReorderPreviewOffset(PointF offset) { 788 offset.set(mTranslationForReorderPreview); 789 } 790 setReorderBounceScale(float scale)791 public void setReorderBounceScale(float scale) { 792 mScaleForReorderBounce = scale; 793 super.setScaleX(scale); 794 super.setScaleY(scale); 795 } 796 getReorderBounceScale()797 public float getReorderBounceScale() { 798 return mScaleForReorderBounce; 799 } 800 getView()801 public View getView() { 802 return this; 803 } 804 805 @Override getViewType()806 public int getViewType() { 807 return DRAGGABLE_ICON; 808 } 809 810 @Override getWorkspaceVisualDragBounds(Rect bounds)811 public void getWorkspaceVisualDragBounds(Rect bounds) { 812 getPreviewBounds(bounds); 813 } 814 815 /** 816 * Returns a formatted accessibility title for folder 817 */ getAccessiblityTitle(CharSequence title)818 public String getAccessiblityTitle(CharSequence title) { 819 int size = mInfo.contents.size(); 820 if (size < MAX_NUM_ITEMS_IN_PREVIEW) { 821 return getContext().getString(R.string.folder_name_format_exact, title, size); 822 } else { 823 return getContext().getString(R.string.folder_name_format_overflow, title, 824 MAX_NUM_ITEMS_IN_PREVIEW); 825 } 826 } 827 828 /** 829 * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. 830 */ 831 public interface FolderIconParent { 832 /** 833 * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a 834 * gap where the FolderIcon would be when the Folder is closed. 835 */ drawFolderLeaveBehindForIcon(FolderIcon child)836 void drawFolderLeaveBehindForIcon(FolderIcon child); 837 /** 838 * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed. 839 */ clearFolderLeaveBehind(FolderIcon child)840 void clearFolderLeaveBehind(FolderIcon child); 841 } 842 } 843