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