├── MoviesDemoProject
├── MoviesDemo
│ ├── .gitignore
│ ├── libs
│ │ └── joda-time-2.3.jar
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── bg_tabs.9.png
│ │ │ │ ├── star_empty.png
│ │ │ │ ├── star_full.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── poster_frozen.jpg
│ │ │ │ ├── poster_thor.jpg
│ │ │ │ ├── bg_actionbar.9.png
│ │ │ │ ├── poster_gravity.jpg
│ │ │ │ ├── tab_selected.9.png
│ │ │ │ ├── ic_action_search.png
│ │ │ │ ├── poster_about_time.jpg
│ │ │ │ ├── poster_bad_grandpa.jpg
│ │ │ │ ├── poster_enders_game.jpg
│ │ │ │ ├── poster_enough_said.jpg
│ │ │ │ ├── poster_prisoners.jpg
│ │ │ │ ├── tab_unselected.9.png
│ │ │ │ ├── card_overlay_left.9.png
│ │ │ │ ├── card_overlay_right.9.png
│ │ │ │ ├── poster_runner_runner.jpg
│ │ │ │ ├── poster_the_counselor.jpg
│ │ │ │ ├── card_overlay_middle.9.png
│ │ │ │ ├── poster_the_fifth_estate.jpg
│ │ │ │ ├── tab_selected_pressed.9.png
│ │ │ │ ├── tab_unselected_pressed.9.png
│ │ │ │ └── poster_cloudy_with_a_chance_of_meatballs_2.jpg
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── layout
│ │ │ │ ├── include_header_footer_space.xml
│ │ │ │ ├── fragment_space.xml
│ │ │ │ ├── activity_movies.xml
│ │ │ │ ├── row_movie_pair.xml
│ │ │ │ └── include_movie_row.xml
│ │ │ ├── values-sw600dp
│ │ │ │ └── dimens.xml
│ │ │ ├── values-sw720dp-land
│ │ │ │ └── dimens.xml
│ │ │ ├── values-v11
│ │ │ │ └── styles.xml
│ │ │ ├── drawable
│ │ │ │ ├── ratingbar_movie.xml
│ │ │ │ ├── bg_film_rating.xml
│ │ │ │ └── tab_indicator.xml
│ │ │ ├── values-v14
│ │ │ │ └── styles.xml
│ │ │ └── menu
│ │ │ │ └── movies.xml
│ │ │ ├── assets
│ │ │ ├── fonts
│ │ │ │ └── RobotoCondensed-Bold.ttf
│ │ │ └── data.json
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── idunnolol
│ │ │ │ └── moviesdemo
│ │ │ │ ├── util
│ │ │ │ ├── FontCache.java
│ │ │ │ ├── BitmapCache.java
│ │ │ │ └── ResourceUtils.java
│ │ │ │ ├── view
│ │ │ │ ├── DecorFrameLayout.java
│ │ │ │ ├── CenteringRelativeLayout.java
│ │ │ │ ├── SlidingListView.java
│ │ │ │ ├── SlidingPairView.java
│ │ │ │ ├── SlidingRevealViewGroup.java
│ │ │ │ ├── MovieRowView.java
│ │ │ │ └── ViewPager.java
│ │ │ │ ├── ui
│ │ │ │ ├── AboutDialogFragment.java
│ │ │ │ ├── MoviesApplication.java
│ │ │ │ └── MoviesActivity.java
│ │ │ │ ├── data
│ │ │ │ └── Movie.java
│ │ │ │ └── widget
│ │ │ │ └── MovieAdapter.java
│ │ │ └── AndroidManifest.xml
│ └── build.gradle
├── settings.gradle
├── .gitignore
├── build.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew.bat
└── gradlew
├── .gitignore
├── README.md
└── LICENSE
/MoviesDemoProject/MoviesDemo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/MoviesDemoProject/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':MoviesDemo'
2 |
--------------------------------------------------------------------------------
/MoviesDemoProject/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/MoviesDemoProject/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/libs/joda-time-2.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/libs/joda-time-2.3.jar
--------------------------------------------------------------------------------
/MoviesDemoProject/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_tabs.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_tabs.9.png
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_empty.png
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_full.png
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 | As it is a sample, it's got a lot of hardcoded data and what have you. It also only works on phones in portrait. Sorry!
--------------------------------------------------------------------------------
/MoviesDemoProject/MoviesDemo/src/main/res/menu/movies.xml:
--------------------------------------------------------------------------------
1 |
As property animation is only supported as of Android 3.0 and forward, 301 | * setting a PageTransformer on a ViewPager on earlier platform versions will 302 | * be ignored.
303 | */ 304 | public interface PageTransformer { 305 | /** 306 | * Apply a property transformation to the given page. 307 | * 308 | * @param page Apply the transformation to this page 309 | * @param position Position of page relative to the current front-and-center 310 | * position of the pager. 0 is front and center. 1 is one full 311 | * page position to the right, and -1 is one page position to the left. 312 | */ 313 | public void transformPage(View page, float position); 314 | } 315 | 316 | /** 317 | * Used internally to monitor when adapters are switched. 318 | */ 319 | interface OnAdapterChangeListener { 320 | public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 321 | } 322 | 323 | /** 324 | * Used internally to tag special types of child views that should be added as 325 | * pager decorations by default. 326 | */ 327 | interface Decor {} 328 | 329 | public ViewPager(Context context) { 330 | super(context); 331 | initViewPager(); 332 | } 333 | 334 | public ViewPager(Context context, AttributeSet attrs) { 335 | super(context, attrs); 336 | initViewPager(); 337 | } 338 | 339 | void initViewPager() { 340 | setWillNotDraw(false); 341 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 342 | setFocusable(true); 343 | final Context context = getContext(); 344 | mScroller = new Scroller(context, sInterpolator); 345 | final ViewConfiguration configuration = ViewConfiguration.get(context); 346 | final float density = context.getResources().getDisplayMetrics().density; 347 | 348 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 349 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 350 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 351 | mLeftEdge = new EdgeEffectCompat(context); 352 | mRightEdge = new EdgeEffectCompat(context); 353 | 354 | mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 355 | mCloseEnough = (int) (CLOSE_ENOUGH * density); 356 | mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); 357 | 358 | ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); 359 | 360 | if (ViewCompat.getImportantForAccessibility(this) 361 | == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 362 | ViewCompat.setImportantForAccessibility(this, 363 | ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 364 | } 365 | } 366 | 367 | @Override 368 | protected void onDetachedFromWindow() { 369 | removeCallbacks(mEndScrollRunnable); 370 | super.onDetachedFromWindow(); 371 | } 372 | 373 | private void setScrollState(int newState) { 374 | if (mScrollState == newState) { 375 | return; 376 | } 377 | 378 | mScrollState = newState; 379 | if (mPageTransformer != null) { 380 | // PageTransformers can do complex things that benefit from hardware layers. 381 | enableLayers(newState != SCROLL_STATE_IDLE); 382 | } 383 | if (mOnPageChangeListener != null) { 384 | mOnPageChangeListener.onPageScrollStateChanged(newState); 385 | } 386 | } 387 | 388 | /** 389 | * Set a PagerAdapter that will supply views for this pager as needed. 390 | * 391 | * @param adapter Adapter to use 392 | */ 393 | public void setAdapter(PagerAdapter adapter) { 394 | if (mAdapter != null) { 395 | mAdapter.unregisterDataSetObserver(mObserver); 396 | mAdapter.startUpdate(this); 397 | for (int i = 0; i < mItems.size(); i++) { 398 | final ItemInfo ii = mItems.get(i); 399 | mAdapter.destroyItem(this, ii.position, ii.object); 400 | } 401 | mAdapter.finishUpdate(this); 402 | mItems.clear(); 403 | removeNonDecorViews(); 404 | mCurItem = 0; 405 | scrollTo(0, 0); 406 | } 407 | 408 | final PagerAdapter oldAdapter = mAdapter; 409 | mAdapter = adapter; 410 | mExpectedAdapterCount = 0; 411 | 412 | if (mAdapter != null) { 413 | if (mObserver == null) { 414 | mObserver = new PagerObserver(); 415 | } 416 | mAdapter.registerDataSetObserver(mObserver); 417 | mPopulatePending = false; 418 | final boolean wasFirstLayout = mFirstLayout; 419 | mFirstLayout = true; 420 | mExpectedAdapterCount = mAdapter.getCount(); 421 | if (mRestoredCurItem >= 0) { 422 | mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 423 | setCurrentItemInternal(mRestoredCurItem, false, true); 424 | mRestoredCurItem = -1; 425 | mRestoredAdapterState = null; 426 | mRestoredClassLoader = null; 427 | } else if (!wasFirstLayout) { 428 | populate(); 429 | } else { 430 | requestLayout(); 431 | } 432 | } 433 | 434 | if (mAdapterChangeListener != null && oldAdapter != adapter) { 435 | mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 436 | } 437 | } 438 | 439 | private void removeNonDecorViews() { 440 | for (int i = 0; i < getChildCount(); i++) { 441 | final View child = getChildAt(i); 442 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 443 | if (!lp.isDecor) { 444 | removeViewAt(i); 445 | i--; 446 | } 447 | } 448 | } 449 | 450 | /** 451 | * Retrieve the current adapter supplying pages. 452 | * 453 | * @return The currently registered PagerAdapter 454 | */ 455 | public PagerAdapter getAdapter() { 456 | return mAdapter; 457 | } 458 | 459 | void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 460 | mAdapterChangeListener = listener; 461 | } 462 | 463 | private int getClientWidth() { 464 | return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 465 | } 466 | 467 | /** 468 | * Set the currently selected page. If the ViewPager has already been through its first 469 | * layout with its current adapter there will be a smooth animated transition between 470 | * the current item and the specified item. 471 | * 472 | * @param item Item index to select 473 | */ 474 | public void setCurrentItem(int item) { 475 | mPopulatePending = false; 476 | setCurrentItemInternal(item, !mFirstLayout, false); 477 | } 478 | 479 | /** 480 | * Set the currently selected page. 481 | * 482 | * @param item Item index to select 483 | * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 484 | */ 485 | public void setCurrentItem(int item, boolean smoothScroll) { 486 | mPopulatePending = false; 487 | setCurrentItemInternal(item, smoothScroll, false); 488 | } 489 | 490 | public int getCurrentItem() { 491 | return mCurItem; 492 | } 493 | 494 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 495 | setCurrentItemInternal(item, smoothScroll, always, 0); 496 | } 497 | 498 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 499 | if (mAdapter == null || mAdapter.getCount() <= 0) { 500 | setScrollingCacheEnabled(false); 501 | return; 502 | } 503 | if (!always && mCurItem == item && mItems.size() != 0) { 504 | setScrollingCacheEnabled(false); 505 | return; 506 | } 507 | 508 | if (item < 0) { 509 | item = 0; 510 | } else if (item >= mAdapter.getCount()) { 511 | item = mAdapter.getCount() - 1; 512 | } 513 | final int pageLimit = mOffscreenPageLimit; 514 | if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 515 | // We are doing a jump by more than one page. To avoid 516 | // glitches, we want to keep all current pages in the view 517 | // until the scroll ends. 518 | for (int i=0; iThis is offered as an optimization. If you know in advance the number 662 | * of pages you will need to support or have lazy-loading mechanisms in place 663 | * on your pages, tweaking this setting can have benefits in perceived smoothness 664 | * of paging animations and interaction. If you have a small number of pages (3-4) 665 | * that you can keep active all at once, less time will be spent in layout for 666 | * newly created view subtrees as the user pages back and forth.
667 | * 668 | *You should keep this limit low, especially if your pages have complex layouts. 669 | * This setting defaults to 1.
670 | * 671 | * @param limit How many pages will be kept offscreen in an idle state. 672 | */ 673 | public void setOffscreenPageLimit(int limit) { 674 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 675 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 676 | DEFAULT_OFFSCREEN_PAGES); 677 | limit = DEFAULT_OFFSCREEN_PAGES; 678 | } 679 | if (limit != mOffscreenPageLimit) { 680 | mOffscreenPageLimit = limit; 681 | populate(); 682 | } 683 | } 684 | 685 | /** 686 | * Set the margin between pages. 687 | * 688 | * @param marginPixels Distance between adjacent pages in pixels 689 | * @see #getPageMargin() 690 | * @see #setPageMarginDrawable(android.graphics.drawable.Drawable) 691 | * @see #setPageMarginDrawable(int) 692 | */ 693 | public void setPageMargin(int marginPixels) { 694 | final int oldMargin = mPageMargin; 695 | mPageMargin = marginPixels; 696 | 697 | final int width = getWidth(); 698 | recomputeScrollPosition(width, width, marginPixels, oldMargin); 699 | 700 | requestLayout(); 701 | } 702 | 703 | /** 704 | * Return the margin between pages. 705 | * 706 | * @return The size of the margin in pixels 707 | */ 708 | public int getPageMargin() { 709 | return mPageMargin; 710 | } 711 | 712 | /** 713 | * Set a drawable that will be used to fill the margin between pages. 714 | * 715 | * @param d Drawable to display between pages 716 | */ 717 | public void setPageMarginDrawable(Drawable d) { 718 | mMarginDrawable = d; 719 | if (d != null) refreshDrawableState(); 720 | setWillNotDraw(d == null); 721 | invalidate(); 722 | } 723 | 724 | /** 725 | * Set a drawable that will be used to fill the margin between pages. 726 | * 727 | * @param resId Resource ID of a drawable to display between pages 728 | */ 729 | public void setPageMarginDrawable(int resId) { 730 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 731 | } 732 | 733 | @Override 734 | protected boolean verifyDrawable(Drawable who) { 735 | return super.verifyDrawable(who) || who == mMarginDrawable; 736 | } 737 | 738 | @Override 739 | protected void drawableStateChanged() { 740 | super.drawableStateChanged(); 741 | final Drawable d = mMarginDrawable; 742 | if (d != null && d.isStateful()) { 743 | d.setState(getDrawableState()); 744 | } 745 | } 746 | 747 | // We want the duration of the page snap animation to be influenced by the distance that 748 | // the screen has to travel, however, we don't want this duration to be effected in a 749 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 750 | // of travel has on the overall snap duration. 751 | float distanceInfluenceForSnapDuration(float f) { 752 | f -= 0.5f; // center the values about 0. 753 | f *= 0.3f * Math.PI / 2.0f; 754 | return (float) Math.sin(f); 755 | } 756 | 757 | /** 758 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 759 | * 760 | * @param x the number of pixels to scroll by on the X axis 761 | * @param y the number of pixels to scroll by on the Y axis 762 | */ 763 | void smoothScrollTo(int x, int y) { 764 | smoothScrollTo(x, y, 0); 765 | } 766 | 767 | /** 768 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 769 | * 770 | * @param x the number of pixels to scroll by on the X axis 771 | * @param y the number of pixels to scroll by on the Y axis 772 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 773 | */ 774 | void smoothScrollTo(int x, int y, int velocity) { 775 | if (getChildCount() == 0) { 776 | // Nothing to do. 777 | setScrollingCacheEnabled(false); 778 | return; 779 | } 780 | int sx = getScrollX(); 781 | int sy = getScrollY(); 782 | int dx = x - sx; 783 | int dy = y - sy; 784 | if (dx == 0 && dy == 0) { 785 | completeScroll(false); 786 | populate(); 787 | setScrollState(SCROLL_STATE_IDLE); 788 | return; 789 | } 790 | 791 | setScrollingCacheEnabled(true); 792 | setScrollState(SCROLL_STATE_SETTLING); 793 | 794 | final int width = getClientWidth(); 795 | final int halfWidth = width / 2; 796 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 797 | final float distance = halfWidth + halfWidth * 798 | distanceInfluenceForSnapDuration(distanceRatio); 799 | 800 | int duration = 0; 801 | velocity = Math.abs(velocity); 802 | if (velocity > 0) { 803 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 804 | } else { 805 | final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 806 | final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 807 | duration = (int) ((pageDelta + 1) * 300); 808 | } 809 | duration = Math.min(duration, MAX_SETTLE_DURATION); 810 | 811 | mScroller.startScroll(sx, sy, dx, dy, duration); 812 | ViewCompat.postInvalidateOnAnimation(this); 813 | } 814 | 815 | ItemInfo addNewItem(int position, int index) { 816 | ItemInfo ii = new ItemInfo(); 817 | ii.position = position; 818 | ii.object = mAdapter.instantiateItem(this, position); 819 | ii.widthFactor = mAdapter.getPageWidth(position); 820 | if (index < 0 || index >= mItems.size()) { 821 | mItems.add(ii); 822 | } else { 823 | mItems.add(index, ii); 824 | } 825 | return ii; 826 | } 827 | 828 | void dataSetChanged() { 829 | // This method only gets called if our observer is attached, so mAdapter is non-null. 830 | 831 | final int adapterCount = mAdapter.getCount(); 832 | mExpectedAdapterCount = adapterCount; 833 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 834 | mItems.size() < adapterCount; 835 | int newCurrItem = mCurItem; 836 | 837 | boolean isUpdating = false; 838 | for (int i = 0; i < mItems.size(); i++) { 839 | final ItemInfo ii = mItems.get(i); 840 | final int newPos = mAdapter.getItemPosition(ii.object); 841 | 842 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 843 | continue; 844 | } 845 | 846 | if (newPos == PagerAdapter.POSITION_NONE) { 847 | mItems.remove(i); 848 | i--; 849 | 850 | if (!isUpdating) { 851 | mAdapter.startUpdate(this); 852 | isUpdating = true; 853 | } 854 | 855 | mAdapter.destroyItem(this, ii.position, ii.object); 856 | needPopulate = true; 857 | 858 | if (mCurItem == ii.position) { 859 | // Keep the current item in the valid range 860 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 861 | needPopulate = true; 862 | } 863 | continue; 864 | } 865 | 866 | if (ii.position != newPos) { 867 | if (ii.position == mCurItem) { 868 | // Our current item changed position. Follow it. 869 | newCurrItem = newPos; 870 | } 871 | 872 | ii.position = newPos; 873 | needPopulate = true; 874 | } 875 | } 876 | 877 | if (isUpdating) { 878 | mAdapter.finishUpdate(this); 879 | } 880 | 881 | Collections.sort(mItems, COMPARATOR); 882 | 883 | if (needPopulate) { 884 | // Reset our known page widths; populate will recompute them. 885 | final int childCount = getChildCount(); 886 | for (int i = 0; i < childCount; i++) { 887 | final View child = getChildAt(i); 888 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 889 | if (!lp.isDecor) { 890 | lp.widthFactor = 0.f; 891 | } 892 | } 893 | 894 | setCurrentItemInternal(newCurrItem, false, true); 895 | requestLayout(); 896 | } 897 | } 898 | 899 | void populate() { 900 | populate(mCurItem); 901 | } 902 | 903 | void populate(int newCurrentItem) { 904 | ItemInfo oldCurInfo = null; 905 | int focusDirection = View.FOCUS_FORWARD; 906 | if (mCurItem != newCurrentItem) { 907 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 908 | oldCurInfo = infoForPosition(mCurItem); 909 | mCurItem = newCurrentItem; 910 | } 911 | 912 | if (mAdapter == null) { 913 | sortChildDrawingOrder(); 914 | return; 915 | } 916 | 917 | // Bail now if we are waiting to populate. This is to hold off 918 | // on creating views from the time the user releases their finger to 919 | // fling to a new position until we have finished the scroll to 920 | // that position, avoiding glitches from happening at that point. 921 | if (mPopulatePending) { 922 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 923 | sortChildDrawingOrder(); 924 | return; 925 | } 926 | 927 | // Also, don't populate until we are attached to a window. This is to 928 | // avoid trying to populate before we have restored our view hierarchy 929 | // state and conflicting with what is restored. 930 | if (getWindowToken() == null) { 931 | return; 932 | } 933 | 934 | mAdapter.startUpdate(this); 935 | 936 | final int pageLimit = mOffscreenPageLimit; 937 | final int startPos = Math.max(0, mCurItem - pageLimit); 938 | final int N = mAdapter.getCount(); 939 | final int endPos = Math.min(N-1, mCurItem + pageLimit); 940 | 941 | if (N != mExpectedAdapterCount) { 942 | String resName; 943 | try { 944 | resName = getResources().getResourceName(getId()); 945 | } catch (Resources.NotFoundException e) { 946 | resName = Integer.toHexString(getId()); 947 | } 948 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 949 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 950 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 951 | " Pager id: " + resName + 952 | " Pager class: " + getClass() + 953 | " Problematic adapter: " + mAdapter.getClass()); 954 | } 955 | 956 | // Locate the currently focused item or add it if needed. 957 | int curIndex = -1; 958 | ItemInfo curItem = null; 959 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 960 | final ItemInfo ii = mItems.get(curIndex); 961 | if (ii.position >= mCurItem) { 962 | if (ii.position == mCurItem) curItem = ii; 963 | break; 964 | } 965 | } 966 | 967 | if (curItem == null && N > 0) { 968 | curItem = addNewItem(mCurItem, curIndex); 969 | } 970 | 971 | // Fill 3x the available width or up to the number of offscreen 972 | // pages requested to either side, whichever is larger. 973 | // If we have no current item we have no work to do. 974 | if (curItem != null) { 975 | float extraWidthLeft = 0.f; 976 | int itemIndex = curIndex - 1; 977 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 978 | final int clientWidth = getClientWidth(); 979 | final float leftWidthNeeded = clientWidth <= 0 ? 0 : 980 | 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; 981 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 982 | if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { 983 | if (ii == null) { 984 | break; 985 | } 986 | if (pos == ii.position && !ii.scrolling) { 987 | mItems.remove(itemIndex); 988 | mAdapter.destroyItem(this, pos, ii.object); 989 | if (DEBUG) { 990 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 991 | " view: " + ((View) ii.object)); 992 | } 993 | itemIndex--; 994 | curIndex--; 995 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 996 | } 997 | } else if (ii != null && pos == ii.position) { 998 | extraWidthLeft += ii.widthFactor; 999 | itemIndex--; 1000 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1001 | } else { 1002 | ii = addNewItem(pos, itemIndex + 1); 1003 | extraWidthLeft += ii.widthFactor; 1004 | curIndex++; 1005 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1006 | } 1007 | } 1008 | 1009 | float extraWidthRight = curItem.widthFactor; 1010 | itemIndex = curIndex + 1; 1011 | if (extraWidthRight < 2.f) { 1012 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1013 | final float rightWidthNeeded = clientWidth <= 0 ? 0 : 1014 | (float) getPaddingRight() / (float) clientWidth + 2.f; 1015 | for (int pos = mCurItem + 1; pos < N; pos++) { 1016 | if (extraWidthRight >= rightWidthNeeded && pos > endPos) { 1017 | if (ii == null) { 1018 | break; 1019 | } 1020 | if (pos == ii.position && !ii.scrolling) { 1021 | mItems.remove(itemIndex); 1022 | mAdapter.destroyItem(this, pos, ii.object); 1023 | if (DEBUG) { 1024 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1025 | " view: " + ((View) ii.object)); 1026 | } 1027 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1028 | } 1029 | } else if (ii != null && pos == ii.position) { 1030 | extraWidthRight += ii.widthFactor; 1031 | itemIndex++; 1032 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1033 | } else { 1034 | ii = addNewItem(pos, itemIndex); 1035 | itemIndex++; 1036 | extraWidthRight += ii.widthFactor; 1037 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1038 | } 1039 | } 1040 | } 1041 | 1042 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 1043 | } 1044 | 1045 | if (DEBUG) { 1046 | Log.i(TAG, "Current page list:"); 1047 | for (int i=0; iA fake drag can be useful if you want to synchronize the motion of the ViewPager 2231 | * with the touch scrolling of another view, while still letting the ViewPager 2232 | * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) 2233 | * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call 2234 | * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. 2235 | * 2236 | *
During a fake drag the ViewPager will ignore all touch events. If a real drag
2237 | * is already in progress, this method will return false.
2238 | *
2239 | * @return true if the fake drag began successfully, false if it could not be started.
2240 | *
2241 | * @see #fakeDragBy(float)
2242 | * @see #endFakeDrag()
2243 | */
2244 | public boolean beginFakeDrag() {
2245 | if (mIsBeingDragged) {
2246 | return false;
2247 | }
2248 | mFakeDragging = true;
2249 | setScrollState(SCROLL_STATE_DRAGGING);
2250 | mInitialMotionX = mLastMotionX = 0;
2251 | if (mVelocityTracker == null) {
2252 | mVelocityTracker = VelocityTracker.obtain();
2253 | } else {
2254 | mVelocityTracker.clear();
2255 | }
2256 | final long time = SystemClock.uptimeMillis();
2257 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2258 | mVelocityTracker.addMovement(ev);
2259 | ev.recycle();
2260 | mFakeDragBeginTime = time;
2261 | return true;
2262 | }
2263 |
2264 | /**
2265 | * End a fake drag of the pager.
2266 | *
2267 | * @see #beginFakeDrag()
2268 | * @see #fakeDragBy(float)
2269 | */
2270 | public void endFakeDrag() {
2271 | if (!mFakeDragging) {
2272 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2273 | }
2274 |
2275 | final VelocityTracker velocityTracker = mVelocityTracker;
2276 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2277 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
2278 | velocityTracker, mActivePointerId);
2279 | mPopulatePending = true;
2280 | final int width = getClientWidth();
2281 | final int scrollX = getScrollX();
2282 | final ItemInfo ii = infoForCurrentScrollPosition();
2283 | final int currentPage = ii.position;
2284 | final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
2285 | final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
2286 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2287 | totalDelta);
2288 | setCurrentItemInternal(nextPage, true, true, initialVelocity);
2289 | endDrag();
2290 |
2291 | mFakeDragging = false;
2292 | }
2293 |
2294 | /**
2295 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2296 | *
2297 | * @param xOffset Offset in pixels to drag by.
2298 | * @see #beginFakeDrag()
2299 | * @see #endFakeDrag()
2300 | */
2301 | public void fakeDragBy(float xOffset) {
2302 | if (!mFakeDragging) {
2303 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2304 | }
2305 |
2306 | mLastMotionX += xOffset;
2307 |
2308 | float oldScrollX = getScrollX();
2309 | float scrollX = oldScrollX - xOffset;
2310 | final int width = getClientWidth();
2311 |
2312 | float leftBound = width * mFirstOffset;
2313 | float rightBound = width * mLastOffset;
2314 |
2315 | final ItemInfo firstItem = mItems.get(0);
2316 | final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2317 | if (firstItem.position != 0) {
2318 | leftBound = firstItem.offset * width;
2319 | }
2320 | if (lastItem.position != mAdapter.getCount() - 1) {
2321 | rightBound = lastItem.offset * width;
2322 | }
2323 |
2324 | if (scrollX < leftBound) {
2325 | scrollX = leftBound;
2326 | } else if (scrollX > rightBound) {
2327 | scrollX = rightBound;
2328 | }
2329 | // Don't lose the rounded component
2330 | mLastMotionX += scrollX - (int) scrollX;
2331 | scrollTo((int) scrollX, getScrollY());
2332 | pageScrolled((int) scrollX);
2333 |
2334 | // Synthesize an event for the VelocityTracker.
2335 | final long time = SystemClock.uptimeMillis();
2336 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2337 | mLastMotionX, 0, 0);
2338 | mVelocityTracker.addMovement(ev);
2339 | ev.recycle();
2340 | }
2341 |
2342 | /**
2343 | * Returns true if a fake drag is in progress.
2344 | *
2345 | * @return true if currently in a fake drag, false otherwise.
2346 | *
2347 | * @see #beginFakeDrag()
2348 | * @see #fakeDragBy(float)
2349 | * @see #endFakeDrag()
2350 | */
2351 | public boolean isFakeDragging() {
2352 | return mFakeDragging;
2353 | }
2354 |
2355 | private void onSecondaryPointerUp(MotionEvent ev) {
2356 | final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2357 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2358 | if (pointerId == mActivePointerId) {
2359 | // This was our active pointer going up. Choose a new
2360 | // active pointer and adjust accordingly.
2361 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2362 | mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
2363 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2364 | if (mVelocityTracker != null) {
2365 | mVelocityTracker.clear();
2366 | }
2367 | }
2368 | }
2369 |
2370 | private void endDrag() {
2371 | mIsBeingDragged = false;
2372 | mIsUnableToDrag = false;
2373 |
2374 | if (mVelocityTracker != null) {
2375 | mVelocityTracker.recycle();
2376 | mVelocityTracker = null;
2377 | }
2378 | }
2379 |
2380 | private void setScrollingCacheEnabled(boolean enabled) {
2381 | if (mScrollingCacheEnabled != enabled) {
2382 | mScrollingCacheEnabled = enabled;
2383 | if (USE_CACHE) {
2384 | final int size = getChildCount();
2385 | for (int i = 0; i < size; ++i) {
2386 | final View child = getChildAt(i);
2387 | if (child.getVisibility() != GONE) {
2388 | child.setDrawingCacheEnabled(enabled);
2389 | }
2390 | }
2391 | }
2392 | }
2393 | }
2394 |
2395 | public boolean canScrollHorizontally(int direction) {
2396 | if (mAdapter == null) {
2397 | return false;
2398 | }
2399 |
2400 | final int width = getClientWidth();
2401 | final int scrollX = getScrollX();
2402 | if (direction < 0) {
2403 | return (scrollX > (int) (width * mFirstOffset));
2404 | } else if (direction > 0) {
2405 | return (scrollX < (int) (width * mLastOffset));
2406 | } else {
2407 | return false;
2408 | }
2409 | }
2410 |
2411 | /**
2412 | * Tests scrollability within child views of v given a delta of dx.
2413 | *
2414 | * @param v View to test for horizontal scrollability
2415 | * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2416 | * or just its children (false).
2417 | * @param dx Delta scrolled in pixels
2418 | * @param x X coordinate of the active touch point
2419 | * @param y Y coordinate of the active touch point
2420 | * @return true if child views of v can be scrolled by delta of dx.
2421 | */
2422 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
2423 | if (v instanceof ViewGroup) {
2424 | final ViewGroup group = (ViewGroup) v;
2425 | final int scrollX = v.getScrollX();
2426 | final int scrollY = v.getScrollY();
2427 | final int count = group.getChildCount();
2428 | // Count backwards - let topmost views consume scroll distance first.
2429 | for (int i = count - 1; i >= 0; i--) {
2430 | // TODO: Add versioned support here for transformed views.
2431 | // This will not work for transformed views in Honeycomb+
2432 | final View child = group.getChildAt(i);
2433 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2434 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2435 | canScroll(child, true, dx, x + scrollX - child.getLeft(),
2436 | y + scrollY - child.getTop())) {
2437 | return true;
2438 | }
2439 | }
2440 | }
2441 |
2442 | return checkV && ViewCompat.canScrollHorizontally(v, -dx);
2443 | }
2444 |
2445 | @Override
2446 | public boolean dispatchKeyEvent(KeyEvent event) {
2447 | // Let the focused view and/or our descendants get the key first
2448 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2449 | }
2450 |
2451 | /**
2452 | * You can call this function yourself to have the scroll view perform
2453 | * scrolling from a key event, just as if the event had been dispatched to
2454 | * it by the view hierarchy.
2455 | *
2456 | * @param event The key event to execute.
2457 | * @return Return true if the event was handled, else false.
2458 | */
2459 | public boolean executeKeyEvent(KeyEvent event) {
2460 | boolean handled = false;
2461 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
2462 | switch (event.getKeyCode()) {
2463 | case KeyEvent.KEYCODE_DPAD_LEFT:
2464 | handled = arrowScroll(FOCUS_LEFT);
2465 | break;
2466 | case KeyEvent.KEYCODE_DPAD_RIGHT:
2467 | handled = arrowScroll(FOCUS_RIGHT);
2468 | break;
2469 | case KeyEvent.KEYCODE_TAB:
2470 | if (Build.VERSION.SDK_INT >= 11) {
2471 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2472 | // before Android 3.0. Ignore the tab key on those devices.
2473 | if (KeyEventCompat.hasNoModifiers(event)) {
2474 | handled = arrowScroll(FOCUS_FORWARD);
2475 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2476 | handled = arrowScroll(FOCUS_BACKWARD);
2477 | }
2478 | }
2479 | break;
2480 | }
2481 | }
2482 | return handled;
2483 | }
2484 |
2485 | public boolean arrowScroll(int direction) {
2486 | View currentFocused = findFocus();
2487 | if (currentFocused == this) {
2488 | currentFocused = null;
2489 | } else if (currentFocused != null) {
2490 | boolean isChild = false;
2491 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2492 | parent = parent.getParent()) {
2493 | if (parent == this) {
2494 | isChild = true;
2495 | break;
2496 | }
2497 | }
2498 | if (!isChild) {
2499 | // This would cause the focus search down below to fail in fun ways.
2500 | final StringBuilder sb = new StringBuilder();
2501 | sb.append(currentFocused.getClass().getSimpleName());
2502 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2503 | parent = parent.getParent()) {
2504 | sb.append(" => ").append(parent.getClass().getSimpleName());
2505 | }
2506 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2507 | "current focused view " + sb.toString());
2508 | currentFocused = null;
2509 | }
2510 | }
2511 |
2512 | boolean handled = false;
2513 |
2514 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2515 | direction);
2516 | if (nextFocused != null && nextFocused != currentFocused) {
2517 | if (direction == View.FOCUS_LEFT) {
2518 | // If there is nothing to the left, or this is causing us to
2519 | // jump to the right, then what we really want to do is page left.
2520 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2521 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2522 | if (currentFocused != null && nextLeft >= currLeft) {
2523 | handled = pageLeft();
2524 | } else {
2525 | handled = nextFocused.requestFocus();
2526 | }
2527 | } else if (direction == View.FOCUS_RIGHT) {
2528 | // If there is nothing to the right, or this is causing us to
2529 | // jump to the left, then what we really want to do is page right.
2530 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2531 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2532 | if (currentFocused != null && nextLeft <= currLeft) {
2533 | handled = pageRight();
2534 | } else {
2535 | handled = nextFocused.requestFocus();
2536 | }
2537 | }
2538 | } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
2539 | // Trying to move left and nothing there; try to page.
2540 | handled = pageLeft();
2541 | } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
2542 | // Trying to move right and nothing there; try to page.
2543 | handled = pageRight();
2544 | }
2545 | if (handled) {
2546 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2547 | }
2548 | return handled;
2549 | }
2550 |
2551 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2552 | if (outRect == null) {
2553 | outRect = new Rect();
2554 | }
2555 | if (child == null) {
2556 | outRect.set(0, 0, 0, 0);
2557 | return outRect;
2558 | }
2559 | outRect.left = child.getLeft();
2560 | outRect.right = child.getRight();
2561 | outRect.top = child.getTop();
2562 | outRect.bottom = child.getBottom();
2563 |
2564 | ViewParent parent = child.getParent();
2565 | while (parent instanceof ViewGroup && parent != this) {
2566 | final ViewGroup group = (ViewGroup) parent;
2567 | outRect.left += group.getLeft();
2568 | outRect.right += group.getRight();
2569 | outRect.top += group.getTop();
2570 | outRect.bottom += group.getBottom();
2571 |
2572 | parent = group.getParent();
2573 | }
2574 | return outRect;
2575 | }
2576 |
2577 | boolean pageLeft() {
2578 | if (mCurItem > 0) {
2579 | setCurrentItem(mCurItem-1, true);
2580 | return true;
2581 | }
2582 | return false;
2583 | }
2584 |
2585 | boolean pageRight() {
2586 | if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
2587 | setCurrentItem(mCurItem+1, true);
2588 | return true;
2589 | }
2590 | return false;
2591 | }
2592 |
2593 | /**
2594 | * We only want the current page that is being shown to be focusable.
2595 | */
2596 | @Override
2597 | public void addFocusables(ArrayList