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