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