2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.gallery3d.ui;
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.os.Handler;
22 import android.view.GestureDetector;
23 import android.view.MotionEvent;
24 import android.view.animation.DecelerateInterpolator;
26 import com.android.gallery3d.anim.Animation;
27 import com.android.gallery3d.common.Utils;
28 import com.android.gallery3d.ui.PositionRepository.Position;
29 import com.android.gallery3d.util.LinkedNode;
31 import java.util.ArrayList;
32 import java.util.HashMap;
34 public class SlotView extends GLView {
35 @SuppressWarnings("unused")
36 private static final String TAG = "SlotView";
38 private static final boolean WIDE = true;
40 private static final int INDEX_NONE = -1;
42 public interface Listener {
43 public void onDown(int index);
45 public void onSingleTapUp(int index);
46 public void onLongTap(int index);
47 public void onScrollPositionChanged(int position, int total);
50 public static class SimpleListener implements Listener {
51 public void onDown(int index) {}
53 public void onSingleTapUp(int index) {}
54 public void onLongTap(int index) {}
55 public void onScrollPositionChanged(int position, int total) {}
58 private final GestureDetector mGestureDetector;
59 private final ScrollerHelper mScroller;
60 private final Paper mPaper = new Paper();
62 private Listener mListener;
63 private UserInteractionListener mUIListener;
65 // Use linked hash map to keep the rendering order
66 private final HashMap<DisplayItem, ItemEntry> mItems =
67 new HashMap<DisplayItem, ItemEntry>();
69 public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList();
71 // This is used for multipass rendering
72 private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>();
73 private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>();
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;
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;
87 public static final int OVERSCROLL_3D = 0;
88 public static final int OVERSCROLL_SYSTEM = 1;
89 public static final int OVERSCROLL_NONE = 2;
91 public SlotView(Context context) {
93 new GestureDetector(context, new MyGestureListener());
94 mScroller = new ScrollerHelper(context);
95 mHandler = new Handler(context.getMainLooper());
98 public void setCenterIndex(int index) {
99 int slotCount = mLayout.mSlotCount;
100 if (index < 0 || index >= slotCount) {
103 Rect rect = mLayout.getSlotRect(index);
105 ? (rect.left + rect.right - getWidth()) / 2
106 : (rect.top + rect.bottom - getHeight()) / 2;
107 setScrollPosition(position);
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;
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;
127 setScrollPosition(position);
130 public void setScrollPosition(int position) {
131 position = Utils.clamp(position, 0, mLayout.getScrollLimit());
132 mScroller.setPosition(position);
133 updateScrollPosition(position, false);
136 public void setSlotSpec(Spec spec) {
137 mLayout.setSlotSpec(spec);
141 public void addComponent(GLView view) {
142 throw new UnsupportedOperationException();
146 protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
147 if (!changeSize) return;
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.
153 (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
154 mLayout.setSize(r - l, b - t);
155 makeSlotVisible(visibleIndex);
157 onLayoutChanged(r - l, b - t);
158 if (mOverscrollEffect == OVERSCROLL_3D) {
159 mPaper.setSize(r - l, b - t);
163 protected void onLayoutChanged(int width, int height) {
166 public void startTransition(PositionProvider position) {
167 mPositions = position;
168 mAnimation = new MyAnimation();
170 if (mItems.size() != 0) invalidate();
173 public void savePositions(PositionRepository repository) {
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);
187 private void updateScrollPosition(int position, boolean force) {
188 if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
194 mLayout.setScrollPosition(position);
195 onScrollPositionChanged(position);
198 protected void onScrollPositionChanged(int newPosition) {
199 int limit = mLayout.getScrollLimit();
200 mListener.onScrollPositionChanged(newPosition, limit);
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);
210 public void removeDisplayItem(DisplayItem item) {
211 ItemEntry entry = mItems.remove(item);
212 if (entry != null) entry.remove();
215 public Rect getSlotRect(int slotIndex) {
216 return mLayout.getSlotRect(slotIndex);
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();
228 case MotionEvent.ACTION_UP:
236 public void setListener(Listener listener) {
237 mListener = listener;
240 public void setUserInteractionListener(UserInteractionListener listener) {
241 mUIListener = listener;
244 public void setOverscrollEffect(int kind) {
245 mOverscrollEffect = kind;
246 mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
250 protected void render(GLCanvas canvas) {
251 super.render(canvas);
253 long animTime = AnimationTime.get();
254 boolean more = mScroller.advanceAnimation(animTime);
256 updateScrollPosition(mScroller.getPosition(), false);
258 boolean paperActive = false;
259 if (mOverscrollEffect == OVERSCROLL_3D) {
260 // Check if an edge is reached and notify mPaper if so.
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;
267 // I don't know why, but getCurrVelocity() can return NaN.
268 if (!Float.isNaN(v)) {
269 mPaper.edgeReached(v);
272 paperActive = mPaper.advanceAnimation();
277 float interpolate = 1f;
278 if (mAnimation != null) {
279 more |= mAnimation.calculate(animTime);
280 interpolate = mAnimation.value;
284 canvas.translate(-mScrollX, 0);
286 canvas.translate(0, -mScrollY);
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);
295 more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
296 entry = list.previousOf(entry);
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);
307 more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0);
309 mCurrentItems.clear();
310 // swap mNextItems with mCurrentItems
311 ArrayList<ItemEntry> tmp = mNextItems;
312 mNextItems = mCurrentItems;
318 canvas.translate(mScrollX, 0);
320 canvas.translate(0, mScrollY);
323 if (more) invalidate();
325 final UserInteractionListener listener = mUIListener;
326 if (mMoreAnimation && !more && listener != null) {
327 mHandler.post(new Runnable() {
330 listener.onUserInteractionEnd();
334 mMoreAnimation = more;
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);
354 canvas.multiplyAlpha(position.alpha);
356 canvas.multiplyMatrix(mPaper.getTransform(
357 position, entry.base, mScrollX, mScrollY), 0);
359 canvas.translate(position.x, position.y, position.z);
361 if (position.theta != 0) {
362 canvas.rotate(position.theta, 0, 0, 1);
364 int more = entry.item.render(canvas, pass);
369 public static class MyAnimation extends Animation {
372 public MyAnimation() {
373 setInterpolator(new DecelerateInterpolator(4));
378 protected void onCalculate(float progress) {
383 private static class ItemEntry extends LinkedNode {
384 public DisplayItem item;
385 public Position target;
386 public Position base;
388 public ItemEntry(DisplayItem item, Position target, Position base) {
390 this.target = target;
395 // This Spec class is used to specify the size of each slot in the SlotView.
396 // There are two ways to do it:
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.
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;
410 public int rowsLand = -1;
411 public int rowsPort = -1;
412 public int slotGap = -1;
415 public static class Layout {
417 private int mVisibleStart;
418 private int mVisibleEnd;
420 private int mSlotCount;
421 private int mSlotWidth;
422 private int mSlotHeight;
423 private int mSlotGap;
430 private int mUnitCount;
431 private int mContentLength;
432 private int mScrollPosition;
434 private int mVerticalPadding;
435 private int mHorizontalPadding;
437 public void setSlotSpec(Spec spec) {
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;
449 public Rect getSlotRect(int index) {
452 col = index / mUnitCount;
453 row = index - col * mUnitCount;
455 row = index / mUnitCount;
456 col = index - row * mUnitCount;
459 int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap);
460 int y = mVerticalPadding + row * (mSlotHeight + mSlotGap);
461 return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
464 public int getSlotWidth() {
468 public int getSlotHeight() {
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
476 // (3) padding[]: the vertical and horizontal padding we need in order
477 // to put the slots towards to the center of the display.
479 // The "major" direction is the direction the user can scroll. The other
480 // direction is the "minor" direction.
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 */
488 int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
489 if (unitCount == 0) unitCount = 1;
490 mUnitCount = unitCount;
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;
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;
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);
507 private void initLayoutParameters() {
508 // Initialize mSlotWidth and mSlotHeight from mSpec
509 if (mSpec.slotWidth != -1) {
511 mSlotWidth = mSpec.slotWidth;
512 mSlotHeight = mSpec.slotHeight;
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;
520 int[] padding = new int[2];
522 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
523 mVerticalPadding = padding[0];
524 mHorizontalPadding = padding[1];
526 initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
527 mVerticalPadding = padding[1];
528 mHorizontalPadding = padding[0];
530 updateVisibleSlotRange();
533 public void setSize(int width, int height) {
536 initLayoutParameters();
539 private void updateVisibleSlotRange() {
540 int position = mScrollPosition;
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);
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);
559 public void setScrollPosition(int position) {
560 if (mScrollPosition == position) return;
561 mScrollPosition = position;
562 updateVisibleSlotRange();
565 private void setVisibleRange(int start, int end) {
566 if (start == mVisibleStart && end == mVisibleEnd) return;
568 mVisibleStart = start;
571 mVisibleStart = mVisibleEnd = 0;
575 public int getVisibleStart() {
576 return mVisibleStart;
579 public int getVisibleEnd() {
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);
587 absoluteX -= mHorizontalPadding;
588 absoluteY -= mVerticalPadding;
590 if (absoluteX < 0 || absoluteY < 0) {
594 int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
595 int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
597 if (!WIDE && columnIdx >= mUnitCount) {
601 if (WIDE && rowIdx >= mUnitCount) {
605 if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
609 if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
614 ? (columnIdx * mUnitCount + rowIdx)
615 : (rowIdx * mUnitCount + columnIdx);
617 return index >= mSlotCount ? INDEX_NONE : index;
620 public int getScrollLimit() {
621 int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
622 return limit <= 0 ? 0 : limit;
626 private class MyGestureListener implements
627 GestureDetector.OnGestureListener {
628 private boolean isDown;
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.
633 public void onShowPress(MotionEvent e) {
635 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
636 if (index != INDEX_NONE) {
638 mListener.onDown(index);
642 private void cancelDown() {
649 public boolean onDown(MotionEvent e) {
654 public boolean onFling(MotionEvent e1,
655 MotionEvent e2, float velocityX, float velocityY) {
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();
667 public boolean onScroll(MotionEvent e1,
668 MotionEvent e2, float distanceX, float distanceY) {
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);
681 public boolean onSingleTapUp(MotionEvent e) {
683 if (mDownInScrolling) return true;
684 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
685 if (index != INDEX_NONE) mListener.onSingleTapUp(index);
690 public void onLongPress(MotionEvent e) {
692 if (mDownInScrolling) return;
695 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
696 if (index != INDEX_NONE) mListener.onLongTap(index);
703 public void setStartIndex(int index) {
707 // Return true if the layout parameters have been changed
708 public boolean setSlotCount(int slotCount) {
709 boolean changed = mLayout.setSlotCount(slotCount);
711 // mStartIndex is applied the first time setSlotCount is called.
712 if (mStartIndex != INDEX_NONE) {
713 setCenterIndex(mStartIndex);
714 mStartIndex = INDEX_NONE;
716 // Reset the scroll position to avoid scrolling over the updated limit.
717 setScrollPosition(WIDE ? mScrollX : mScrollY);
721 public int getVisibleStart() {
722 return mLayout.getVisibleStart();
725 public int getVisibleEnd() {
726 return mLayout.getVisibleEnd();
729 public int getScrollX() {
733 public int getScrollY() {