1 /* 2 * Copyright (C) 2015 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.AbstractFloatingView.TYPE_ALL; 20 import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER; 21 22 import android.annotation.SuppressLint; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.Path; 26 import android.graphics.drawable.Drawable; 27 import android.util.ArrayMap; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.view.Gravity; 31 import android.view.View; 32 import android.view.ViewDebug; 33 34 import com.android.launcher3.AbstractFloatingView; 35 import com.android.launcher3.BubbleTextView; 36 import com.android.launcher3.CellLayout; 37 import com.android.launcher3.DeviceProfile; 38 import com.android.launcher3.InvariantDeviceProfile; 39 import com.android.launcher3.LauncherAppState; 40 import com.android.launcher3.PagedView; 41 import com.android.launcher3.R; 42 import com.android.launcher3.ShortcutAndWidgetContainer; 43 import com.android.launcher3.Utilities; 44 import com.android.launcher3.Workspace.ItemOperator; 45 import com.android.launcher3.keyboard.ViewGroupFocusHelper; 46 import com.android.launcher3.model.data.ItemInfo; 47 import com.android.launcher3.model.data.WorkspaceItemInfo; 48 import com.android.launcher3.pageindicators.PageIndicatorDots; 49 import com.android.launcher3.touch.ItemClickHandler; 50 import com.android.launcher3.util.Thunk; 51 import com.android.launcher3.util.ViewCache; 52 import com.android.launcher3.views.ActivityContext; 53 import com.android.launcher3.views.ClipPathView; 54 55 import java.util.ArrayList; 56 import java.util.Iterator; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.function.ToIntFunction; 60 import java.util.stream.Collectors; 61 62 public class FolderPagedView extends PagedView<PageIndicatorDots> implements ClipPathView { 63 64 private static final String TAG = "FolderPagedView"; 65 66 private static final int REORDER_ANIMATION_DURATION = 230; 67 private static final int START_VIEW_REORDER_DELAY = 30; 68 private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f; 69 70 /** 71 * Fraction of the width to scroll when showing the next page hint. 72 */ 73 private static final float SCROLL_HINT_FRACTION = 0.07f; 74 75 private static final int[] sTmpArray = new int[2]; 76 77 public final boolean mIsRtl; 78 79 private final ViewGroupFocusHelper mFocusIndicatorHelper; 80 81 @Thunk final ArrayMap<View, Runnable> mPendingAnimations = new ArrayMap<>(); 82 83 private final FolderGridOrganizer mOrganizer; 84 private final ViewCache mViewCache; 85 86 private int mAllocatedContentSize; 87 @ViewDebug.ExportedProperty(category = "launcher") 88 private int mGridCountX; 89 @ViewDebug.ExportedProperty(category = "launcher") 90 private int mGridCountY; 91 92 private Folder mFolder; 93 94 private Path mClipPath; 95 96 // If the views are attached to the folder or not. A folder should be bound when its 97 // animating or is open. 98 private boolean mViewsBound = false; 99 FolderPagedView(Context context, AttributeSet attrs)100 public FolderPagedView(Context context, AttributeSet attrs) { 101 super(context, attrs); 102 InvariantDeviceProfile profile = LauncherAppState.getIDP(context); 103 mOrganizer = new FolderGridOrganizer(profile); 104 105 mIsRtl = Utilities.isRtl(getResources()); 106 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 107 108 mFocusIndicatorHelper = new ViewGroupFocusHelper(this); 109 mViewCache = ActivityContext.lookupContext(context).getViewCache(); 110 } 111 setFolder(Folder folder)112 public void setFolder(Folder folder) { 113 mFolder = folder; 114 mPageIndicator = folder.findViewById(R.id.folder_page_indicator); 115 initParentViews(folder); 116 } 117 118 /** 119 * Sets up the grid size such that {@param count} items can fit in the grid. 120 */ setupContentDimensions(int count)121 private void setupContentDimensions(int count) { 122 mAllocatedContentSize = count; 123 mOrganizer.setContentSize(count); 124 mGridCountX = mOrganizer.getCountX(); 125 mGridCountY = mOrganizer.getCountY(); 126 127 // Update grid size 128 for (int i = getPageCount() - 1; i >= 0; i--) { 129 getPageAt(i).setGridSize(mGridCountX, mGridCountY); 130 } 131 } 132 133 @Override dispatchDraw(Canvas canvas)134 protected void dispatchDraw(Canvas canvas) { 135 if (mClipPath != null) { 136 int count = canvas.save(); 137 canvas.clipPath(mClipPath); 138 mFocusIndicatorHelper.draw(canvas); 139 super.dispatchDraw(canvas); 140 canvas.restoreToCount(count); 141 } else { 142 mFocusIndicatorHelper.draw(canvas); 143 super.dispatchDraw(canvas); 144 } 145 } 146 147 /** 148 * Binds items to the layout. 149 */ bindItems(List<WorkspaceItemInfo> items)150 public void bindItems(List<WorkspaceItemInfo> items) { 151 if (mViewsBound) { 152 unbindItems(); 153 } 154 arrangeChildren(items.stream().map(this::createNewView).collect(Collectors.toList())); 155 mViewsBound = true; 156 } 157 158 /** 159 * Removes all the icons from the folder 160 */ unbindItems()161 public void unbindItems() { 162 for (int i = getChildCount() - 1; i >= 0; i--) { 163 CellLayout page = (CellLayout) getChildAt(i); 164 ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets(); 165 for (int j = container.getChildCount() - 1; j >= 0; j--) { 166 container.getChildAt(j).setVisibility(View.VISIBLE); 167 mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j)); 168 } 169 page.removeAllViews(); 170 mViewCache.recycleView(R.layout.folder_page, page); 171 } 172 removeAllViews(); 173 mViewsBound = false; 174 } 175 176 /** 177 * Returns true if the icons are bound to the folder 178 */ areViewsBound()179 public boolean areViewsBound() { 180 return mViewsBound; 181 } 182 183 /** 184 * Creates and adds an icon corresponding to the provided rank 185 * @return the created icon 186 */ createAndAddViewForRank(WorkspaceItemInfo item, int rank)187 public View createAndAddViewForRank(WorkspaceItemInfo item, int rank) { 188 View icon = createNewView(item); 189 if (!mViewsBound) { 190 return icon; 191 } 192 ArrayList<View> views = new ArrayList<>(mFolder.getIconsInReadingOrder()); 193 views.add(rank, icon); 194 arrangeChildren(views); 195 return icon; 196 } 197 198 /** 199 * Adds the {@param view} to the layout based on {@param rank} and updated the position 200 * related attributes. It assumes that {@param item} is already attached to the view. 201 */ addViewForRank(View view, WorkspaceItemInfo item, int rank)202 public void addViewForRank(View view, WorkspaceItemInfo item, int rank) { 203 int pageNo = rank / mOrganizer.getMaxItemsPerPage(); 204 205 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); 206 lp.setCellXY(mOrganizer.getPosForRank(rank)); 207 getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true); 208 } 209 210 @SuppressLint("InflateParams") createNewView(WorkspaceItemInfo item)211 public View createNewView(WorkspaceItemInfo item) { 212 if (item == null) { 213 return null; 214 } 215 final BubbleTextView textView = mViewCache.getView( 216 R.layout.folder_application, getContext(), null); 217 textView.applyFromWorkspaceItem(item); 218 textView.setOnClickListener(ItemClickHandler.INSTANCE); 219 textView.setOnLongClickListener(mFolder); 220 textView.setOnFocusChangeListener(mFocusIndicatorHelper); 221 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) textView.getLayoutParams(); 222 if (lp == null) { 223 textView.setLayoutParams(new CellLayout.LayoutParams( 224 item.cellX, item.cellY, item.spanX, item.spanY)); 225 } else { 226 lp.cellX = item.cellX; 227 lp.cellY = item.cellY; 228 lp.cellHSpan = lp.cellVSpan = 1; 229 } 230 return textView; 231 } 232 233 @Override getPageAt(int index)234 public CellLayout getPageAt(int index) { 235 return (CellLayout) getChildAt(index); 236 } 237 getCurrentCellLayout()238 public CellLayout getCurrentCellLayout() { 239 return getPageAt(getNextPage()); 240 } 241 createAndAddNewPage()242 private CellLayout createAndAddNewPage() { 243 DeviceProfile grid = mFolder.mActivityContext.getDeviceProfile(); 244 CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this); 245 page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); 246 page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); 247 page.setInvertIfRtl(true); 248 page.setGridSize(mGridCountX, mGridCountY); 249 250 addView(page, -1, generateDefaultLayoutParams()); 251 return page; 252 } 253 254 @Override getChildGap()255 protected int getChildGap() { 256 return getPaddingLeft() + getPaddingRight(); 257 } 258 setFixedSize(int width, int height)259 public void setFixedSize(int width, int height) { 260 width -= (getPaddingLeft() + getPaddingRight()); 261 height -= (getPaddingTop() + getPaddingBottom()); 262 for (int i = getChildCount() - 1; i >= 0; i --) { 263 ((CellLayout) getChildAt(i)).setFixedSize(width, height); 264 } 265 } 266 removeItem(View v)267 public void removeItem(View v) { 268 for (int i = getChildCount() - 1; i >= 0; i --) { 269 getPageAt(i).removeView(v); 270 } 271 } 272 273 @Override onScrollChanged(int l, int t, int oldl, int oldt)274 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 275 super.onScrollChanged(l, t, oldl, oldt); 276 if (mMaxScroll > 0) mPageIndicator.setScroll(l, mMaxScroll); 277 } 278 279 /** 280 * Updates position and rank of all the children in the view. 281 * It essentially removes all views from all the pages and then adds them again in appropriate 282 * page. 283 * 284 * @param list the ordered list of children. 285 */ 286 @SuppressLint("RtlHardcoded") arrangeChildren(List<View> list)287 public void arrangeChildren(List<View> list) { 288 int itemCount = list.size(); 289 ArrayList<CellLayout> pages = new ArrayList<>(); 290 for (int i = 0; i < getChildCount(); i++) { 291 CellLayout page = (CellLayout) getChildAt(i); 292 page.removeAllViews(); 293 pages.add(page); 294 } 295 mOrganizer.setFolderInfo(mFolder.getInfo()); 296 setupContentDimensions(itemCount); 297 298 Iterator<CellLayout> pageItr = pages.iterator(); 299 CellLayout currentPage = null; 300 301 int position = 0; 302 int rank = 0; 303 304 for (int i = 0; i < itemCount; i++) { 305 View v = list.size() > i ? list.get(i) : null; 306 if (currentPage == null || position >= mOrganizer.getMaxItemsPerPage()) { 307 // Next page 308 if (pageItr.hasNext()) { 309 currentPage = pageItr.next(); 310 } else { 311 currentPage = createAndAddNewPage(); 312 } 313 position = 0; 314 } 315 316 if (v != null) { 317 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); 318 ItemInfo info = (ItemInfo) v.getTag(); 319 lp.setCellXY(mOrganizer.getPosForRank(rank)); 320 currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true); 321 322 if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) { 323 ((BubbleTextView) v).verifyHighRes(); 324 } 325 } 326 327 rank++; 328 position++; 329 } 330 331 // Remove extra views. 332 boolean removed = false; 333 while (pageItr.hasNext()) { 334 removeView(pageItr.next()); 335 removed = true; 336 } 337 if (removed) { 338 setCurrentPage(0); 339 } 340 341 setEnableOverscroll(getPageCount() > 1); 342 343 // Update footer 344 mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); 345 // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text. 346 mFolder.mFolderName.setGravity(getPageCount() > 1 ? 347 (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); 348 } 349 getDesiredWidth()350 public int getDesiredWidth() { 351 return getPageCount() > 0 ? 352 (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0; 353 } 354 getDesiredHeight()355 public int getDesiredHeight() { 356 return getPageCount() > 0 ? 357 (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0; 358 } 359 360 /** 361 * @return the rank of the cell nearest to the provided pixel position. 362 */ findNearestArea(int pixelX, int pixelY)363 public int findNearestArea(int pixelX, int pixelY) { 364 int pageIndex = getNextPage(); 365 CellLayout page = getPageAt(pageIndex); 366 page.findNearestArea(pixelX, pixelY, 1, 1, sTmpArray); 367 if (mFolder.isLayoutRtl()) { 368 sTmpArray[0] = page.getCountX() - sTmpArray[0] - 1; 369 } 370 return Math.min(mAllocatedContentSize - 1, 371 pageIndex * mOrganizer.getMaxItemsPerPage() 372 + sTmpArray[1] * mGridCountX + sTmpArray[0]); 373 } 374 getFirstItem()375 public View getFirstItem() { 376 return getViewInCurrentPage(c -> 0); 377 } 378 getLastItem()379 public View getLastItem() { 380 return getViewInCurrentPage(c -> c.getChildCount() - 1); 381 } 382 getViewInCurrentPage(ToIntFunction<ShortcutAndWidgetContainer> rankProvider)383 private View getViewInCurrentPage(ToIntFunction<ShortcutAndWidgetContainer> rankProvider) { 384 if (getChildCount() < 1) { 385 return null; 386 } 387 ShortcutAndWidgetContainer container = getCurrentCellLayout().getShortcutsAndWidgets(); 388 int rank = rankProvider.applyAsInt(container); 389 if (mGridCountX > 0) { 390 return container.getChildAt(rank % mGridCountX, rank / mGridCountX); 391 } else { 392 return container.getChildAt(rank); 393 } 394 } 395 396 /** 397 * Iterates over all its items in a reading order. 398 * @return the view for which the operator returned true. 399 */ iterateOverItems(ItemOperator op)400 public View iterateOverItems(ItemOperator op) { 401 for (int k = 0 ; k < getChildCount(); k++) { 402 CellLayout page = getPageAt(k); 403 for (int j = 0; j < page.getCountY(); j++) { 404 for (int i = 0; i < page.getCountX(); i++) { 405 View v = page.getChildAt(i, j); 406 if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v)) { 407 return v; 408 } 409 } 410 } 411 } 412 return null; 413 } 414 getAccessibilityDescription()415 public String getAccessibilityDescription() { 416 return getContext().getString(R.string.folder_opened, mGridCountX, mGridCountY); 417 } 418 419 /** 420 * Sets the focus on the first visible child. 421 */ setFocusOnFirstChild()422 public void setFocusOnFirstChild() { 423 View firstChild = getCurrentCellLayout().getChildAt(0, 0); 424 if (firstChild != null) { 425 firstChild.requestFocus(); 426 } 427 } 428 429 @Override notifyPageSwitchListener(int prevPage)430 protected void notifyPageSwitchListener(int prevPage) { 431 super.notifyPageSwitchListener(prevPage); 432 if (mFolder != null) { 433 mFolder.updateTextViewFocus(); 434 } 435 } 436 437 /** 438 * Scrolls the current view by a fraction 439 */ showScrollHint(int direction)440 public void showScrollHint(int direction) { 441 float fraction = (direction == Folder.SCROLL_LEFT) ^ mIsRtl 442 ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION; 443 int hint = (int) (fraction * getWidth()); 444 int scroll = getScrollForPage(getNextPage()) + hint; 445 int delta = scroll - getScrollX(); 446 if (delta != 0) { 447 mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION); 448 invalidate(); 449 } 450 } 451 clearScrollHint()452 public void clearScrollHint() { 453 if (getScrollX() != getScrollForPage(getNextPage())) { 454 snapToPage(getNextPage()); 455 } 456 } 457 458 /** 459 * Finish animation all the views which are animating across pages 460 */ completePendingPageChanges()461 public void completePendingPageChanges() { 462 if (!mPendingAnimations.isEmpty()) { 463 ArrayMap<View, Runnable> pendingViews = new ArrayMap<>(mPendingAnimations); 464 for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) { 465 e.getKey().animate().cancel(); 466 e.getValue().run(); 467 } 468 } 469 } 470 rankOnCurrentPage(int rank)471 public boolean rankOnCurrentPage(int rank) { 472 int p = rank / mOrganizer.getMaxItemsPerPage(); 473 return p == getNextPage(); 474 } 475 476 @Override onPageBeginTransition()477 protected void onPageBeginTransition() { 478 super.onPageBeginTransition(); 479 // Ensure that adjacent pages have high resolution icons 480 verifyVisibleHighResIcons(getCurrentPage() - 1); 481 verifyVisibleHighResIcons(getCurrentPage() + 1); 482 } 483 484 /** 485 * Ensures that all the icons on the given page are of high-res 486 */ verifyVisibleHighResIcons(int pageNo)487 public void verifyVisibleHighResIcons(int pageNo) { 488 CellLayout page = getPageAt(pageNo); 489 if (page != null) { 490 ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); 491 for (int i = parent.getChildCount() - 1; i >= 0; i--) { 492 BubbleTextView icon = ((BubbleTextView) parent.getChildAt(i)); 493 icon.verifyHighRes(); 494 // Set the callback back to the actual icon, in case 495 // it was captured by the FolderIcon 496 Drawable d = icon.getIcon(); 497 if (d != null) { 498 d.setCallback(icon); 499 } 500 } 501 } 502 } 503 getAllocatedContentSize()504 public int getAllocatedContentSize() { 505 return mAllocatedContentSize; 506 } 507 508 /** 509 * Reorders the items such that the {@param empty} spot moves to {@param target} 510 */ realTimeReorder(int empty, int target)511 public void realTimeReorder(int empty, int target) { 512 if (!mViewsBound) { 513 return; 514 } 515 completePendingPageChanges(); 516 int delay = 0; 517 float delayAmount = START_VIEW_REORDER_DELAY; 518 519 // Animation only happens on the current page. 520 int pageToAnimate = getNextPage(); 521 int maxItemsPerPage = mOrganizer.getMaxItemsPerPage(); 522 523 int pageT = target / maxItemsPerPage; 524 int pagePosT = target % maxItemsPerPage; 525 526 if (pageT != pageToAnimate) { 527 Log.e(TAG, "Cannot animate when the target cell is invisible"); 528 } 529 int pagePosE = empty % maxItemsPerPage; 530 int pageE = empty / maxItemsPerPage; 531 532 int startPos, endPos; 533 int moveStart, moveEnd; 534 int direction; 535 536 if (target == empty) { 537 // No animation 538 return; 539 } else if (target > empty) { 540 // Items will move backwards to make room for the empty cell. 541 direction = 1; 542 543 // If empty cell is in a different page, move them instantly. 544 if (pageE < pageToAnimate) { 545 moveStart = empty; 546 // Instantly move the first item in the current page. 547 moveEnd = pageToAnimate * maxItemsPerPage; 548 // Animate the 2nd item in the current page, as the first item was already moved to 549 // the last page. 550 startPos = 0; 551 } else { 552 moveStart = moveEnd = -1; 553 startPos = pagePosE; 554 } 555 556 endPos = pagePosT; 557 } else { 558 // The items will move forward. 559 direction = -1; 560 561 if (pageE > pageToAnimate) { 562 // Move the items immediately. 563 moveStart = empty; 564 // Instantly move the last item in the current page. 565 moveEnd = (pageToAnimate + 1) * maxItemsPerPage - 1; 566 567 // Animations start with the second last item in the page 568 startPos = maxItemsPerPage - 1; 569 } else { 570 moveStart = moveEnd = -1; 571 startPos = pagePosE; 572 } 573 574 endPos = pagePosT; 575 } 576 577 // Instant moving views. 578 while (moveStart != moveEnd) { 579 int rankToMove = moveStart + direction; 580 int p = rankToMove / maxItemsPerPage; 581 int pagePos = rankToMove % maxItemsPerPage; 582 int x = pagePos % mGridCountX; 583 int y = pagePos / mGridCountX; 584 585 final CellLayout page = getPageAt(p); 586 final View v = page.getChildAt(x, y); 587 if (v != null) { 588 if (pageToAnimate != p) { 589 page.removeView(v); 590 addViewForRank(v, (WorkspaceItemInfo) v.getTag(), moveStart); 591 } else { 592 // Do a fake animation before removing it. 593 final int newRank = moveStart; 594 final float oldTranslateX = v.getTranslationX(); 595 596 Runnable endAction = new Runnable() { 597 598 @Override 599 public void run() { 600 mPendingAnimations.remove(v); 601 v.setTranslationX(oldTranslateX); 602 ((CellLayout) v.getParent().getParent()).removeView(v); 603 addViewForRank(v, (WorkspaceItemInfo) v.getTag(), newRank); 604 } 605 }; 606 v.animate() 607 .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth()) 608 .setDuration(REORDER_ANIMATION_DURATION) 609 .setStartDelay(0) 610 .withEndAction(endAction); 611 mPendingAnimations.put(v, endAction); 612 } 613 } 614 moveStart = rankToMove; 615 } 616 617 if ((endPos - startPos) * direction <= 0) { 618 // No animation 619 return; 620 } 621 622 CellLayout page = getPageAt(pageToAnimate); 623 for (int i = startPos; i != endPos; i += direction) { 624 int nextPos = i + direction; 625 View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX); 626 if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, 627 REORDER_ANIMATION_DURATION, delay, true, true)) { 628 delay += delayAmount; 629 delayAmount *= VIEW_REORDER_DELAY_FACTOR; 630 } 631 } 632 } 633 634 @Override canScroll(float absVScroll, float absHScroll)635 protected boolean canScroll(float absVScroll, float absHScroll) { 636 return AbstractFloatingView.getTopOpenViewWithType(mFolder.mActivityContext, 637 TYPE_ALL & ~TYPE_FOLDER) == null; 638 } 639 itemsPerPage()640 public int itemsPerPage() { 641 return mOrganizer.getMaxItemsPerPage(); 642 } 643 644 @Override setClipPath(Path clipPath)645 public void setClipPath(Path clipPath) { 646 mClipPath = clipPath; 647 invalidate(); 648 } 649 } 650