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