SlotView.java revision 7b83fb8e3a8978b33a6b9bfc56d85fe2c1a9cf06
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 android.content.Context; 20import android.graphics.Rect; 21import android.os.Handler; 22import android.view.GestureDetector; 23import android.view.MotionEvent; 24import android.view.animation.DecelerateInterpolator; 25 26import com.android.gallery3d.anim.Animation; 27import com.android.gallery3d.common.Utils; 28import com.android.gallery3d.ui.PositionRepository.Position; 29import com.android.gallery3d.util.LinkedNode; 30 31import java.util.ArrayList; 32import java.util.HashMap; 33 34public class SlotView extends GLView { 35 @SuppressWarnings("unused") 36 private static final String TAG = "SlotView"; 37 38 private static final boolean WIDE = true; 39 40 private static final int INDEX_NONE = -1; 41 42 public interface Listener { 43 public void onDown(int index); 44 public void onUp(); 45 public void onSingleTapUp(int index); 46 public void onLongTap(int index); 47 public void onScrollPositionChanged(int position, int total); 48 } 49 50 public static class SimpleListener implements Listener { 51 public void onDown(int index) {} 52 public void onUp() {} 53 public void onSingleTapUp(int index) {} 54 public void onLongTap(int index) {} 55 public void onScrollPositionChanged(int position, int total) {} 56 } 57 58 private final GestureDetector mGestureDetector; 59 private final ScrollerHelper mScroller; 60 private final Paper mPaper = new Paper(); 61 62 private Listener mListener; 63 private UserInteractionListener mUIListener; 64 65 // Use linked hash map to keep the rendering order 66 private final HashMap<DisplayItem, ItemEntry> mItems = 67 new HashMap<DisplayItem, ItemEntry>(); 68 69 public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList(); 70 71 // This is used for multipass rendering 72 private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>(); 73 private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>(); 74 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; 81 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; 86 87 public static final int OVERSCROLL_3D = 0; 88 public static final int OVERSCROLL_SYSTEM = 1; 89 public static final int OVERSCROLL_NONE = 2; 90 91 public SlotView(Context context) { 92 mGestureDetector = 93 new GestureDetector(context, new MyGestureListener()); 94 mScroller = new ScrollerHelper(context); 95 mHandler = new Handler(context.getMainLooper()); 96 } 97 98 public void setCenterIndex(int index) { 99 int slotCount = mLayout.mSlotCount; 100 if (index < 0 || index >= slotCount) { 101 return; 102 } 103 Rect rect = mLayout.getSlotRect(index); 104 int position = WIDE 105 ? (rect.left + rect.right - getWidth()) / 2 106 : (rect.top + rect.bottom - getHeight()) / 2; 107 setScrollPosition(position); 108 } 109 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; 117 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; 125 } 126 127 setScrollPosition(position); 128 } 129 130 public void setScrollPosition(int position) { 131 position = Utils.clamp(position, 0, mLayout.getScrollLimit()); 132 mScroller.setPosition(position); 133 updateScrollPosition(position, false); 134 } 135 136 public void setSlotSpec(Spec spec) { 137 mLayout.setSlotSpec(spec); 138 } 139 140 @Override 141 public void addComponent(GLView view) { 142 throw new UnsupportedOperationException(); 143 } 144 145 @Override 146 protected void onLayout(boolean changeSize, int l, int t, int r, int b) { 147 if (!changeSize) return; 148 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. 152 int visibleIndex = 153 (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2; 154 mLayout.setSize(r - l, b - t); 155 makeSlotVisible(visibleIndex); 156 157 onLayoutChanged(r - l, b - t); 158 if (mOverscrollEffect == OVERSCROLL_3D) { 159 mPaper.setSize(r - l, b - t); 160 } 161 } 162 163 protected void onLayoutChanged(int width, int height) { 164 } 165 166 public void startTransition(PositionProvider position) { 167 mPositions = position; 168 mAnimation = new MyAnimation(); 169 mAnimation.start(); 170 if (mItems.size() != 0) invalidate(); 171 } 172 173 public void savePositions(PositionRepository repository) { 174 repository.clear(); 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); 184 } 185 } 186 187 private void updateScrollPosition(int position, boolean force) { 188 if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; 189 if (WIDE) { 190 mScrollX = position; 191 } else { 192 mScrollY = position; 193 } 194 mLayout.setScrollPosition(position); 195 onScrollPositionChanged(position); 196 } 197 198 protected void onScrollPositionChanged(int newPosition) { 199 int limit = mLayout.getScrollLimit(); 200 mListener.onScrollPositionChanged(newPosition, limit); 201 } 202 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); 208 } 209 210 public void removeDisplayItem(DisplayItem item) { 211 ItemEntry entry = mItems.remove(item); 212 if (entry != null) entry.remove(); 213 } 214 215 public Rect getSlotRect(int slotIndex) { 216 return mLayout.getSlotRect(slotIndex); 217 } 218 219 @Override 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(); 227 break; 228 case MotionEvent.ACTION_UP: 229 mPaper.onRelease(); 230 invalidate(); 231 break; 232 } 233 return true; 234 } 235 236 public void setListener(Listener listener) { 237 mListener = listener; 238 } 239 240 public void setUserInteractionListener(UserInteractionListener listener) { 241 mUIListener = listener; 242 } 243 244 public void setOverscrollEffect(int kind) { 245 mOverscrollEffect = kind; 246 mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); 247 } 248 249 @Override 250 protected void render(GLCanvas canvas) { 251 super.render(canvas); 252 253 long animTime = AnimationTime.get(); 254 boolean more = mScroller.advanceAnimation(animTime); 255 int oldX = mScrollX; 256 updateScrollPosition(mScroller.getPosition(), false); 257 258 boolean paperActive = false; 259 if (mOverscrollEffect == OVERSCROLL_3D) { 260 // Check if an edge is reached and notify mPaper if so. 261 int newX = mScrollX; 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; 266 267 // I don't know why, but getCurrVelocity() can return NaN. 268 if (!Float.isNaN(v)) { 269 mPaper.edgeReached(v); 270 } 271 } 272 paperActive = mPaper.advanceAnimation(); 273 } 274 275 more |= paperActive; 276 277 float interpolate = 1f; 278 if (mAnimation != null) { 279 more |= mAnimation.calculate(animTime); 280 interpolate = mAnimation.value; 281 } 282 283 if (WIDE) { 284 canvas.translate(-mScrollX, 0); 285 } else { 286 canvas.translate(0, -mScrollY); 287 } 288 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); 294 } 295 more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0); 296 entry = list.previousOf(entry); 297 } 298 299 int pass = 1; 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); 306 } 307 more |= ((r & DisplayItem.RENDER_MORE_FRAME) != 0); 308 } 309 mCurrentItems.clear(); 310 // swap mNextItems with mCurrentItems 311 ArrayList<ItemEntry> tmp = mNextItems; 312 mNextItems = mCurrentItems; 313 mCurrentItems = tmp; 314 pass += 1; 315 } 316 317 if (WIDE) { 318 canvas.translate(mScrollX, 0); 319 } else { 320 canvas.translate(0, mScrollY); 321 } 322 323 if (more) invalidate(); 324 325 final UserInteractionListener listener = mUIListener; 326 if (mMoreAnimation && !more && listener != null) { 327 mHandler.post(new Runnable() { 328 @Override 329 public void run() { 330 listener.onUserInteractionEnd(); 331 } 332 }); 333 } 334 mMoreAnimation = more; 335 } 336 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); 353 } 354 canvas.multiplyAlpha(position.alpha); 355 if (paperActive) { 356 canvas.multiplyMatrix(mPaper.getTransform( 357 position, entry.base, mScrollX, mScrollY), 0); 358 } else { 359 canvas.translate(position.x, position.y, position.z); 360 } 361 if (position.theta != 0) { 362 canvas.rotate(position.theta, 0, 0, 1); 363 } 364 int more = entry.item.render(canvas, pass); 365 canvas.restore(); 366 return more; 367 } 368 369 public static class MyAnimation extends Animation { 370 public float value; 371 372 public MyAnimation() { 373 setInterpolator(new DecelerateInterpolator(4)); 374 setDuration(1500); 375 } 376 377 @Override 378 protected void onCalculate(float progress) { 379 value = progress; 380 } 381 } 382 383 private static class ItemEntry extends LinkedNode { 384 public DisplayItem item; 385 public Position target; 386 public Position base; 387 388 public ItemEntry(DisplayItem item, Position target, Position base) { 389 this.item = item; 390 this.target = target; 391 this.base = base; 392 } 393 } 394 395 // This Spec class is used to specify the size of each slot in the SlotView. 396 // There are two ways to do it: 397 // 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. 404 // 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; 409 410 public int rowsLand = -1; 411 public int rowsPort = -1; 412 public int slotGap = -1; 413 } 414 415 public static class Layout { 416 417 private int mVisibleStart; 418 private int mVisibleEnd; 419 420 private int mSlotCount; 421 private int mSlotWidth; 422 private int mSlotHeight; 423 private int mSlotGap; 424 425 private Spec mSpec; 426 427 private int mWidth; 428 private int mHeight; 429 430 private int mUnitCount; 431 private int mContentLength; 432 private int mScrollPosition; 433 434 private int mVerticalPadding; 435 private int mHorizontalPadding; 436 437 public void setSlotSpec(Spec spec) { 438 mSpec = spec; 439 } 440 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; 447 } 448 449 public Rect getSlotRect(int index) { 450 int col, row; 451 if (WIDE) { 452 col = index / mUnitCount; 453 row = index - col * mUnitCount; 454 } else { 455 row = index / mUnitCount; 456 col = index - row * mUnitCount; 457 } 458 459 int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap); 460 int y = mVerticalPadding + row * (mSlotHeight + mSlotGap); 461 return new Rect(x, y, x + mSlotWidth, y + mSlotHeight); 462 } 463 464 public int getSlotWidth() { 465 return mSlotWidth; 466 } 467 468 public int getSlotHeight() { 469 return mSlotHeight; 470 } 471 472 // Calculate 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 475 // columns (rows). 476 // (3) padding[]: the vertical and horizontal padding we need in order 477 // to put the slots towards to the center of the display. 478 // 479 // The "major" direction is the direction the user can scroll. The other 480 // direction is the "minor" direction. 481 // 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 */ 487 int[] padding) { 488 int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap); 489 if (unitCount == 0) unitCount = 1; 490 mUnitCount = unitCount; 491 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; 497 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; 501 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); 505 } 506 507 private void initLayoutParameters() { 508 // Initialize mSlotWidth and mSlotHeight from mSpec 509 if (mSpec.slotWidth != -1) { 510 mSlotGap = 0; 511 mSlotWidth = mSpec.slotWidth; 512 mSlotHeight = mSpec.slotHeight; 513 } else { 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; 518 } 519 520 int[] padding = new int[2]; 521 if (WIDE) { 522 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); 523 mVerticalPadding = padding[0]; 524 mHorizontalPadding = padding[1]; 525 } else { 526 initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); 527 mVerticalPadding = padding[1]; 528 mHorizontalPadding = padding[0]; 529 } 530 updateVisibleSlotRange(); 531 } 532 533 public void setSize(int width, int height) { 534 mWidth = width; 535 mHeight = height; 536 initLayoutParameters(); 537 } 538 539 private void updateVisibleSlotRange() { 540 int position = mScrollPosition; 541 542 if (WIDE) { 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); 549 } else { 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); 556 } 557 } 558 559 public void setScrollPosition(int position) { 560 if (mScrollPosition == position) return; 561 mScrollPosition = position; 562 updateVisibleSlotRange(); 563 } 564 565 private void setVisibleRange(int start, int end) { 566 if (start == mVisibleStart && end == mVisibleEnd) return; 567 if (start < end) { 568 mVisibleStart = start; 569 mVisibleEnd = end; 570 } else { 571 mVisibleStart = mVisibleEnd = 0; 572 } 573 } 574 575 public int getVisibleStart() { 576 return mVisibleStart; 577 } 578 579 public int getVisibleEnd() { 580 return mVisibleEnd; 581 } 582 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); 586 587 absoluteX -= mHorizontalPadding; 588 absoluteY -= mVerticalPadding; 589 590 if (absoluteX < 0 || absoluteY < 0) { 591 return INDEX_NONE; 592 } 593 594 int columnIdx = absoluteX / (mSlotWidth + mSlotGap); 595 int rowIdx = absoluteY / (mSlotHeight + mSlotGap); 596 597 if (!WIDE && columnIdx >= mUnitCount) { 598 return INDEX_NONE; 599 } 600 601 if (WIDE && rowIdx >= mUnitCount) { 602 return INDEX_NONE; 603 } 604 605 if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) { 606 return INDEX_NONE; 607 } 608 609 if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) { 610 return INDEX_NONE; 611 } 612 613 int index = WIDE 614 ? (columnIdx * mUnitCount + rowIdx) 615 : (rowIdx * mUnitCount + columnIdx); 616 617 return index >= mSlotCount ? INDEX_NONE : index; 618 } 619 620 public int getScrollLimit() { 621 int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; 622 return limit <= 0 ? 0 : limit; 623 } 624 } 625 626 private class MyGestureListener implements 627 GestureDetector.OnGestureListener { 628 private boolean isDown; 629 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. 632 @Override 633 public void onShowPress(MotionEvent e) { 634 if (isDown) return; 635 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); 636 if (index != INDEX_NONE) { 637 isDown = true; 638 mListener.onDown(index); 639 } 640 } 641 642 private void cancelDown() { 643 if (!isDown) return; 644 isDown = false; 645 mListener.onUp(); 646 } 647 648 @Override 649 public boolean onDown(MotionEvent e) { 650 return false; 651 } 652 653 @Override 654 public boolean onFling(MotionEvent e1, 655 MotionEvent e2, float velocityX, float velocityY) { 656 cancelDown(); 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(); 662 invalidate(); 663 return true; 664 } 665 666 @Override 667 public boolean onScroll(MotionEvent e1, 668 MotionEvent e2, float distanceX, float distanceY) { 669 cancelDown(); 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); 675 } 676 invalidate(); 677 return true; 678 } 679 680 @Override 681 public boolean onSingleTapUp(MotionEvent e) { 682 cancelDown(); 683 if (mDownInScrolling) return true; 684 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); 685 if (index != INDEX_NONE) mListener.onSingleTapUp(index); 686 return true; 687 } 688 689 @Override 690 public void onLongPress(MotionEvent e) { 691 cancelDown(); 692 if (mDownInScrolling) return; 693 lockRendering(); 694 try { 695 int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); 696 if (index != INDEX_NONE) mListener.onLongTap(index); 697 } finally { 698 unlockRendering(); 699 } 700 } 701 } 702 703 public void setStartIndex(int index) { 704 mStartIndex = index; 705 } 706 707 // Return true if the layout parameters have been changed 708 public boolean setSlotCount(int slotCount) { 709 boolean changed = mLayout.setSlotCount(slotCount); 710 711 // mStartIndex is applied the first time setSlotCount is called. 712 if (mStartIndex != INDEX_NONE) { 713 setCenterIndex(mStartIndex); 714 mStartIndex = INDEX_NONE; 715 } 716 // Reset the scroll position to avoid scrolling over the updated limit. 717 setScrollPosition(WIDE ? mScrollX : mScrollY); 718 return changed; 719 } 720 721 public int getVisibleStart() { 722 return mLayout.getVisibleStart(); 723 } 724 725 public int getVisibleEnd() { 726 return mLayout.getVisibleEnd(); 727 } 728 729 public int getScrollX() { 730 return mScrollX; 731 } 732 733 public int getScrollY() { 734 return mScrollY; 735 } 736} 737