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