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} &amp; {@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