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