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