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