1 /* 2 * Copyright (C) 2014 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.tv.settings.widget; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.view.View; 22 import android.view.animation.DecelerateInterpolator; 23 import android.view.animation.LinearInterpolator; 24 import android.widget.Scroller; 25 26 /** 27 * Maintains a Scroller object and two axis scrolling information 28 */ 29 public class ScrollController { 30 /** 31 * try to keep focused view kept in middle of viewport, focus move to the side of viewport when 32 * scroll to the beginning or end, this will make sure you won't see blank space in viewport 33 * {@link Axis.ItemWindow#setCount(int)} defines the size of window (how many items) we are 34 * trying to keep in the middle. <p> 35 * The middle point is calculated by "scrollCenterOffset" or "scrollCenterOffsetPercent"; 36 * if none of these two are defined, default value is 1/2 of the size. 37 * 38 * @see Axis#setScrollCenterStrategy(int) 39 * @see Axis#getSystemScrollPos(int) 40 */ 41 public final static int SCROLL_CENTER_IN_MIDDLE = 0; 42 43 /** 44 * focus view kept at a fixed location, might see blank space. The distance of fixed location 45 * to left/top is given by {@link Axis#setScrollCenterOffset(int)} 46 * 47 * @see Axis#setScrollCenterStrategy(int) 48 * @see Axis#getSystemScrollPos(int) 49 */ 50 public final static int SCROLL_CENTER_FIXED = 1; 51 52 /** 53 * focus view kept at a fixed percentage distance from the left/top of the view, 54 * might see blank space. The offset percent is set by 55 * {@link Axis#setScrollCenterOffsetPercent(int)}. A fixed offset from this 56 * position may also be set with {@link Axis#setScrollCenterOffset(int)}. 57 * 58 * @see Axis#setScrollCenterStrategy(int) 59 * @see Axis#getSystemScrollPos(int) 60 */ 61 public final static int SCROLL_CENTER_FIXED_PERCENT = 2; 62 63 /** 64 * focus view kept at a fixed location, might see blank space. The distance of fixed location 65 * to right/bottom is given by {@link Axis#setScrollCenterOffset(int)} 66 * 67 * @see Axis#setScrollCenterStrategy(int) 68 * @see Axis#getSystemScrollPos(int) 69 */ 70 public final static int SCROLL_CENTER_FIXED_TO_END = 3; 71 72 /** 73 * Align center of the item 74 */ 75 public final static int SCROLL_ITEM_ALIGN_CENTER = 0; 76 77 /** 78 * Align left/top of the item 79 */ 80 public final static int SCROLL_ITEM_ALIGN_LOW = 1; 81 82 /** 83 * Align right/bottom of the item 84 */ 85 public final static int SCROLL_ITEM_ALIGN_HIGH = 2; 86 87 /** operation not allowed */ 88 public final static int OPERATION_DISABLE = 0; 89 90 /** 91 * operation is using {@link Axis#mScrollMin} {@link Axis#mScrollMax}, see description in 92 * {@link Axis#mScrollCenter} 93 */ 94 public final static int OPERATION_NOTOUCH = 1; 95 96 /** 97 * operation is using {@link Axis#mTouchScrollMax} and {@link Axis#mTouchScrollMin}, see 98 * description in {@link Axis#mScrollCenter} 99 */ 100 public final static int OPERATION_TOUCH = 2; 101 102 /** 103 * maps to OPERATION_TOUCH for touchscreen, OPERATION_NORMAL for non-touchscreen 104 */ 105 public final static int OPERATION_AUTO = 3; 106 107 private static final int SCROLL_DURATION_MIN = 250; 108 private static final int SCROLL_DURATION_MAX = 1500; 109 private static final int SCROLL_DURATION_PAGE_MIN = 250; 110 // millisecond per pixel 111 private static final float SCROLL_DURATION_MS_PER_PIX = 0.25f; 112 113 /** 114 * Maintains scroll information in one direction 115 */ 116 public static class Axis { 117 private int mOperationMode = OPERATION_NOTOUCH; 118 /** 119 * In {@link ScrollController#OPERATION_TOUCH} mode:<br> 120 * {@link #mScrollCenter} changes from {@link #mTouchScrollMin} and 121 * {@link #mTouchScrollMax}; focus won't moved to two sides when scroll to edge of view 122 * port. 123 * <p> 124 * In {@link ScrollController#OPERATION_NOTOUCH} mode:<br> 125 * mScrollCenter changes from {@link #mScrollMin} and {@link #mScrollMax}. It is different 126 * than {@link View#getScrollX()} which starts from left edge of first child; mScrollCenter 127 * starts from center of first child, ends at center of last child; expanded views are 128 * excluded from calculating the mScrollCenter. We convert the mScrollCenter to system 129 * scroll position (see {@link ScrollAdapterView#adjustSystemScrollPos}), note it's not 130 * necessarily a linear transformation between system scrollX and mScrollCenter. <br> 131 * For {@link #SCROLL_CENTER_IN_MIDDLE}: <br> 132 * When mScrollCenter is close to {@link #mScrollMin}, {@link View#getScrollX()} will be 133 * fixed 0, but mScrollCenter is still decreasing, so we can move focus from the item which 134 * is at center of screen to the first child. <br> 135 * For {@link #SCROLL_CENTER_FIXED} and 136 * {@link #SCROLL_CENTER_FIXED_PERCENT}: It's a easy linear conversion 137 * applied 138 * <p> 139 * mScrollCenter is also used to calculate dynamic transformation based on how far a view 140 * is from the mScrollCenter. For example, the views with center close to mScrollCenter 141 * will be scaled up in {@link ScrollAdapterView#applyTransformations} 142 */ 143 private float mScrollCenter; 144 /** 145 * Maximum scroll value, initially unlimited until we will get the value when scroll to the 146 * last item of ListAdapter and set the value to center of last child 147 */ 148 private int mScrollMax; 149 /** 150 * scroll max for standard touch friendly operation, i.e. focus will not move to side when 151 * scroll to two edges. 152 */ 153 private int mTouchScrollMax; 154 /** right/bottom edge of last child */ 155 private int mMaxEdge; 156 /** left/top edge of first child, typically should be zero*/ 157 private int mMinEdge; 158 /** Minimum scroll value, point to center of first child, typically half of child size */ 159 private int mScrollMin; 160 /** 161 * scroll min for standard touch friendly operation, i.e. focus will not move to side when 162 * scroll to two edges. 163 */ 164 private int mTouchScrollMin; 165 166 private int mScrollItemAlign = SCROLL_ITEM_ALIGN_CENTER; 167 168 private boolean mSelectedTakesMoreSpace = false; 169 170 /** the offset set by a mouse dragging event */ 171 private float mDragOffset; 172 173 /** 174 * Total extra spaces. Divided into four parts:<p> 175 * 1. extraSpace before scrollPosition, given by {@link #mExtraSpaceLow} 176 * This value is animating from the extra space of "transition from" to the value 177 * of "transition to"<p> 178 * 2. extraSpace after scrollPosition<p> 179 * 3. size of expanded view of "transition from"<p> 180 * 4. size of expanded view of "transition to"<p> 181 * Among the four parts: 2,3,4 are after scroll position.<p> 182 * 3,4 are included in mExpandedSize when {@link #mSelectedTakesMoreSpace} is true<p> 183 * */ 184 private int mExpandedSize; 185 /** extra space used before the scroll position */ 186 private int mExtraSpaceLow; 187 private int mExtraSpaceHigh; 188 189 private int mAlignExtraOffset; 190 191 /** 192 * Describes how to put the mScrollCenter in the view port different types affects how to 193 * translate mScrollCenter to system scroll position, see details in getSystemScrollPos(). 194 */ 195 private int mScrollCenterStrategy; 196 197 /** 198 * used when {@link #mScrollCenterStrategy} is 199 * {@link #SCROLL_CENTER_FIXED}, {@link #SCROLL_CENTER_FIXED_PERCENT} or 200 * {@link #SCROLL_CENTER_FIXED_TO_END}, the offset for the fixed location of center 201 * scroll position relative to left/top, percentage or right/bottom 202 */ 203 private int mScrollCenterOffset = -1; 204 205 /** 206 * used when {@link #mScrollCenterStrategy} is 207 * {@link #SCROLL_CENTER_FIXED_PERCENT}. The ratio of the view's height 208 * at which to place the scroll center from the top. 209 */ 210 private float mScrollCenterOffsetPercent = -1; 211 212 /** represents position information of child views, see {@link ItemWindow} */ 213 public static class Item { 214 215 private int mIndex; 216 private int mLow; 217 private int mHigh; 218 private int mCenter; 219 Item()220 public Item() { 221 mIndex = -1; 222 } 223 getLow()224 final public int getLow() { 225 return mLow; 226 } 227 getHigh()228 final public int getHigh() { 229 return mHigh; 230 } 231 getCenter()232 final public int getCenter() { 233 return mCenter; 234 } 235 getIndex()236 final public int getIndex() { 237 return mIndex; 238 } 239 240 /** set low bound, high bound and index for the item */ setValue(int index, int low, int high)241 final public void setValue(int index, int low, int high) { 242 mIndex = index; 243 mLow = low; 244 mHigh = high; 245 mCenter = (low + high) / 2; 246 } 247 isValid()248 final public boolean isValid() { 249 return mIndex >= 0; 250 } 251 252 @Override toString()253 final public String toString() { 254 return mIndex + "[" + mLow + "," + mHigh + "]"; 255 } 256 } 257 258 private int mSize; 259 260 private int mPaddingLow; 261 262 private int mPaddingHigh; 263 264 private Lerper mLerper; 265 266 private String mName; // for debugging 267 Axis(Lerper lerper, String name)268 public Axis(Lerper lerper, String name) { 269 mScrollCenterStrategy = SCROLL_CENTER_IN_MIDDLE; 270 mLerper = lerper; 271 reset(); 272 mName = name; 273 } 274 getScrollCenterStrategy()275 final public int getScrollCenterStrategy() { 276 return mScrollCenterStrategy; 277 } 278 setScrollCenterStrategy(int scrollCenterStrategy)279 final public void setScrollCenterStrategy(int scrollCenterStrategy) { 280 mScrollCenterStrategy = scrollCenterStrategy; 281 } 282 getScrollCenterOffset()283 final public int getScrollCenterOffset() { 284 return mScrollCenterOffset; 285 } 286 setScrollCenterOffset(int scrollCenterOffset)287 final public void setScrollCenterOffset(int scrollCenterOffset) { 288 mScrollCenterOffset = scrollCenterOffset; 289 } 290 setScrollCenterOffsetPercent(int scrollCenterOffsetPercent)291 final public void setScrollCenterOffsetPercent(int scrollCenterOffsetPercent) { 292 if (scrollCenterOffsetPercent < 0) { 293 scrollCenterOffsetPercent = 0; 294 } else if (scrollCenterOffsetPercent > 100) { 295 scrollCenterOffsetPercent = 100; 296 } 297 mScrollCenterOffsetPercent = ( scrollCenterOffsetPercent / 100.0f); 298 } 299 setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace)300 final public void setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace) { 301 mSelectedTakesMoreSpace = selectedTakesMoreSpace; 302 } 303 getSelectedTakesMoreSpace()304 final public boolean getSelectedTakesMoreSpace() { 305 return mSelectedTakesMoreSpace; 306 } 307 setScrollItemAlign(int align)308 final public void setScrollItemAlign(int align) { 309 mScrollItemAlign = align; 310 } 311 getScrollItemAlign()312 final public int getScrollItemAlign() { 313 return mScrollItemAlign; 314 } 315 getScrollCenter()316 final public int getScrollCenter() { 317 return (int) mScrollCenter; 318 } 319 setOperationMode(int mode)320 final public void setOperationMode(int mode) { 321 mOperationMode = mode; 322 } 323 scrollMin()324 private int scrollMin() { 325 return mOperationMode == OPERATION_TOUCH ? mTouchScrollMin : mScrollMin; 326 } 327 scrollMax()328 private int scrollMax() { 329 return mOperationMode == OPERATION_TOUCH ? mTouchScrollMax : mScrollMax; 330 } 331 332 /** update scroll min and minEdge, Integer.MIN_VALUE means unknown*/ updateScrollMin(int scrollMin, int minEdge)333 final public void updateScrollMin(int scrollMin, int minEdge) { 334 mScrollMin = scrollMin; 335 if (mScrollCenter < mScrollMin) { 336 mScrollCenter = mScrollMin; 337 } 338 mMinEdge = minEdge; 339 if (mScrollCenterStrategy != SCROLL_CENTER_IN_MIDDLE 340 || mScrollMin == Integer.MIN_VALUE) { 341 mTouchScrollMin = mScrollMin; 342 } else { 343 mTouchScrollMin = Math.max(mScrollMin, mMinEdge + mSize / 2); 344 } 345 } 346 invalidateScrollMin()347 public void invalidateScrollMin() { 348 mScrollMin = Integer.MIN_VALUE; 349 mMinEdge = Integer.MIN_VALUE; 350 mTouchScrollMin = Integer.MIN_VALUE; 351 } 352 353 /** update scroll max and maxEdge, Integer.MAX_VALUE means unknown*/ updateScrollMax(int scrollMax, int maxEdge)354 final public void updateScrollMax(int scrollMax, int maxEdge) { 355 mScrollMax = scrollMax; 356 if (mScrollCenter > mScrollMax) { 357 mScrollCenter = mScrollMax; 358 } 359 mMaxEdge = maxEdge; 360 if (mScrollCenterStrategy != SCROLL_CENTER_IN_MIDDLE 361 || mScrollMax == Integer.MAX_VALUE) { 362 mTouchScrollMax = mScrollMax; 363 } else { 364 mTouchScrollMax = Math.min(mScrollMax, mMaxEdge - mSize / 2); 365 } 366 } 367 invalidateScrollMax()368 public void invalidateScrollMax() { 369 mScrollMax = Integer.MAX_VALUE; 370 mMaxEdge = Integer.MAX_VALUE; 371 mTouchScrollMax = Integer.MAX_VALUE; 372 } 373 canScroll(boolean forward)374 final public boolean canScroll(boolean forward) { 375 if (forward) { 376 if (mScrollCenter >= mScrollMax) { 377 return false; 378 } 379 } else { 380 if (mScrollCenter <= mScrollMin) { 381 return false; 382 } 383 } 384 return true; 385 } 386 updateScrollCenter(float scrollTarget, boolean lerper)387 private boolean updateScrollCenter(float scrollTarget, boolean lerper) { 388 mDragOffset = 0; 389 int scrollMin = scrollMin(); 390 int scrollMax = scrollMax(); 391 boolean overScroll = false; 392 if (scrollMin >= scrollMax) { 393 scrollTarget = mScrollCenter; 394 overScroll = true; 395 } else if (scrollTarget < scrollMin) { 396 scrollTarget = scrollMin; 397 overScroll = true; 398 } else if (scrollTarget > scrollMax) { 399 scrollTarget = scrollMax; 400 overScroll = true; 401 } 402 if (lerper) { 403 mScrollCenter = mLerper.getValue(mScrollCenter, scrollTarget); 404 } else { 405 mScrollCenter = scrollTarget; 406 } 407 return overScroll; 408 } 409 updateFromDrag()410 private void updateFromDrag() { 411 updateScrollCenter(mScrollCenter + mDragOffset, false); 412 } 413 dragBy(float distanceX)414 private void dragBy(float distanceX) { 415 mDragOffset += distanceX; 416 } 417 reset()418 private void reset() { 419 mScrollCenter = Integer.MIN_VALUE; 420 mScrollMin = Integer.MIN_VALUE; 421 mMinEdge = Integer.MIN_VALUE; 422 mTouchScrollMin = Integer.MIN_VALUE; 423 mScrollMax = Integer.MAX_VALUE; 424 mMaxEdge = Integer.MAX_VALUE; 425 mTouchScrollMax = Integer.MAX_VALUE; 426 mExpandedSize = 0; 427 mDragOffset = 0; 428 } 429 isMinUnknown()430 final public boolean isMinUnknown() { 431 return mScrollMin == Integer.MIN_VALUE; 432 } 433 isMaxUnknown()434 final public boolean isMaxUnknown() { 435 return mScrollMax == Integer.MAX_VALUE; 436 } 437 getSizeForExpandableItem()438 final public int getSizeForExpandableItem() { 439 return mSize - mPaddingLow - mPaddingHigh - mExpandedSize; 440 } 441 setSize(int size)442 final public void setSize(int size) { 443 mSize = size; 444 } 445 setExpandedSize(int expandedSize)446 final public void setExpandedSize(int expandedSize) { 447 mExpandedSize = expandedSize; 448 } 449 setExtraSpaceLow(int extraSpaceLow)450 final public void setExtraSpaceLow(int extraSpaceLow) { 451 mExtraSpaceLow = extraSpaceLow; 452 } 453 setExtraSpaceHigh(int extraSpaceHigh)454 final public void setExtraSpaceHigh(int extraSpaceHigh) { 455 mExtraSpaceHigh = extraSpaceHigh; 456 } 457 setAlignExtraOffset(int extraOffset)458 final public void setAlignExtraOffset(int extraOffset) { 459 mAlignExtraOffset = extraOffset; 460 } 461 setPadding(int paddingLow, int paddingHigh)462 final public void setPadding(int paddingLow, int paddingHigh) { 463 mPaddingLow = paddingLow; 464 mPaddingHigh = paddingHigh; 465 } 466 getPaddingLow()467 final public int getPaddingLow() { 468 return mPaddingLow; 469 } 470 getPaddingHigh()471 final public int getPaddingHigh() { 472 return mPaddingHigh; 473 } 474 getSystemScrollPos()475 final public int getSystemScrollPos() { 476 return getSystemScrollPos((int) mScrollCenter); 477 } 478 getSystemScrollPos(int scrollCenter)479 final public int getSystemScrollPos(int scrollCenter) { 480 scrollCenter += mAlignExtraOffset; 481 482 // For the "FIXED" strategy family: 483 int compensate = mSelectedTakesMoreSpace ? mExtraSpaceLow : -mExtraSpaceLow; 484 if (mScrollCenterStrategy == SCROLL_CENTER_FIXED) { 485 return scrollCenter - mScrollCenterOffset + compensate; 486 } else if (mScrollCenterStrategy == SCROLL_CENTER_FIXED_TO_END) { 487 return scrollCenter - (mSize - mScrollCenterOffset) + compensate; 488 } else if (mScrollCenterStrategy == SCROLL_CENTER_FIXED_PERCENT) { 489 return (int) (scrollCenter - mScrollCenterOffset - mSize 490 * mScrollCenterOffsetPercent) + compensate; 491 } 492 int clientSize = mSize - mPaddingLow - mPaddingHigh; 493 // For SCROLL_CENTER_IN_MIDDLE, first calculate the middle point: 494 // if the scrollCenterOffset or scrollCenterOffsetPercent is specified, 495 // use it for middle point, otherwise, use 1/2 of the size 496 int middlePosition; 497 if (mScrollCenterOffset >= 0) { 498 middlePosition = mScrollCenterOffset - mPaddingLow; 499 } else if (mScrollCenterOffsetPercent >= 0) { 500 middlePosition = (int) (mSize * mScrollCenterOffsetPercent) - mPaddingLow; 501 } else { 502 middlePosition = clientSize / 2; 503 } 504 int afterMiddlePosition = clientSize - middlePosition; 505 // Following code for mSelectedTakesMoreSpace = true/false is quite similar, 506 // but it's still more clear and easier to debug when separating them. 507 boolean isMinUnknown = isMinUnknown(); 508 boolean isMaxUnknown = isMaxUnknown(); 509 if (mSelectedTakesMoreSpace) { 510 int extraSpaceLow; 511 switch (getScrollItemAlign()) { 512 case SCROLL_ITEM_ALIGN_LOW: 513 extraSpaceLow = 0; 514 break; 515 case SCROLL_ITEM_ALIGN_HIGH: 516 extraSpaceLow = mExtraSpaceLow + mExtraSpaceHigh; 517 break; 518 case SCROLL_ITEM_ALIGN_CENTER: 519 default: 520 extraSpaceLow = mExtraSpaceLow; 521 break; 522 } 523 if (!isMinUnknown && !isMaxUnknown) { 524 if (mMaxEdge - mMinEdge + mExpandedSize <= clientSize) { 525 // total children size is less than view port: align the left edge 526 // of first child to view port's left edge 527 return mMinEdge - mPaddingLow; 528 } 529 } 530 if (!isMinUnknown) { 531 if (scrollCenter - mMinEdge + extraSpaceLow <= middlePosition) { 532 // scroll center is within half of view port size: align the left edge 533 // of first child to the left edge of view port 534 return mMinEdge - mPaddingLow; 535 } 536 } 537 if (!isMaxUnknown) { 538 int spaceAfterScrollCenter = mExpandedSize - extraSpaceLow; 539 if (mMaxEdge - scrollCenter + spaceAfterScrollCenter <= afterMiddlePosition) { 540 // scroll center is very close to the right edge of view port : align the 541 // right edge of last children (plus expanded size) to view port's right 542 return mMaxEdge -mPaddingLow - (clientSize - mExpandedSize ); 543 } 544 } 545 // else put scroll center in middle of view port 546 return scrollCenter - middlePosition - mPaddingLow + extraSpaceLow; 547 } else { 548 int shift; 549 switch (getScrollItemAlign()) { 550 case SCROLL_ITEM_ALIGN_LOW: 551 shift = - mExtraSpaceLow; 552 break; 553 case SCROLL_ITEM_ALIGN_HIGH: 554 shift = + mExtraSpaceHigh; 555 break; 556 case SCROLL_ITEM_ALIGN_CENTER: 557 default: 558 shift = 0; 559 break; 560 } 561 if (!isMinUnknown && !isMaxUnknown) { 562 if (mMaxEdge - mMinEdge + mExpandedSize <= clientSize) { 563 // total children size is less than view port: align the left edge 564 // of first child to view port's left edge 565 return mMinEdge - mPaddingLow; 566 } 567 } 568 if (!isMinUnknown) { 569 if (scrollCenter + shift - mMinEdge <= middlePosition) { 570 // scroll center is within half of view port size: align the left edge 571 // of first child to the left edge of view port 572 return mMinEdge - mPaddingLow; 573 } 574 } 575 if (!isMaxUnknown) { 576 if (mMaxEdge - scrollCenter - shift + mExpandedSize <= afterMiddlePosition) { 577 // scroll center is very close to the right edge of view port : align the 578 // right edge of last children (plus expanded size) to view port's right 579 return mMaxEdge -mPaddingLow - (clientSize - mExpandedSize ); 580 } 581 } 582 // else put scroll center in middle of view port 583 return scrollCenter - middlePosition - mPaddingLow + shift; 584 } 585 } 586 587 @Override toString()588 public String toString() { 589 return "center: " + mScrollCenter + " min:" + mMinEdge + "," + mScrollMin + 590 " max:" + mScrollMax + "," + mMaxEdge; 591 } 592 593 } 594 595 private Context mContext; 596 597 // we separate Scrollers for scroll animation and fling animation; this is because we want a 598 // flywheel feature for fling animation, ScrollAdapterView inserts scroll animation between 599 // fling animations, the fling animation will mistakenly continue the old velocity of scroll 600 // animation: that's wrong, we want fling animation pickup the old velocity of last fling. 601 private Scroller mScrollScroller; 602 private Scroller mFlingScroller; 603 604 private final static int STATE_NONE = 0; 605 606 /** using fling scroller */ 607 private final static int STATE_FLING = 1; 608 609 /** using scroll scroller */ 610 private final static int STATE_SCROLL = 2; 611 612 /** using drag */ 613 private final static int STATE_DRAG = 3; 614 615 private int mState = STATE_NONE; 616 617 private int mOrientation = ScrollAdapterView.HORIZONTAL; 618 619 private Lerper mLerper = new Lerper(); 620 621 final public Axis vertical = new Axis(mLerper, "vertical"); 622 623 final public Axis horizontal = new Axis(mLerper, "horizontal"); 624 625 private Axis mMainAxis = horizontal; 626 627 private Axis mSecondAxis = vertical; 628 629 /** fling operation mode */ 630 private int mFlingMode = OPERATION_AUTO; 631 632 /** drag operation mode */ 633 private int mDragMode = OPERATION_AUTO; 634 635 /** scroll operation mode (for DPAD) */ 636 private int mScrollMode = OPERATION_NOTOUCH; 637 638 /** the major movement is in horizontal or vertical */ 639 private boolean mMainHorizontal; 640 private boolean mHorizontalForward = true; 641 private boolean mVerticalForward = true; 642 lerper()643 final public Lerper lerper() { 644 return mLerper; 645 } 646 mainAxis()647 final public Axis mainAxis() { 648 return mMainAxis; 649 } 650 secondAxis()651 final public Axis secondAxis() { 652 return mSecondAxis; 653 } 654 setLerperDivisor(float divisor)655 final public void setLerperDivisor(float divisor) { 656 mLerper.setDivisor(divisor); 657 } 658 ScrollController(Context context)659 public ScrollController(Context context) { 660 mContext = context; 661 // Quint easeOut 662 mScrollScroller = new Scroller(mContext, new DecelerateInterpolator(2)); 663 mFlingScroller = new Scroller(mContext, new LinearInterpolator()); 664 } 665 setOrientation(int orientation)666 final public void setOrientation(int orientation) { 667 int align = mainAxis().getScrollItemAlign(); 668 boolean selectedTakesMoreSpace = mainAxis().getSelectedTakesMoreSpace(); 669 mOrientation = orientation; 670 if (mOrientation == ScrollAdapterView.HORIZONTAL) { 671 mMainAxis = horizontal; 672 mSecondAxis = vertical; 673 } else { 674 mMainAxis = vertical; 675 mSecondAxis = horizontal; 676 } 677 mMainAxis.setScrollItemAlign(align); 678 mSecondAxis.setScrollItemAlign(SCROLL_ITEM_ALIGN_CENTER); 679 mMainAxis.setSelectedTakesMoreSpace(selectedTakesMoreSpace); 680 mSecondAxis.setSelectedTakesMoreSpace(false); 681 } 682 setScrollItemAlign(int align)683 public void setScrollItemAlign(int align) { 684 mainAxis().setScrollItemAlign(align); 685 } 686 getScrollItemAlign()687 public int getScrollItemAlign() { 688 return mainAxis().getScrollItemAlign(); 689 } 690 getOrientation()691 final public int getOrientation() { 692 return mOrientation; 693 } 694 getFlingMode()695 final public int getFlingMode() { 696 return mFlingMode; 697 } 698 setFlingMode(int mode)699 final public void setFlingMode(int mode) { 700 this.mFlingMode = mode; 701 } 702 getDragMode()703 final public int getDragMode() { 704 return mDragMode; 705 } 706 setDragMode(int mode)707 final public void setDragMode(int mode) { 708 this.mDragMode = mode; 709 } 710 getScrollMode()711 final public int getScrollMode() { 712 return mScrollMode; 713 } 714 setScrollMode(int mode)715 final public void setScrollMode(int mode) { 716 this.mScrollMode = mode; 717 } 718 getCurrVelocity()719 final public float getCurrVelocity() { 720 if (mState == STATE_FLING) { 721 return mFlingScroller.getCurrVelocity(); 722 } else if (mState == STATE_SCROLL) { 723 return mScrollScroller.getCurrVelocity(); 724 } 725 return 0; 726 } 727 canScroll(int dx, int dy)728 final public boolean canScroll(int dx, int dy) { 729 if (dx == 0 && dy == 0) { 730 return false; 731 } 732 return (dx == 0 || horizontal.canScroll(dx < 0)) && 733 (dy == 0 || vertical.canScroll(dy < 0)); 734 } 735 getMode(int mode)736 private int getMode(int mode) { 737 if (mode == OPERATION_AUTO) { 738 if (mContext.getResources().getConfiguration().touchscreen 739 == Configuration.TOUCHSCREEN_NOTOUCH) { 740 mode = OPERATION_NOTOUCH; 741 } else { 742 mode = OPERATION_TOUCH; 743 } 744 } 745 return mode; 746 } 747 updateDirection(float dx, float dy)748 private void updateDirection(float dx, float dy) { 749 mMainHorizontal = Math.abs(dx) >= Math.abs(dy); 750 if (dx > 0) { 751 mHorizontalForward = true; 752 } else if (dx < 0) { 753 mHorizontalForward = false; 754 } 755 if (dy > 0) { 756 mVerticalForward = true; 757 } else if (dy < 0) { 758 mVerticalForward = false; 759 } 760 } 761 fling(int velocity_x, int velocity_y)762 final public boolean fling(int velocity_x, int velocity_y){ 763 if (mFlingMode == OPERATION_DISABLE) { 764 return false; 765 } 766 final int operationMode = getMode(mFlingMode); 767 horizontal.setOperationMode(operationMode); 768 vertical.setOperationMode(operationMode); 769 mState = STATE_FLING; 770 mFlingScroller.fling((int)(horizontal.mScrollCenter), 771 (int)(vertical.mScrollCenter), 772 velocity_x, 773 velocity_y, 774 Integer.MIN_VALUE, 775 Integer.MAX_VALUE, 776 Integer.MIN_VALUE, 777 Integer.MAX_VALUE); 778 updateDirection(velocity_x, velocity_y); 779 return true; 780 } 781 startScroll(int dx, int dy, boolean easeFling, int duration, boolean page)782 final public void startScroll(int dx, int dy, boolean easeFling, int duration, boolean page) { 783 if (mScrollMode == OPERATION_DISABLE) { 784 return; 785 } 786 final int operationMode = getMode(mScrollMode); 787 horizontal.setOperationMode(operationMode); 788 vertical.setOperationMode(operationMode); 789 Scroller scroller; 790 if (easeFling) { 791 mState = STATE_FLING; 792 scroller = mFlingScroller; 793 } else { 794 mState = STATE_SCROLL; 795 scroller = mScrollScroller; 796 } 797 int basex = horizontal.getScrollCenter(); 798 int basey = vertical.getScrollCenter(); 799 if (!scroller.isFinished()) { 800 // during scrolling, we should continue from getCurrX/getCurrY() (might be different 801 // than current Scroll Center due to Lerper) 802 dx = basex + dx - scroller.getCurrX(); 803 dy = basey + dy - scroller.getCurrY(); 804 basex = scroller.getCurrX(); 805 basey = scroller.getCurrY(); 806 } 807 updateDirection(dx, dy); 808 if (easeFling) { 809 float curDx = Math.abs(mFlingScroller.getFinalX() - mFlingScroller.getStartX()); 810 float curDy = Math.abs(mFlingScroller.getFinalY() - mFlingScroller.getStartY()); 811 float hyp = (float) Math.sqrt(curDx * curDx + curDy * curDy); 812 float velocity = mFlingScroller.getCurrVelocity(); 813 float velocityX = velocity * curDx / hyp; 814 float velocityY = velocity * curDy / hyp; 815 int durationX = velocityX ==0 ? 0 : (int)((Math.abs(dx) * 1000) / velocityX); 816 int durationY = velocityY ==0 ? 0 : (int)((Math.abs(dy) * 1000) / velocityY); 817 if (duration == 0) duration = Math.max(durationX, durationY); 818 } else { 819 if (duration == 0) { 820 duration = getScrollDuration((int) Math.sqrt(dx * dx + dy * dy), page); 821 } 822 } 823 scroller.startScroll(basex, basey, dx, dy, duration); 824 } 825 getCurrentAnimationDuration()826 final public int getCurrentAnimationDuration() { 827 Scroller scroller; 828 if (mState == STATE_FLING) { 829 scroller = mFlingScroller; 830 } else if (mState == STATE_SCROLL) { 831 scroller = mScrollScroller; 832 } else { 833 return 0; 834 } 835 return scroller.getDuration(); 836 } 837 startScrollByMain(int deltaMain, int deltaSecond, boolean easeFling, int duration, boolean page)838 final public void startScrollByMain(int deltaMain, int deltaSecond, boolean easeFling, 839 int duration, boolean page) { 840 int dx, dy; 841 if (mOrientation == ScrollAdapterView.HORIZONTAL) { 842 dx = deltaMain; 843 dy = deltaSecond; 844 } else { 845 dx = deltaSecond; 846 dy = deltaMain; 847 } 848 startScroll(dx, dy, easeFling, duration, page); 849 } 850 dragBy(float distanceX, float distanceY)851 final public boolean dragBy(float distanceX, float distanceY) { 852 if (mDragMode == OPERATION_DISABLE) { 853 return false; 854 } 855 final int operationMode = getMode(mDragMode); 856 horizontal.setOperationMode(operationMode); 857 vertical.setOperationMode(operationMode); 858 horizontal.dragBy(distanceX); 859 vertical.dragBy(distanceY); 860 mState = STATE_DRAG; 861 return true; 862 } 863 stopDrag()864 final public void stopDrag() { 865 mState = STATE_NONE; 866 vertical.mDragOffset = 0; 867 horizontal.mDragOffset = 0; 868 } 869 setScrollCenterByMain(int centerMain, int centerSecond)870 final public void setScrollCenterByMain(int centerMain, int centerSecond) { 871 if (mOrientation == ScrollAdapterView.HORIZONTAL) { 872 setScrollCenter(centerMain, centerSecond); 873 } else { 874 setScrollCenter(centerSecond, centerMain); 875 } 876 } 877 setScrollCenter(int centerX, int centerY)878 final public void setScrollCenter(int centerX, int centerY) { 879 horizontal.updateScrollCenter(centerX, false); 880 vertical.updateScrollCenter(centerY, false); 881 // centerX, centerY might be clipped by min/max 882 centerX = horizontal.getScrollCenter(); 883 centerY = vertical.getScrollCenter(); 884 mFlingScroller.setFinalX(centerX); 885 mFlingScroller.setFinalY(centerY); 886 mFlingScroller.abortAnimation(); 887 mScrollScroller.setFinalX(centerX); 888 mScrollScroller.setFinalY(centerY); 889 mScrollScroller.abortAnimation(); 890 } 891 getFinalX()892 final public int getFinalX() { 893 if (mState == STATE_FLING) { 894 return mFlingScroller.getFinalX(); 895 } else if (mState == STATE_SCROLL) { 896 return mScrollScroller.getFinalX(); 897 } 898 return horizontal.getScrollCenter(); 899 } 900 getFinalY()901 final public int getFinalY() { 902 if (mState == STATE_FLING) { 903 return mFlingScroller.getFinalY(); 904 } else if (mState == STATE_SCROLL) { 905 return mScrollScroller.getFinalY(); 906 } 907 return vertical.getScrollCenter(); 908 } 909 setFinalX(int finalX)910 final public void setFinalX(int finalX) { 911 if (mState == STATE_FLING) { 912 mFlingScroller.setFinalX(finalX); 913 } else if (mState == STATE_SCROLL) { 914 mScrollScroller.setFinalX(finalX); 915 } 916 } 917 setFinalY(int finalY)918 final public void setFinalY(int finalY) { 919 if (mState == STATE_FLING) { 920 mFlingScroller.setFinalY(finalY); 921 } else if (mState == STATE_SCROLL) { 922 mScrollScroller.setFinalY(finalY); 923 } 924 } 925 926 /** return true if scroll/fling animation or lerper is not stopped */ isFinished()927 final public boolean isFinished() { 928 Scroller scroller; 929 if (mState == STATE_FLING) { 930 scroller = mFlingScroller; 931 } else if (mState == STATE_SCROLL) { 932 scroller = mScrollScroller; 933 } else if (mState == STATE_DRAG){ 934 return false; 935 } else { 936 return true; 937 } 938 if (scroller.isFinished()) { 939 return (horizontal.getScrollCenter() == scroller.getCurrX() && 940 vertical.getScrollCenter() == scroller.getCurrY()); 941 } 942 return false; 943 } 944 isMainAxisMovingForward()945 final public boolean isMainAxisMovingForward() { 946 return mOrientation == ScrollAdapterView.HORIZONTAL ? 947 mHorizontalForward : mVerticalForward; 948 } 949 isSecondAxisMovingForward()950 final public boolean isSecondAxisMovingForward() { 951 return mOrientation == ScrollAdapterView.HORIZONTAL ? 952 mVerticalForward : mHorizontalForward; 953 } 954 getLastDirection()955 final public int getLastDirection() { 956 if (mMainHorizontal) { 957 return mHorizontalForward ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 958 } else { 959 return mVerticalForward ? View.FOCUS_DOWN : View.FOCUS_UP; 960 } 961 } 962 963 /** 964 * update scroller position, this is either trigger by fling()/startScroll() on the 965 * scroller object, or lerper, or can be caused by a dragBy() 966 */ computeAndSetScrollPosition()967 final public void computeAndSetScrollPosition() { 968 Scroller scroller; 969 if (mState == STATE_FLING) { 970 scroller = mFlingScroller; 971 } else if (mState == STATE_SCROLL) { 972 scroller = mScrollScroller; 973 } else if (mState == STATE_DRAG) { 974 if (horizontal.mDragOffset != 0 || vertical.mDragOffset !=0 ) { 975 horizontal.updateFromDrag(); 976 vertical.updateFromDrag(); 977 } 978 return; 979 } else { 980 return; 981 } 982 if (!isFinished()) { 983 scroller.computeScrollOffset(); 984 horizontal.updateScrollCenter(scroller.getCurrX(), true); 985 vertical.updateScrollCenter(scroller.getCurrY(), true); 986 } 987 } 988 989 /** get Scroll animation duration in ms for given pixels */ getScrollDuration(int distance, boolean isPage)990 final public int getScrollDuration(int distance, boolean isPage) { 991 int ms = (int)(distance * SCROLL_DURATION_MS_PER_PIX); 992 int minValue = isPage ? SCROLL_DURATION_PAGE_MIN : SCROLL_DURATION_MIN; 993 if (ms < minValue) { 994 ms = minValue; 995 } else if (ms > SCROLL_DURATION_MAX) { 996 ms = SCROLL_DURATION_MAX; 997 } 998 return ms; 999 } 1000 reset()1001 final public void reset() { 1002 mainAxis().reset(); 1003 } 1004 1005 @Override toString()1006 public String toString() { 1007 return new StringBuffer().append("horizontal=") 1008 .append(horizontal.toString()) 1009 .append("vertical=") 1010 .append(vertical.toString()) 1011 .toString(); 1012 } 1013 1014 } 1015