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