SlotView.java revision 7d19f7f4281f232b9dceee4a5df390c03e2bd16b
1/*
2 * Copyright (C) 2010 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.gallery3d.ui;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.os.Handler;
22import android.view.GestureDetector;
23import android.view.MotionEvent;
24import android.view.animation.DecelerateInterpolator;
25
26import com.android.gallery3d.anim.Animation;
27import com.android.gallery3d.common.Utils;
28import com.android.gallery3d.ui.PositionRepository.Position;
29import com.android.gallery3d.util.LinkedNode;
30
31import java.util.ArrayList;
32import java.util.HashMap;
33
34public class SlotView extends GLView {
35    @SuppressWarnings("unused")
36    private static final String TAG = "SlotView";
37
38    private static final boolean WIDE = true;
39
40    private static final int INDEX_NONE = -1;
41
42    public interface Listener {
43        public void onDown(int index);
44        public void onUp();
45        public void onSingleTapUp(int index);
46        public void onLongTap(int index);
47        public void onScrollPositionChanged(int position, int total);
48    }
49
50    public static class SimpleListener implements Listener {
51        public void onDown(int index) {}
52        public void onUp() {}
53        public void onSingleTapUp(int index) {}
54        public void onLongTap(int index) {}
55        public void onScrollPositionChanged(int position, int total) {}
56    }
57
58    private final GestureDetector mGestureDetector;
59    private final ScrollerHelper mScroller;
60    private final Paper mPaper = new Paper();
61
62    private Listener mListener;
63    private UserInteractionListener mUIListener;
64
65    // Use linked hash map to keep the rendering order
66    private final HashMap<DisplayItem, ItemEntry> mItems =
67            new HashMap<DisplayItem, ItemEntry>();
68
69    public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList();
70
71    // This is used for multipass rendering
72    private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>();
73    private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>();
74
75    private boolean mMoreAnimation = false;
76    private MyAnimation mAnimation = null;
77    private final Position mTempPosition = new Position();
78    private final Layout mLayout = new Layout();
79    private PositionProvider mPositions;
80    private int mStartIndex = INDEX_NONE;
81
82    // whether the down action happened while the view is scrolling.
83    private boolean mDownInScrolling;
84    private int mOverscrollEffect = OVERSCROLL_3D;
85    private final Handler mHandler;
86
87    public static final int OVERSCROLL_3D = 0;
88    public static final int OVERSCROLL_SYSTEM = 1;
89    public static final int OVERSCROLL_NONE = 2;
90
91    public SlotView(Context context) {
92        mGestureDetector =
93                new GestureDetector(context, new MyGestureListener());
94        mScroller = new ScrollerHelper(context);
95        mHandler = new Handler(context.getMainLooper());
96    }
97
98    public void setCenterIndex(int index) {
99        int slotCount = mLayout.mSlotCount;
100        if (index < 0 || index >= slotCount) {
101            return;
102        }
103        Rect rect = mLayout.getSlotRect(index);
104        int position = WIDE
105                ? (rect.left + rect.right - getWidth()) / 2
106                : (rect.top + rect.bottom - getHeight()) / 2;
107        setScrollPosition(position);
108    }
109
110    public void makeSlotVisible(int index) {
111        Rect rect = mLayout.getSlotRect(index);
112        int visibleBegin = WIDE ? mScrollX : mScrollY;
113        int visibleLength = WIDE ? getWidth() : getHeight();
114        int visibleEnd = visibleBegin + visibleLength;
115        int slotBegin = WIDE ? rect.left : rect.top;
116        int slotEnd = WIDE ? rect.right : rect.bottom;
117
118        int position = visibleBegin;
119        if (visibleLength < slotEnd - slotBegin) {
120            position = visibleBegin;
121        } else if (slotBegin < visibleBegin) {
122            position = slotBegin;
123        } else if (slotEnd > visibleEnd) {
124            position = slotEnd - visibleLength;
125        }
126
127        setScrollPosition(position);
128    }
129
130    public void setScrollPosition(int position) {
131        position = Utils.clamp(position, 0, mLayout.getScrollLimit());
132        mScroller.setPosition(position);
133        updateScrollPosition(position, false);
134    }
135
136    public void setSlotSpec(Spec spec) {
137        mLayout.setSlotSpec(spec);
138    }
139
140    @Override
141    public void addComponent(GLView view) {
142        throw new UnsupportedOperationException();
143    }
144
145    @Override
146    public boolean removeComponent(GLView view) {
147        throw new UnsupportedOperationException();
148    }
149
150    @Override
151    protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
152        if (!changeSize) return;
153
154        // Make sure we are still at a resonable scroll position after the size
155        // is changed (like orientation change). We choose to keep the center
156        // visible slot still visible. This is arbitrary but reasonable.
157        int visibleIndex =
158                (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
159        mLayout.setSize(r - l, b - t);
160        makeSlotVisible(visibleIndex);
161
162        onLayoutChanged(r - l, b - t);
163        if (mOverscrollEffect == OVERSCROLL_3D) {
164            mPaper.setSize(r - l, b - t);
165        }
166    }
167
168    protected void onLayoutChanged(int width, int height) {
169    }
170
171    public void startTransition(PositionProvider position) {
172        mPositions = position;
173        mAnimation = new MyAnimation();
174        mAnimation.start();
175        if (mItems.size() != 0) invalidate();
176    }
177
178    public void savePositions(PositionRepository repository) {
179        repository.clear();
180        LinkedNode.List<ItemEntry> list = mItemList;
181        ItemEntry entry = list.getFirst();
182        Position position = new Position();
183        while (entry != null) {
184            position.set(entry.target);
185            position.x -= mScrollX;
186            position.y -= mScrollY;
187            repository.putPosition(entry.item.getIdentity(), position);
188            entry = list.nextOf(entry);
189        }
190    }
191
192    private void updateScrollPosition(int position, boolean force) {
193        if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
194        if (WIDE) {
195            mScrollX = position;
196        } else {
197            mScrollY = position;
198        }
199        mLayout.setScrollPosition(position);
200        onScrollPositionChanged(position);
201    }
202
203    protected void onScrollPositionChanged(int newPosition) {
204        int limit = mLayout.getScrollLimit();
205        mListener.onScrollPositionChanged(newPosition, limit);
206    }
207
208    public void putDisplayItem(Position target, Position base, DisplayItem item) {
209        item.setBox(mLayout.getSlotWidth(), mLayout.getSlotHeight());
210        ItemEntry entry = new ItemEntry(item, target, base);
211        mItemList.insertLast(entry);
212        mItems.put(item, entry);
213    }
214
215    public void removeDisplayItem(DisplayItem item) {
216        ItemEntry entry = mItems.remove(item);
217        if (entry != null) entry.remove();
218    }
219
220    public Rect getSlotRect(int slotIndex) {
221        return mLayout.getSlotRect(slotIndex);
222    }
223
224    @Override
225    protected boolean onTouch(MotionEvent event) {
226        if (mUIListener != null) mUIListener.onUserInteraction();
227        mGestureDetector.onTouchEvent(event);
228        switch (event.getAction()) {
229            case MotionEvent.ACTION_DOWN:
230                mDownInScrolling = !mScroller.isFinished();
231                mScroller.forceFinished();
232                break;
233            case MotionEvent.ACTION_UP:
234                mPaper.onRelease();
235                invalidate();
236                break;
237        }
238        return true;
239    }
240
241    public void setListener(Listener listener) {
242        mListener = listener;
243    }
244
245    public void setUserInteractionListener(UserInteractionListener listener) {
246        mUIListener = listener;
247    }
248
249    public void setOverscrollEffect(int kind) {
250        mOverscrollEffect = kind;
251        mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
252    }
253
254    @Override
255    protected void render(GLCanvas canvas) {
256        super.render(canvas);
257
258        long animTime = AnimationTime.get();
259        boolean more = mScroller.advanceAnimation(animTime);
260        int oldX = mScrollX;
261        updateScrollPosition(mScroller.getPosition(), false);
262
263        boolean paperActive = false;
264        if (mOverscrollEffect == OVERSCROLL_3D) {
265            // Check if an edge is reached and notify mPaper if so.
266            int newX = mScrollX;
267            int limit = mLayout.getScrollLimit();
268            if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
269                float v = mScroller.getCurrVelocity();
270                if (newX == limit) v = -v;
271
272                // I don't know why, but getCurrVelocity() can return NaN.
273                if (!Float.isNaN(v)) {
274                    mPaper.edgeReached(v);
275                }
276            }
277            paperActive = mPaper.advanceAnimation();
278        }
279
280        more |= paperActive;
281
282        float interpolate = 1f;
283        if (mAnimation != null) {
284            more |= mAnimation.calculate(animTime);
285            interpolate = mAnimation.value;
286        }
287
288        if (WIDE) {
289            canvas.translate(-mScrollX, 0);
290        } else {
291            canvas.translate(0, -mScrollY);
292        }
293
294        LinkedNode.List<ItemEntry> list = mItemList;
295        for (ItemEntry entry = list.getLast(); entry != null;) {
296            int r = renderItem(canvas, entry, interpolate, 0, paperActive);
297            if ((r & DisplayItem.RENDER_MORE_PASS) != 0) {
298                mCurrentItems.add(entry);
299            }
300            more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
301            entry = list.previousOf(entry);
302        }
303
304        int pass = 1;
305        while (!mCurrentItems.isEmpty()) {
306            for (int i = 0, n = mCurrentItems.size(); i < n; i++) {
307                ItemEntry entry = mCurrentItems.get(i);
308                int r = renderItem(canvas, entry, interpolate, pass, paperActive);
309                if ((r & DisplayItem.RENDER_MORE_PASS) != 0) {
310                    mNextItems.add(entry);
311                }
312                more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
313            }
314            mCurrentItems.clear();
315            // swap mNextItems with mCurrentItems
316            ArrayList<ItemEntry> tmp = mNextItems;
317            mNextItems = mCurrentItems;
318            mCurrentItems = tmp;
319            pass += 1;
320        }
321
322        if (WIDE) {
323            canvas.translate(mScrollX, 0);
324        } else {
325            canvas.translate(0, mScrollY);
326        }
327
328        if (more) invalidate();
329
330        final UserInteractionListener listener = mUIListener;
331        if (mMoreAnimation && !more && listener != null) {
332            mHandler.post(new Runnable() {
333                @Override
334                public void run() {
335                    listener.onUserInteractionEnd();
336                }
337            });
338        }
339        mMoreAnimation = more;
340    }
341
342    private int renderItem(GLCanvas canvas, ItemEntry entry,
343            float interpolate, int pass, boolean paperActive) {
344        canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
345        Position position = entry.target;
346        if (mPositions != null) {
347            position = mTempPosition;
348            position.set(entry.target);
349            position.x -= mScrollX;
350            position.y -= mScrollY;
351            Position source = mPositions
352                    .getPosition(entry.item.getIdentity(), position);
353            source.x += mScrollX;
354            source.y += mScrollY;
355            position = mTempPosition;
356            Position.interpolate(
357                    source, entry.target, position, interpolate);
358        }
359        canvas.multiplyAlpha(position.alpha);
360        if (paperActive) {
361            canvas.multiplyMatrix(mPaper.getTransform(
362                    position, entry.base, mScrollX, mScrollY), 0);
363        } else {
364            canvas.translate(position.x, position.y, position.z);
365        }
366        if (position.theta != 0) {
367            canvas.rotate(position.theta, 0, 0, 1);
368        }
369        int more = entry.item.render(canvas, pass);
370        canvas.restore();
371        return more;
372    }
373
374    public static class MyAnimation extends Animation {
375        public float value;
376
377        public MyAnimation() {
378            setInterpolator(new DecelerateInterpolator(4));
379            setDuration(1500);
380        }
381
382        @Override
383        protected void onCalculate(float progress) {
384            value = progress;
385        }
386    }
387
388    private static class ItemEntry extends LinkedNode {
389        public DisplayItem item;
390        public Position target;
391        public Position base;
392
393        public ItemEntry(DisplayItem item, Position target, Position base) {
394            this.item = item;
395            this.target = target;
396            this.base = base;
397        }
398    }
399
400    // This Spec class is used to specify the size of each slot in the SlotView.
401    // There are two ways to do it:
402    //
403    // (1) Specify slotWidth and slotHeight: they specify the width and height
404    //     of each slot. The number of rows and the gap between slots will be
405    //     determined automatically.
406    // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
407    //     of rows in landscape/portrait mode and the gap between slots. The
408    //     width and height of each slot is determined automatically.
409    //
410    // The initial value of -1 means they are not specified.
411    public static class Spec {
412        public int slotWidth = -1;
413        public int slotHeight = -1;
414
415        public int rowsLand = -1;
416        public int rowsPort = -1;
417        public int slotGap = -1;
418
419        static Spec newWithSize(int width, int height) {
420            Spec s = new Spec();
421            s.slotWidth = width;
422            s.slotHeight = height;
423            return s;
424        }
425
426        static Spec newWithRows(int rowsLand, int rowsPort, int slotGap) {
427            Spec s = new Spec();
428            s.rowsLand = rowsLand;
429            s.rowsPort = rowsPort;
430            s.slotGap = slotGap;
431            return s;
432        }
433    }
434
435    public static class Layout {
436
437        private int mVisibleStart;
438        private int mVisibleEnd;
439
440        private int mSlotCount;
441        private int mSlotWidth;
442        private int mSlotHeight;
443        private int mSlotGap;
444
445        private Spec mSpec;
446
447        private int mWidth;
448        private int mHeight;
449
450        private int mUnitCount;
451        private int mContentLength;
452        private int mScrollPosition;
453
454        private int mVerticalPadding;
455        private int mHorizontalPadding;
456
457        public void setSlotSpec(Spec spec) {
458            mSpec = spec;
459        }
460
461        public boolean setSlotCount(int slotCount) {
462            mSlotCount = slotCount;
463            int hPadding = mHorizontalPadding;
464            int vPadding = mVerticalPadding;
465            initLayoutParameters();
466            return vPadding != mVerticalPadding || hPadding != mHorizontalPadding;
467        }
468
469        public Rect getSlotRect(int index) {
470            int col, row;
471            if (WIDE) {
472                col = index / mUnitCount;
473                row = index - col * mUnitCount;
474            } else {
475                row = index / mUnitCount;
476                col = index - row * mUnitCount;
477            }
478
479            int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap);
480            int y = mVerticalPadding + row * (mSlotHeight + mSlotGap);
481            return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
482        }
483
484        public int getSlotWidth() {
485            return mSlotWidth;
486        }
487
488        public int getSlotHeight() {
489            return mSlotHeight;
490        }
491
492        public int getContentLength() {
493            return mContentLength;
494        }
495
496        // Calculate
497        // (1) mUnitCount: the number of slots we can fit into one column (or row).
498        // (2) mContentLength: the width (or height) we need to display all the
499        //     columns (rows).
500        // (3) padding[]: the vertical and horizontal padding we need in order
501        //     to put the slots towards to the center of the display.
502        //
503        // The "major" direction is the direction the user can scroll. The other
504        // direction is the "minor" direction.
505        //
506        // The comments inside this method are the description when the major
507        // directon is horizontal (X), and the minor directon is vertical (Y).
508        private void initLayoutParameters(
509                int majorLength, int minorLength,  /* The view width and height */
510                int majorUnitSize, int minorUnitSize,  /* The slot width and height */
511                int[] padding) {
512            int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
513            if (unitCount == 0) unitCount = 1;
514            mUnitCount = unitCount;
515
516            // We put extra padding above and below the column.
517            int availableUnits = Math.min(mUnitCount, mSlotCount);
518            int usedMinorLength = availableUnits * minorUnitSize +
519                    (availableUnits - 1) * mSlotGap;
520            padding[0] = (minorLength - usedMinorLength) / 2;
521
522            // Then calculate how many columns we need for all slots.
523            int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
524            mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
525
526            // If the content length is less then the screen width, put
527            // extra padding in left and right.
528            padding[1] = Math.max(0, (majorLength - mContentLength) / 2);
529        }
530
531        private void initLayoutParameters() {
532            // Initialize mSlotWidth and mSlotHeight from mSpec
533            if (mSpec.slotWidth != -1) {
534                mSlotGap = 0;
535                mSlotWidth = mSpec.slotWidth;
536                mSlotHeight = mSpec.slotHeight;
537            } else {
538                int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
539                mSlotGap = mSpec.slotGap;
540                mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
541                mSlotWidth = mSlotHeight;
542            }
543
544            int[] padding = new int[2];
545            if (WIDE) {
546                initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
547                mVerticalPadding = padding[0];
548                mHorizontalPadding = padding[1];
549            } else {
550                initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
551                mVerticalPadding = padding[1];
552                mHorizontalPadding = padding[0];
553            }
554            updateVisibleSlotRange();
555        }
556
557        public void setSize(int width, int height) {
558            mWidth = width;
559            mHeight = height;
560            initLayoutParameters();
561        }
562
563        private void updateVisibleSlotRange() {
564            int position = mScrollPosition;
565
566            if (WIDE) {
567                int startCol = position / (mSlotWidth + mSlotGap);
568                int start = Math.max(0, mUnitCount * startCol);
569                int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
570                        (mSlotWidth + mSlotGap);
571                int end = Math.min(mSlotCount, mUnitCount * endCol);
572                setVisibleRange(start, end);
573            } else {
574                int startRow = position / (mSlotHeight + mSlotGap);
575                int start = Math.max(0, mUnitCount * startRow);
576                int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
577                        (mSlotHeight + mSlotGap);
578                int end = Math.min(mSlotCount, mUnitCount * endRow);
579                setVisibleRange(start, end);
580            }
581        }
582
583        public void setScrollPosition(int position) {
584            if (mScrollPosition == position) return;
585            mScrollPosition = position;
586            updateVisibleSlotRange();
587        }
588
589        private void setVisibleRange(int start, int end) {
590            if (start == mVisibleStart && end == mVisibleEnd) return;
591            if (start < end) {
592                mVisibleStart = start;
593                mVisibleEnd = end;
594            } else {
595                mVisibleStart = mVisibleEnd = 0;
596            }
597        }
598
599        public int getVisibleStart() {
600            return mVisibleStart;
601        }
602
603        public int getVisibleEnd() {
604            return mVisibleEnd;
605        }
606
607        public int getSlotIndexByPosition(float x, float y) {
608            int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
609            int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
610
611            absoluteX -= mHorizontalPadding;
612            absoluteY -= mVerticalPadding;
613
614            if (absoluteX < 0 || absoluteY < 0) {
615                return INDEX_NONE;
616            }
617
618            int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
619            int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
620
621            if (!WIDE && columnIdx >= mUnitCount) {
622                return INDEX_NONE;
623            }
624
625            if (WIDE && rowIdx >= mUnitCount) {
626                return INDEX_NONE;
627            }
628
629            if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
630                return INDEX_NONE;
631            }
632
633            if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
634                return INDEX_NONE;
635            }
636
637            int index = WIDE
638                    ? (columnIdx * mUnitCount + rowIdx)
639                    : (rowIdx * mUnitCount + columnIdx);
640
641            return index >= mSlotCount ? INDEX_NONE : index;
642        }
643
644        public int getScrollLimit() {
645            int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
646            return limit <= 0 ? 0 : limit;
647        }
648    }
649
650    private class MyGestureListener implements
651            GestureDetector.OnGestureListener {
652        private boolean isDown;
653
654        // We call the listener's onDown() when our onShowPress() is called and
655        // call the listener's onUp() when we receive any further event.
656        @Override
657        public void onShowPress(MotionEvent e) {
658            if (isDown) return;
659            int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
660            if (index != INDEX_NONE) {
661                isDown = true;
662                mListener.onDown(index);
663            }
664        }
665
666        private void cancelDown() {
667            if (!isDown) return;
668            isDown = false;
669            mListener.onUp();
670        }
671
672        @Override
673        public boolean onDown(MotionEvent e) {
674            return false;
675        }
676
677        @Override
678        public boolean onFling(MotionEvent e1,
679                MotionEvent e2, float velocityX, float velocityY) {
680            cancelDown();
681            int scrollLimit = mLayout.getScrollLimit();
682            if (scrollLimit == 0) return false;
683            float velocity = WIDE ? velocityX : velocityY;
684            mScroller.fling((int) -velocity, 0, scrollLimit);
685            if (mUIListener != null) mUIListener.onUserInteractionBegin();
686            invalidate();
687            return true;
688        }
689
690        @Override
691        public boolean onScroll(MotionEvent e1,
692                MotionEvent e2, float distanceX, float distanceY) {
693            cancelDown();
694            float distance = WIDE ? distanceX : distanceY;
695            int overDistance = mScroller.startScroll(
696                    Math.round(distance), 0, mLayout.getScrollLimit());
697            if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
698                mPaper.overScroll(overDistance);
699            }
700            invalidate();
701            return true;
702        }
703
704        @Override
705        public boolean onSingleTapUp(MotionEvent e) {
706            cancelDown();
707            if (mDownInScrolling) return true;
708            int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
709            if (index != INDEX_NONE) mListener.onSingleTapUp(index);
710            return true;
711        }
712
713        @Override
714        public void onLongPress(MotionEvent e) {
715            cancelDown();
716            if (mDownInScrolling) return;
717            lockRendering();
718            try {
719                int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
720                if (index != INDEX_NONE) mListener.onLongTap(index);
721            } finally {
722                unlockRendering();
723            }
724        }
725    }
726
727    public void setStartIndex(int index) {
728        mStartIndex = index;
729    }
730
731    // Return true if the layout parameters have been changed
732    public boolean setSlotCount(int slotCount) {
733        boolean changed = mLayout.setSlotCount(slotCount);
734
735        // mStartIndex is applied the first time setSlotCount is called.
736        if (mStartIndex != INDEX_NONE) {
737            setCenterIndex(mStartIndex);
738            mStartIndex = INDEX_NONE;
739        }
740        // Reset the scroll position to avoid scrolling over the updated limit.
741        setScrollPosition(WIDE ? mScrollX : mScrollY);
742        return changed;
743    }
744
745    public int getVisibleStart() {
746        return mLayout.getVisibleStart();
747    }
748
749    public int getVisibleEnd() {
750        return mLayout.getVisibleEnd();
751    }
752
753    public int getScrollX() {
754        return mScrollX;
755    }
756
757    public int getScrollY() {
758        return mScrollY;
759    }
760}
761