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