Repository: ksoichiro/Android-ObservableScrollView Branch: master Commit: 47a5fb2db5e9 Files: 352 Total size: 1.0 MB Directory structure: gitextract_qypqnipg/ ├── .editorconfig ├── .gitignore ├── .travis-script.sh ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── docs/ │ ├── _data.json │ ├── _layout.ejs │ ├── advanced/ │ │ ├── _data.json │ │ ├── index.md │ │ ├── sliding-up.md │ │ └── viewpager.md │ ├── basic/ │ │ ├── _data.json │ │ ├── filling-gap.md │ │ ├── flexible-space-toolbar.md │ │ ├── flexible-space-with-image.md │ │ ├── index.md │ │ ├── parallax-image.md │ │ ├── show-hide-action-bar.md │ │ ├── sticky-header.md │ │ └── translating-toolbar.md │ ├── contributor/ │ │ ├── _data.json │ │ ├── ci.md │ │ ├── index.md │ │ ├── release.md │ │ └── update-website.md │ ├── example/ │ │ ├── _data.json │ │ ├── android-studio.md │ │ ├── eclipse.md │ │ ├── google-play.md │ │ ├── index.md │ │ └── wercker.md │ ├── faq.md │ ├── overview.md │ ├── quick-start/ │ │ ├── _data.json │ │ ├── animation.md │ │ ├── dependencies.md │ │ ├── index.md │ │ └── layout.md │ └── reference/ │ ├── _data.json │ ├── environment.md │ ├── index.md │ ├── release-notes.md │ └── supported-widgets.md ├── gradle/ │ ├── gradle-mvn-push.gradle │ ├── version.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── library/ │ ├── .gitignore │ ├── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── res/ │ │ └── .gitkeep │ └── src/ │ ├── androidTest/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── lipsum.html │ │ ├── java/ │ │ │ └── com/ │ │ │ ├── github/ │ │ │ │ └── ksoichiro/ │ │ │ │ └── android/ │ │ │ │ └── observablescrollview/ │ │ │ │ ├── SavedStateTest.java │ │ │ │ └── test/ │ │ │ │ ├── GridViewActivity.java │ │ │ │ ├── GridViewActivityTest.java │ │ │ │ ├── HeaderGridViewActivity.java │ │ │ │ ├── HeaderGridViewActivityTest.java │ │ │ │ ├── ListViewActivity.java │ │ │ │ ├── ListViewActivityTest.java │ │ │ │ ├── ListViewScrollFromBottomActivity.java │ │ │ │ ├── ListViewScrollFromBottomActivityTest.java │ │ │ │ ├── RecyclerViewActivity.java │ │ │ │ ├── RecyclerViewActivityTest.java │ │ │ │ ├── RecyclerViewScrollFromBottomActivity.java │ │ │ │ ├── RecyclerViewScrollFromBottomActivityTest.java │ │ │ │ ├── ScrollUtilsTest.java │ │ │ │ ├── ScrollViewActivity.java │ │ │ │ ├── ScrollViewActivityTest.java │ │ │ │ ├── SimpleHeaderRecyclerAdapter.java │ │ │ │ ├── SimpleRecyclerAdapter.java │ │ │ │ ├── TouchInterceptionGridViewActivity.java │ │ │ │ ├── TouchInterceptionGridViewActivityTest.java │ │ │ │ ├── TouchInterceptionListViewActivity.java │ │ │ │ ├── TouchInterceptionListViewActivityTest.java │ │ │ │ ├── TouchInterceptionRecyclerViewActivity.java │ │ │ │ ├── TouchInterceptionRecyclerViewActivityTest.java │ │ │ │ ├── TouchInterceptionScrollViewActivity.java │ │ │ │ ├── TouchInterceptionScrollViewActivityTest.java │ │ │ │ ├── TouchInterceptionWebViewActivity.java │ │ │ │ ├── TouchInterceptionWebViewActivityTest.java │ │ │ │ ├── UiTestUtils.java │ │ │ │ ├── ViewPagerTab2Activity.java │ │ │ │ ├── ViewPagerTab2ActivityTest.java │ │ │ │ ├── ViewPagerTab2GridViewFragment.java │ │ │ │ ├── ViewPagerTab2ListViewFragment.java │ │ │ │ ├── ViewPagerTab2RecyclerViewFragment.java │ │ │ │ ├── ViewPagerTab2ScrollViewFragment.java │ │ │ │ ├── ViewPagerTab2WebViewFragment.java │ │ │ │ ├── ViewPagerTabActivity.java │ │ │ │ ├── ViewPagerTabActivityTest.java │ │ │ │ ├── ViewPagerTabListViewFragment.java │ │ │ │ ├── ViewPagerTabRecyclerViewFragment.java │ │ │ │ ├── ViewPagerTabScrollViewFragment.java │ │ │ │ ├── WebViewActivity.java │ │ │ │ └── WebViewActivityTest.java │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── iosched/ │ │ │ └── ui/ │ │ │ └── widget/ │ │ │ ├── SlidingTabLayout.java │ │ │ └── SlidingTabStrip.java │ │ └── res/ │ │ ├── color/ │ │ │ └── tab_text_color.xml │ │ ├── layout/ │ │ │ ├── activity_gridview.xml │ │ │ ├── activity_listview.xml │ │ │ ├── activity_recyclerview.xml │ │ │ ├── activity_scrollview.xml │ │ │ ├── activity_touchinterception_gridview.xml │ │ │ ├── activity_touchinterception_listview.xml │ │ │ ├── activity_touchinterception_recyclerview.xml │ │ │ ├── activity_touchinterception_scrollview.xml │ │ │ ├── activity_touchinterception_webview.xml │ │ │ ├── activity_viewpagertab.xml │ │ │ ├── activity_viewpagertab2.xml │ │ │ ├── activity_webview.xml │ │ │ ├── fragment_gridview.xml │ │ │ ├── fragment_listview.xml │ │ │ ├── fragment_recyclerview.xml │ │ │ ├── fragment_scrollview.xml │ │ │ ├── fragment_scrollview_noheader.xml │ │ │ ├── fragment_webview.xml │ │ │ ├── padding.xml │ │ │ └── tab_indicator.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── main/ │ └── java/ │ └── com/ │ └── github/ │ └── ksoichiro/ │ └── android/ │ └── observablescrollview/ │ ├── CacheFragmentStatePagerAdapter.java │ ├── ObservableGridView.java │ ├── ObservableListView.java │ ├── ObservableRecyclerView.java │ ├── ObservableScrollView.java │ ├── ObservableScrollViewCallbacks.java │ ├── ObservableWebView.java │ ├── ScrollState.java │ ├── ScrollUtils.java │ ├── Scrollable.java │ └── TouchInterceptionFrameLayout.java ├── samples/ │ ├── .gitignore │ ├── AndroidManifest.xml │ ├── README.md │ ├── assets/ │ │ ├── handletouch.html │ │ └── lipsum.html │ ├── build.gradle │ ├── proguard-rules.pro │ ├── res/ │ │ ├── color/ │ │ │ └── tab_text_color.xml │ │ ├── drawable/ │ │ │ ├── gradient_header_background.xml │ │ │ └── sliding_header_overlay.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_actionbarcontrolgridview.xml │ │ │ ├── activity_actionbarcontrollistview.xml │ │ │ ├── activity_actionbarcontrolrecyclerview.xml │ │ │ ├── activity_actionbarcontrolscrollview.xml │ │ │ ├── activity_actionbarcontrolwebview.xml │ │ │ ├── activity_fillgap3listview.xml │ │ │ ├── activity_fillgap3recyclerview.xml │ │ │ ├── activity_fillgap3scrollview.xml │ │ │ ├── activity_fillgaplistview.xml │ │ │ ├── activity_fillgaprecyclerview.xml │ │ │ ├── activity_fillgapscrollview.xml │ │ │ ├── activity_flexiblespacetoolbarscrollview.xml │ │ │ ├── activity_flexiblespacetoolbarwebview.xml │ │ │ ├── activity_flexiblespacewithimagegridview.xml │ │ │ ├── activity_flexiblespacewithimagelistview.xml │ │ │ ├── activity_flexiblespacewithimagerecyclerview.xml │ │ │ ├── activity_flexiblespacewithimagescrollview.xml │ │ │ ├── activity_flexiblespacewithimagewithviewpagertab.xml │ │ │ ├── activity_flexiblespacewithimagewithviewpagertab2.xml │ │ │ ├── activity_fragmentactionbarcontrol.xml │ │ │ ├── activity_fragmenttransition.xml │ │ │ ├── activity_handletouchgridview.xml │ │ │ ├── activity_handletouchlistview.xml │ │ │ ├── activity_handletouchrecyclerview.xml │ │ │ ├── activity_handletouchscrollview.xml │ │ │ ├── activity_handletouchwebview.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_parallaxtoolbargridview.xml │ │ │ ├── activity_parallaxtoolbarlistview.xml │ │ │ ├── activity_parallaxtoolbarscrollview.xml │ │ │ ├── activity_slidingupgridview.xml │ │ │ ├── activity_slidinguplistview.xml │ │ │ ├── activity_slidinguprecyclerview.xml │ │ │ ├── activity_slidingupscrollview.xml │ │ │ ├── activity_slidingupwebview.xml │ │ │ ├── activity_stickyheaderlistview.xml │ │ │ ├── activity_stickyheaderrecyclerview.xml │ │ │ ├── activity_stickyheaderscrollview.xml │ │ │ ├── activity_stickyheaderwebview.xml │ │ │ ├── activity_toolbarcontrolgridview.xml │ │ │ ├── activity_toolbarcontrollistview.xml │ │ │ ├── activity_toolbarcontrolrecyclerview.xml │ │ │ ├── activity_toolbarcontrolscrollview.xml │ │ │ ├── activity_toolbarcontrolwebview.xml │ │ │ ├── activity_viewpagertab.xml │ │ │ ├── activity_viewpagertab2.xml │ │ │ ├── activity_viewpagertabfragment.xml │ │ │ ├── divider.xml │ │ │ ├── fragment_actionbarcontrollistview.xml │ │ │ ├── fragment_flexiblespacewithimagegridview.xml │ │ │ ├── fragment_flexiblespacewithimagelistview.xml │ │ │ ├── fragment_flexiblespacewithimagerecyclerview.xml │ │ │ ├── fragment_flexiblespacewithimagescrollview.xml │ │ │ ├── fragment_fragmenttransition_default.xml │ │ │ ├── fragment_fragmenttransition_second.xml │ │ │ ├── fragment_gridview.xml │ │ │ ├── fragment_listview.xml │ │ │ ├── fragment_recyclerview.xml │ │ │ ├── fragment_scrollview.xml │ │ │ ├── fragment_scrollview_noheader.xml │ │ │ ├── fragment_scrollviewwithfab.xml │ │ │ ├── fragment_viewpagertabfragment_parent.xml │ │ │ ├── fragment_webview.xml │ │ │ ├── gradient_header.xml │ │ │ ├── list_item_handletouch.xml │ │ │ ├── list_item_main.xml │ │ │ ├── padding.xml │ │ │ ├── recycler_header.xml │ │ │ └── tab_indicator.xml │ │ ├── layout-v11/ │ │ │ └── tab_indicator.xml │ │ ├── menu/ │ │ │ └── menu_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ ├── strings_license.xml │ │ │ └── styles.xml │ │ ├── values-ar/ │ │ │ └── strings.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── github/ │ │ └── ksoichiro/ │ │ └── app/ │ │ └── ApplicationTest.java │ └── main/ │ └── java/ │ └── com/ │ ├── github/ │ │ └── ksoichiro/ │ │ └── android/ │ │ └── observablescrollview/ │ │ └── samples/ │ │ ├── AboutActivity.java │ │ ├── ActionBarControlGridViewActivity.java │ │ ├── ActionBarControlListViewActivity.java │ │ ├── ActionBarControlRecyclerViewActivity.java │ │ ├── ActionBarControlScrollViewActivity.java │ │ ├── ActionBarControlWebViewActivity.java │ │ ├── BaseActivity.java │ │ ├── BaseFragment.java │ │ ├── FillGap2BaseActivity.java │ │ ├── FillGap2ListViewActivity.java │ │ ├── FillGap2RecyclerViewActivity.java │ │ ├── FillGap2ScrollViewActivity.java │ │ ├── FillGap3BaseActivity.java │ │ ├── FillGap3ListViewActivity.java │ │ ├── FillGap3RecyclerViewActivity.java │ │ ├── FillGap3ScrollViewActivity.java │ │ ├── FillGapBaseActivity.java │ │ ├── FillGapListViewActivity.java │ │ ├── FillGapRecyclerViewActivity.java │ │ ├── FillGapScrollViewActivity.java │ │ ├── FlexibleSpaceToolbarScrollViewActivity.java │ │ ├── FlexibleSpaceToolbarWebViewActivity.java │ │ ├── FlexibleSpaceWithImageBaseFragment.java │ │ ├── FlexibleSpaceWithImageGridViewActivity.java │ │ ├── FlexibleSpaceWithImageGridViewFragment.java │ │ ├── FlexibleSpaceWithImageListViewActivity.java │ │ ├── FlexibleSpaceWithImageListViewFragment.java │ │ ├── FlexibleSpaceWithImageRecyclerViewActivity.java │ │ ├── FlexibleSpaceWithImageRecyclerViewFragment.java │ │ ├── FlexibleSpaceWithImageScrollViewActivity.java │ │ ├── FlexibleSpaceWithImageScrollViewFragment.java │ │ ├── FlexibleSpaceWithImageWithViewPagerTab2Activity.java │ │ ├── FlexibleSpaceWithImageWithViewPagerTabActivity.java │ │ ├── FragmentActionBarControlListViewActivity.java │ │ ├── FragmentActionBarControlListViewFragment.java │ │ ├── FragmentTransitionActivity.java │ │ ├── FragmentTransitionDefaultFragment.java │ │ ├── FragmentTransitionSecondFragment.java │ │ ├── HandleTouchGridViewActivity.java │ │ ├── HandleTouchListViewActivity.java │ │ ├── HandleTouchRecyclerViewActivity.java │ │ ├── HandleTouchScrollViewActivity.java │ │ ├── HandleTouchWebViewActivity.java │ │ ├── MainActivity.java │ │ ├── ParallaxToolbarGridViewActivity.java │ │ ├── ParallaxToolbarListViewActivity.java │ │ ├── ParallaxToolbarScrollViewActivity.java │ │ ├── ScrollFromBottomListViewActivity.java │ │ ├── ScrollFromBottomRecyclerViewActivity.java │ │ ├── SimpleHeaderRecyclerAdapter.java │ │ ├── SimpleRecyclerAdapter.java │ │ ├── SlidingUpBaseActivity.java │ │ ├── SlidingUpGridViewActivity.java │ │ ├── SlidingUpListViewActivity.java │ │ ├── SlidingUpRecyclerViewActivity.java │ │ ├── SlidingUpScrollViewActivity.java │ │ ├── SlidingUpWebViewActivity.java │ │ ├── StickyHeaderListViewActivity.java │ │ ├── StickyHeaderRecyclerViewActivity.java │ │ ├── StickyHeaderScrollViewActivity.java │ │ ├── StickyHeaderWebViewActivity.java │ │ ├── ToolbarControlBaseActivity.java │ │ ├── ToolbarControlGridViewActivity.java │ │ ├── ToolbarControlListViewActivity.java │ │ ├── ToolbarControlRecyclerViewActivity.java │ │ ├── ToolbarControlScrollViewActivity.java │ │ ├── ToolbarControlWebViewActivity.java │ │ ├── ViewPagerTab2Activity.java │ │ ├── ViewPagerTab2GridViewFragment.java │ │ ├── ViewPagerTab2ListViewFragment.java │ │ ├── ViewPagerTab2RecyclerViewFragment.java │ │ ├── ViewPagerTab2ScrollViewFragment.java │ │ ├── ViewPagerTab2WebViewFragment.java │ │ ├── ViewPagerTabActivity.java │ │ ├── ViewPagerTabFragmentActivity.java │ │ ├── ViewPagerTabFragmentGridViewFragment.java │ │ ├── ViewPagerTabFragmentListViewFragment.java │ │ ├── ViewPagerTabFragmentParentFragment.java │ │ ├── ViewPagerTabFragmentRecyclerViewFragment.java │ │ ├── ViewPagerTabFragmentScrollViewFragment.java │ │ ├── ViewPagerTabFragmentWebViewFragment.java │ │ ├── ViewPagerTabGridViewFragment.java │ │ ├── ViewPagerTabListViewActivity.java │ │ ├── ViewPagerTabListViewFragment.java │ │ ├── ViewPagerTabRecyclerViewFragment.java │ │ ├── ViewPagerTabScrollViewActivity.java │ │ ├── ViewPagerTabScrollViewFragment.java │ │ ├── ViewPagerTabScrollViewWithFabActivity.java │ │ └── ViewPagerTabScrollViewWithFabFragment.java │ └── google/ │ └── samples/ │ └── apps/ │ └── iosched/ │ └── ui/ │ └── widget/ │ ├── SlidingTabLayout.java │ └── SlidingTabStrip.java ├── settings.gradle ├── website/ │ ├── .bowerrc │ ├── .gitignore │ ├── bower.json │ ├── gulpfile.js │ ├── harp.json │ ├── package.json │ └── public/ │ ├── 404.ejs │ ├── _data.json │ ├── _footer.ejs │ ├── _head.ejs │ ├── _layout.ejs │ ├── _nav.ejs │ ├── browserconfig.xml │ ├── css/ │ │ ├── _code.less │ │ ├── _colors.less │ │ ├── _fonts.less │ │ ├── _footer.less │ │ ├── _layout.less │ │ ├── _misc.less │ │ ├── _mixins.less │ │ ├── _navbar.less │ │ ├── _roboto-fonts.less │ │ ├── _sidebar.less │ │ ├── _site-top.less │ │ └── main.less │ ├── index.ejs │ ├── js/ │ │ └── main.coffee │ └── manifest.json └── wercker.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{.travis.yml,.travis-script.sh,*.json,*.coffee,*.less}] indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitignore ================================================ .gradle *.iml *.keystore /local.properties private.properties /.idea/ .DS_Store /build bin/ gen/ libs/ aarDependencies/ .project .classpath project.properties *.swp ================================================ FILE: .travis-script.sh ================================================ #!/bin/bash echo "TEST_TARGET=${TEST_TARGET}" echo "TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST}" echo "TRAVIS_BRANCH=${TRAVIS_BRANCH}" if [ "$TEST_TARGET" = "android" ]; then # Release build type is only for Google Play store currently, # which resolve dependency from Maven Central. # This causes build errors while developing a new feature, so disable release build. ./gradlew --full-stacktrace assembleDevDebug :library:connectedCheck elif [ "$TEST_TARGET" = "website" ]; then if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ ! -z "$GH_TOKEN" ]; then echo "Update website..." pushd website > /dev/null 2>&1 npm run deploy popd > /dev/null 2>&1 fi fi ================================================ FILE: .travis.yml ================================================ language: android sudo: false env: global: - GIT_COMMITTER_NAME=ksoichiro - GIT_COMMITTER_EMAIL=soichiro.kashima@gmail.com - GIT_AUTHOR_NAME=ksoichiro - GIT_AUTHOR_EMAIL=soichiro.kashima@gmail.com - secure: Iw0mIseFZ6M/HGi/ERPBT4fabx0G1OVeHCbu6ANoVybO8yHBRHHuUc4pdZOOtEi+Ce/4a5TPZXbGPIVMZrN1ewS3uDAHsEFjmtehsqjk5iKXapvy04dwLsX9jCsQNl0n5679tRZ2eXUGqyVSddc5pIyWwJAGgBmnM/SHDaRy4YA= matrix: - TEST_TARGET=android - TEST_TARGET=website cache: directories: - website/node_modules - website/bower_components install: - true && ([ "$TEST_TARGET" != "website" ] || (cd website && npm install && cd ..)) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=tools) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=build-tools-23.0.2) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=android-19) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=android-21) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=android-22) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=android-23) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=sys-img-armeabi-v7a-android-19) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=extra-android-support) - true && ([ "$TEST_TARGET" != "android" ] || android-update-sdk --accept-licenses='android-sdk-license-.+' --components=extra-android-m2repository) before_script: - true && ([ "$TEST_TARGET" != "android" ] || (echo no | android create avd --force -n test -t android-19 --abi default/armeabi-v7a)) - true && ([ "$TEST_TARGET" != "android" ] || emulator -avd test -no-skin -no-audio -no-window &) - true && ([ "$TEST_TARGET" != "android" ] || android-wait-for-emulator) script: - "/bin/bash .travis-script.sh" after_success: - true && ([ "$TEST_TARGET" != "android" ] || ./gradlew :library:coveralls) ================================================ FILE: CONTRIBUTING.md ================================================ # Thank you for your contribution! Any contributions will be greatly appreciated. Before submitting a new issue, please check the following guideline. ## Describe your issue as much as possible The library itself only provides the scroll information, and creating awesome scrolling effects depends deeply on how you use it: layout, offset calculation to animate views, etc. Therefore, if you find an issue, please describe not only the issue itself but also the following information: ### If you find it in the sample app of this project * Required * Activity name * Operation to produce the issue * Modified code * If you modified some codes in the sample, it should be described with modified codes * Preferred * Version (git commit hash) of the codes * Android OS version * device * Nexus5, x86 emulator, etc. ### If you find it in your app * Required * Operation to produce the issue * Related code * Without the related codes, I can't say anything. Screenshot / movie is useful to understand the issue, but not enough to discuss the cause of the issue. * Preferred * Version of the library * Android OS version * device * Nexus5, x86 emulator, etc. ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Android-ObservableScrollView [![Build Status](http://img.shields.io/travis/ksoichiro/Android-ObservableScrollView.svg?style=flat)](https://travis-ci.org/ksoichiro/Android-ObservableScrollView) [![Coverage Status](https://img.shields.io/coveralls/ksoichiro/Android-ObservableScrollView/master.svg?style=flat)](https://coveralls.io/r/ksoichiro/Android-ObservableScrollView?branch=master) [![Maven Central](http://img.shields.io/maven-central/v/com.github.ksoichiro/android-observablescrollview.svg?style=flat)](https://github.com/ksoichiro/Android-ObservableScrollView/releases/latest) [![API](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=9) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Android--ObservableScrollView-brightgreen.svg?style=flat)](https://android-arsenal.com/details/1/1136) Android library to observe scroll events on scrollable views. It's easy to interact with the Toolbar introduced in Android 5.0 Lollipop and may be helpful to implement look and feel of Material Design apps. ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo12.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo10.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo11.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo13.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo1.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo2.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo3.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo4.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo5.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo6.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo7.gif) ![](https://raw.githubusercontent.com/ksoichiro/Android-ObservableScrollView/master/samples/images/demo8.gif) ## Examples ### Download from Google Play Get it on Google Play Please note that the app on the Play store is not always the latest version. ### Download from wercker If you are a wercker user, you can download the latest build artifact. [See here for details](docs/example/wercker.md). [![wercker status](https://app.wercker.com/status/8d1e27d9f4a662b25dbe70402733582b/m/master "wercker status")](https://app.wercker.com/project/bykey/8d1e27d9f4a662b25dbe70402733582b) ### Install manually Just clone and execute `installDevDebug` task with Gradle. [See here for details](docs/example/android-studio.md). ## Usage 1. Add `com.github.ksoichiro:android-observablescrollview` to your `dependencies` in `build.gradle`. 1. Add `ObservableListView` or other views you'd like to use. 1. Write some animation codes to the callbacks such as `onScrollChanged`, `onUpOrCancelMotionEvent`, etc. See [the quick start guide for details](docs/quick-start/index.md), and [the documentation](docs/overview.md) for further more. ## Reference * [Supported widgets](docs/reference/supported-widgets.md) * [Environment](docs/reference/environment.md) * [Release notes](docs/reference/release-notes.md) * [FAQ](docs/faq.md) ## Apps that use this library [![Badge](http://www.libtastic.com/static/osbadges/4.png)](http://www.libtastic.com/technology/4/) * [Jair Player](https://play.google.com/store/apps/details?id=aj.jair.music) by Akshay Chordiya * [My Gradle](https://play.google.com/store/apps/details?id=se.project.generic.mygradle) by Erick Chavez Alcarraz * [ThemeDIY](https://play.google.com/store/apps/details?id=net.darkion.theme.maker) by Darkion Avey * [{Soft} Skills](https://play.google.com/store/apps/details?id=com.fanaticdevs.androider) by Fanatic Devs If you're using this library in your app and you'd like to list it here, please let me know via [email](mailto:soichiro.kashima@gmail.com) or [pull requests](https://github.com/ksoichiro/Android-ObservableScrollView/pulls) or [issues](https://github.com/ksoichiro/Android-ObservableScrollView/issues). ## Contributions Any contributions are welcome! Please check the [FAQ](docs/faq.md) and [contributing guideline](https://github.com/ksoichiro/Android-ObservableScrollView/tree/master/CONTRIBUTING.md) before submitting a new issue. ## Developed By * Soichiro Kashima - [soichiro.kashima@gmail.com](mailto:soichiro.kashima@gmail.com) ## Thanks * Inspired by `ObservableScrollView` in [romannurik-code](https://code.google.com/p/romannurik-code/). ## License ```license Copyright 2014 Soichiro Kashima Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: build.gradle ================================================ buildscript { repositories { mavenCentral() } dependencies { classpath 'com.github.ksoichiro:gradle-eclipse-aar-plugin:0.1.1' // Related issue: https://code.google.com/p/android/issues/detail?id=192875 classpath 'org.jacoco:org.jacoco.core:0.7.5.201505241946' } } allprojects { group = GROUP version = VERSION_NAME repositories { mavenCentral() } } subprojects { buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.1.0' } } } apply plugin: 'com.github.ksoichiro.eclipse.aar' eclipseAar { projectNamePrefix = 'observablescrollview-' cleanLibsDirectoryEnabled = true } ================================================ FILE: docs/_data.json ================================================ { "overview": { "title": "Overview" }, "faq": { "title": "FAQ" } } ================================================ FILE: docs/_layout.ejs ================================================ <%- partial("../_head", { title: title, ogType: "article" }) %> <%- partial("../_nav") %>

<%- yield %>
<%- partial("../_footer") %> ================================================ FILE: docs/advanced/_data.json ================================================ { "index": { "title": "Advanced techniques" }, "sliding-up": { "title": "Sliding up pattern" }, "viewpager": { "title": "ViewPager pattern" } } ================================================ FILE: docs/advanced/index.md ================================================ # Advanced techniques This section describes advanced scrolling techniques. When you've done this topic, you could be be an expert of handling scrolls! I'd appreciate it if you could suggest new patterns or improvements of the library. 1. [Sliding up pattern](../../docs/advanced/sliding-up.md) 1. [ViewPager pattern](../../docs/advanced/viewpager.md) [Next: Sliding up pattern »](../../docs/advanced/sliding-up.md) ================================================ FILE: docs/advanced/sliding-up.md ================================================ # Sliding up pattern This topic describes how to slide a panel from the bottom like Google's "Map" app, which are implemented in the following examples. * SlidingUpBaseActivity * SlidingUpGridViewActivity * SlidingUpListViewActivity * SlidingUpRecyclerViewActivity * SlidingUpScrollViewActivity * SlidingUpWebViewActivity --- Coming soon... [Next: ViewPager pattern »](../../docs/advanced/viewpager.md) ================================================ FILE: docs/advanced/viewpager.md ================================================ # ViewPager pattern This topic describes how to integrate scrollable views with ViewPager, which are implemented in the following examples. * ViewPagerTab2Activity * ViewPagerTabActivity * ViewPagerTabFragmentActivity * ViewPagerTabListViewActivity * ViewPagerTabScrollViewActivity --- Coming soon... ================================================ FILE: docs/basic/_data.json ================================================ { "index": { "title": "Basic techniques" }, "show-hide-action-bar": { "title": "Show and hide the ActionBar" }, "translating-toolbar": { "title": "Translating the Toolbar" }, "parallax-image": { "title": "Parallax image" }, "sticky-header": { "title": "Sticky header" }, "flexible-space-toolbar": { "title": "Flexible space on the Toolbar" }, "flexible-space-with-image": { "title": "Flexible space with image" }, "filling-gap": { "title": "Filling gap on top of the Toolbar" } } ================================================ FILE: docs/basic/filling-gap.md ================================================ # Filling gap on top of the Toolbar This topic describes how to fill the gap on top of the Toolbar, which are implemented in the following examples. * FillGapBaseActivity * FillGapListViewActivity * FillGapRecyclerViewActivity * FillGapScrollViewActivity * FillGap2BaseActivity * FillGap2ListViewActivity * FillGap2RecyclerViewActivity * FillGap2ScrollViewActivity * FillGap3BaseActivity * FillGap3ListViewActivity * FillGap3RecyclerViewActivity * FillGap3ScrollViewActivity Please note that these patterns only works for Android 4+. --- ## Overview There are many examples for this pattern, but they can be classified to the following: * FillGap * When swiping up, the header bar expands and fill the gap between the header and the top of the screen. * FillGap2 * Almost same as FillGap, but in this pattern, after the gap is filled with primary color, the filled space is going to shrink, and the header bar moves. * FillGap3 * Usually FillGap should work only when the Scrollable view can scroll. But sometimes you may want to scroll them with few items, and you can achieve it with this pattern. * This uses `TouchInterceptionFrameLayout` (one of the widgets in this library), and this component does not handle "velocity" of scrolls, so as soon as you touch up your fingers, translation will be stopped. ## Pattern1 (FillGap) ### ScrollView #### Basic structure ```xml ``` `clipChildren` attribute is important. Without it, part of the views are not drawn. Incorrect (without `android:clipChildren="false"`): ![](../../docs/images/basic_7.png) Correct (with `android:clipChildren="false"`): ![](../../docs/images/basic_6.png) Coming soon... ## Pattern2 (FillGap2) Coming soon... ## Pattern3 (FillGap3) Coming soon... [Next: Advanced techniques »](../../docs/advanced/index.md) ================================================ FILE: docs/basic/flexible-space-toolbar.md ================================================ # Flexible space on the Toolbar This topic describes how to create flexible space on the Toolbar, which are implemented in the following examples. * FlexibleSpaceToolbarScrollViewActivity * FlexibleSpaceToolbarWebViewActivity I originally tried implementing this pattern (only the title animation): [Flexible space with image](http://material-design.storage.googleapis.com/publish/material_v_3/material_ext_publish/0B969e8h0awhvQ3lJdU9WVTh1WWM/patterns_scrolling_flexspaceimage.webm) --- ## Using ScrollView ### Layout with ScrollView #### Basic structure ```xml ``` The root `FrameLayout` is used for moving children separately. The second `FrameLayout`(`@id/body`) inside the ScrollView is the main content, and you can put any views as you like. This time, we'll add just a `TextView`. `View`(`@id/flexible_space`) is for a "flexible space" which has a opaque background. This view will be translated vertically on scrolling. `Toolbar` is a normal Toolbar, but this Toolbar will not have "title". The next `RelativeLayout` and its children are a little tricky. The `TextView`(`@id/title`) is the real title view, and other views (`LinearLayout`, `View`) are padding. In this "flexible space" pattern, `TextView`'s text should move and its font size should change, so it needs additional space. We'll achieve these animations by animate `TextView` itself, so paddings should be outside the `TextView`. To confirm other attributes, please see `res/layout/activity_flexiblespacetoolbarscrollview.xml` in the example app. ### Initialization At first, set the Toolbar as the ActionBar and show "homeAsUp" button. ```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_flexiblespacetoolbarscrollview); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); getSupportActionBar().setDisplayHomeAsUpEnabled(true); ``` And get the Activity's title and set it to the `TextView` which has the ID `@id/title`. ```java mTitleView = (TextView) findViewById(R.id.title); mTitleView.setText(getTitle()); setTitle(null); ``` And initialize other views and fields. ```java private View mFlexibleSpaceView; private View mToolbarView; private TextView mTitleView; private int mFlexibleSpaceHeight; @Override protected void onCreate(Bundle savedInstanceState) { // Codes that are already explained above are omitted mFlexibleSpaceView = findViewById(R.id.flexible_space); mToolbarView = findViewById(R.id.toolbar); final ObservableScrollView scrollView = (ObservableScrollView) findViewById(R.id.scroll); scrollView.setScrollViewCallbacks(this); mFlexibleSpaceHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_height); int flexibleSpaceAndToolbarHeight = mFlexibleSpaceHeight + getActionBarSize(); findViewById(R.id.body).setPadding(0, flexibleSpaceAndToolbarHeight, 0, 0); mFlexibleSpaceView.getLayoutParams().height = flexibleSpaceAndToolbarHeight; } ``` You should also add `implements ObservableScrollViewCallbacks` to the Activity and implement those methods as always. ### Animation We use `onScrollChanged()` to create animation. We must write following codes: * Translate the flexible space view * Translate and scale the title view #### Translate the flexible space view This is easy, just translate it using `scrollY`: ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY); } ``` #### Scale the title view How do you change the size of the font? At first I tried just changing the size of the font, but it didn't work. It should be scaled. The scale should change from `1` to `1.x`. You can change `.x` to fit your app. In this case, I used the height of the flexible space and the height of the Toolbar. This calculates the maximum `.x`: ```java float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight(); ``` The scale (we call `.x` part as "scale" from here) should change between 0 to `maxScale`, so it can be written as follows. ```java // scrollY should be limited. int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight); // When scrollY is 0, scale equals to maxScale. // When scrollY reaches to mFlexibleSpaceHeight, scale will be 0. float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight; ``` When scaling the view, we need to set the center point of scaling. You can handle this by using `pivotX` and `pivotY`, and we should set them to `(0, 0)` like this image: ![](../images/basic_4.png) We will set `pivotX` and `pivotY` first, and then change the scale: ```java // Pivot the title view to (0, 0) ViewHelper.setPivotX(mTitleView, 0); ViewHelper.setPivotY(mTitleView, 0); // Scale the title view ViewHelper.setScaleX(mTitleView, 1 + scale); ViewHelper.setScaleY(mTitleView, 1 + scale); ``` #### Translate the title view And about `translationY`, this is a little complicated. Let's see the following picture. ![](../images/basic_5.png) The minimum `translationY` is obviously 0, and we want to know the maximum `translationY`. As we can see in the picture, the maximum `translationY` can be calculated with `ht1 + hf - ht2`, so we can write like this: ```java int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale)); ``` And we should vary this value using `scrollY`. `scrollY` should be limited and it's already calculated as `adjustedY`: ```java int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight); ViewHelper.setTranslationY(mTitleView, titleTranslationY); ``` Finally, we've finished translation and scaling. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY); // Calculate scale int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight); float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight(); float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight; // Pivot the title view to (0, 0) ViewHelper.setPivotX(mTitleView, 0); ViewHelper.setPivotY(mTitleView, 0); // Scale the title view ViewHelper.setScaleX(mTitleView, 1 + scale); ViewHelper.setScaleY(mTitleView, 1 + scale); // Translate the title view int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale)); int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight); ViewHelper.setTranslationY(mTitleView, titleTranslationY); } ``` #### Adjust the initial state of the title It's almost finished, but maybe you will notice that when the screen is launched, the title is located at the top of the screen. It should be located at the bottom of the header view area and have larger font. This is because `onScrollChanged()` is not called. You can fix that by calling `onScrollChanged()` just after the views are laied out. And you can handle this "laid out" event by using `ViewTreeObserver#addOnGlobalLayoutListener()`. ```java @Override protected void onCreate(Bundle savedInstanceState) { // Other initialization codes are omitted ViewTreeObserver vto = mTitleView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } updateFlexibleSpaceText(scrollView.getCurrentScrollY()); } }); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { updateFlexibleSpaceText(scrollY); } private void updateFlexibleSpaceText(scrollY) { // Original animation codes are omitted } ``` You can replace the following `ViewTreeObserver` codes ```java ViewTreeObserver vto = mTitleView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } updateFlexibleSpaceText(scrollView.getCurrentScrollY()); } }); ``` to this: ```java ScrollUtils.addOnGlobalLayoutListener(mTitleView, new Runnable() { @Override public void run() { updateFlexibleSpaceText(scrollView.getCurrentScrollY()); } }); ``` That's all! [Next: Flexible space with image »](../../docs/basic/flexible-space-with-image.md) ================================================ FILE: docs/basic/flexible-space-with-image.md ================================================ # Flexible space with image This topic describes how to create flexible space with image, which are implemented in the following examples. * FlexibleSpaceWithImageListViewActivity * FlexibleSpaceWithImageRecyclerViewActivity * FlexibleSpaceWithImageScrollViewActivity First, please check the "[Flexible space on the Toolbar](../../docs/basic/flexible-space-toolbar.md)" tutorial, if you haven't. --- ## Using ScrollView ### Layout with ScrollView #### Basic structure ```xml ``` The root `FrameLayout` is used for moving children separately. `ImageView`(`@id/image`) is the image that will be translated with parallax effect. `View`(`@id/overlay`) is a overlay view as the name suggests. If you try this Activity in the demo app, you can see the image is fading in and out. This view overlaps with the image and its opacity is changed by scroll position. `LinearLayout` and its chlidren are the real title views. You would have read the former tutorial, so I will not explain it so much. `FloatingActionButton` is a widget from the simple and awesome [FloatingActionButton](https://github.com/makovkastar/FloatingActionButton) library. But this is optional, so you can remove it if you are not going to place any buttons. I added it just because I think it's a very symbolic widget of the Material Design and some of you might be interested in it. To confirm other attributes, please see `res/layout/activity_flexiblespacewithimagescrollview.xml` in the example app. ### Initialization Most of the codes are easy and not related to this pattern. Just write the following initialization codes: Copy the title to the title view (`TextView`) and set null to the original title: ```java mTitleView.setText(getTitle()); setTitle(null); ``` Get the dimension values and save them to fields (to simplify animation codes): ```Java mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); mFlexibleSpaceShowFabOffset = getResources().getDimensionPixelSize(R.dimen.flexible_space_show_fab_offset); mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); mActionBarSize = getActionBarSize(); ``` Get the views which has ID to fields (to simplify animation codes), and initialize them if necessary: ```java mImageView = findViewById(R.id.image); mOverlayView = findViewById(R.id.overlay); mScrollView = (ObservableScrollView) findViewById(R.id.scroll); mScrollView.setScrollViewCallbacks(this); mTitleView = (TextView) findViewById(R.id.title); mFab = findViewById(R.id.fab); ``` Although this is not so related to the scroll animation, you should scale the floating action button (FAB) to 0 in `onCreate()`, because we'd like to hide it at the beginning and gradually show (scale) it by scrolling. ```java ViewHelper.setScaleX(mFab, 0); ViewHelper.setScaleY(mFab, 0); ``` You should also add `implements ObservableScrollViewCallbacks` to the Activity and implement those methods as always. ### Animation We use `onScrollChanged()` to create animation. We'll write the following codes: * Translate the overlay view and the image view * Change the alpha of the overlay view * Translate and scale the title view * Translate the FAB * Show/hide the FAB Let's see one by one. #### Translate the overlay view and the image view As we implemented in the former tutorials, to move `ImageView` which is outside the ScrollView, we must use `-scrollY` and divide it by 2 to create "parallax" effect. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { float flexibleRange = mFlexibleSpaceImageHeight - mActionBarSize; int minOverlayTransitionY = mActionBarSize - mOverlayView.getHeight(); ViewHelper.setTranslationY(mOverlayView, ScrollUtils.getFloat(-scrollY, minOverlayTransitionY, 0)); ViewHelper.setTranslationY(mImageView, ScrollUtils.getFloat(-scrollY / 2, minOverlayTransitionY, 0)); ``` Although we want to move the overlay view with the image, we don't have to make the scroll speed of the overlay to the same as the image view. So translate `mOverlayView` to `-scrollY` (not `-scrollY / 2`). #### Change the alpha of the overlay view Calculating the alpha value is easy, just convert the `scrollY` to range between 0 and 1. To do this, we divide `scrollY` by `flexibleRange` (which we assigned above), and limit the value range from 0 to 1 by using `ScrollUtils.getFloat()`. ```java ViewHelper.setAlpha(mOverlayView, ScrollUtils.getFloat((float) scrollY / flexibleRange, 0, 1)); ``` #### Translate and scale the title view This is almost the same as the "Flexible space on the Toolbar" pattern. The differences are how to calculate the scale and the translationY. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { // Codes that are already explained above are omitted float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); ViewHelper.setPivotX(mTitleView, 0); ViewHelper.setPivotY(mTitleView, 0); ViewHelper.setScaleX(mTitleView, scale); ViewHelper.setScaleY(mTitleView, scale); int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale); int titleTranslationY = maxTitleTranslationY - scrollY; ViewHelper.setTranslationY(mTitleView, titleTranslationY); ``` #### Translate the FAB Translating the FAB is actually not related to this topic, but I'll explain for your reference. The basic idea is to change the `translationY` property of the FAB, but on pre-Honeycomb devices, this doesn't work when you use `setOnClickListener`. To fix the issue, we'll set the margins of the FrameLayout and lay it out again by calling `requestLayout()`. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { // Codes that are already explained above are omitted int maxFabTranslationY = mFlexibleSpaceImageHeight - mFab.getHeight() / 2; float fabTranslationY = ScrollUtils.getFloat( -scrollY + mFlexibleSpaceImageHeight - mFab.getHeight() / 2, mActionBarSize - mFab.getHeight() / 2, maxFabTranslationY); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { // On pre-honeycomb, ViewHelper.setTranslationX/Y does not set margin, // which causes FAB's OnClickListener not working. FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFab.getLayoutParams(); lp.leftMargin = mOverlayView.getWidth() - mFabMargin - mFab.getWidth(); lp.topMargin = (int) fabTranslationY; mFab.requestLayout(); } else { ViewHelper.setTranslationX(mFab, mOverlayView.getWidth() - mFabMargin - mFab.getWidth()); ViewHelper.setTranslationY(mFab, fabTranslationY); } ``` The expression `- mFab.getHeight() / 2` in the calculation of `maxFabTranslationY` means that the half of the FAB overlaps to the image. And about the `fabTranslationY` calculation, you might think that the expression `mActionBarSize - mFab.getHeight() / 2` for the min value is meaningless, but this is required when you scroll the view fast. Because if it scrolls faster than the FAB scaling to 0, it looks as if it just moved away. #### Show/hide the FAB Showing or hiding the FAB is easy. If the translationY of the FAB exceeds the threshold, then hide it. Otherwise, show it. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { // Codes that are already explained above are omitted if (fabTranslationY < mFlexibleSpaceShowFabOffset) { hideFab(); } else { showFab(); } } ``` `hideFab()` and `showFab()` methods can be implemented like this: ```java private boolean mFabIsShown; private void showFab() { if (!mFabIsShown) { ViewPropertyAnimator.animate(mFab).cancel(); ViewPropertyAnimator.animate(mFab).scaleX(1).scaleY(1).setDuration(200).start(); mFabIsShown = true; } } private void hideFab() { if (mFabIsShown) { ViewPropertyAnimator.animate(mFab).cancel(); ViewPropertyAnimator.animate(mFab).scaleX(0).scaleY(0).setDuration(200).start(); mFabIsShown = false; } } ``` We need a state variable to indicate whether the FAB is shown or not. That's all. [Next: Filling gap on top of the Toolbar »](../../docs/basic/filling-gap.md) ================================================ FILE: docs/basic/index.md ================================================ # Basic techniques This section explains the basic scrolling techniques. 1. [Show and hide the ActionBar](../../docs/basic/show-hide-action-bar.md) 1. [Translating the Toolbar](../../docs/basic/translating-toolbar.md) 1. [Parallax image](../../docs/basic/parallax-image.md) 1. [Sticky header](../../docs/basic/sticky-header.md) 1. [Flexible space on the Toolbar](../../docs/basic/flexible-space-toolbar.md) 1. [Flexible space with image](../../docs/basic/flexible-space-with-image.md) 1. [Filling gap on top of the Toolbar](../../docs/basic/filling-gap.md) [Next: Show and hide the ActionBar »](../../docs/basic/show-hide-action-bar.md) ================================================ FILE: docs/basic/parallax-image.md ================================================ # Parallax image This topic describes how to create parallax effect, which are implemented in the following examples. * ParallaxToolbarScrollViewActivity * ParallaxToolbarListViewActivity --- ## Overview In this topic, "parallax" means the following layout and behavior: * The layout has an image on the top of the layout. * The image will move with half the speed of that of the ScrollView. * ScrollView itself has a big padding, which is like a "window" to see the image. To make the image "parallax", we need to do some tricks on the layout. `ObservableScrollView` and `ObservableListView` are a little different around handling paddings. I'll explain from `ObservableScrollView`. --- ## ScrollView ### Layout #### Basic structure At first, let's see the following basic structure of the layout. ```xml ``` Please note that in this XML, I intentionally omitted attributes(`android:XXX`) and package name (`com.github.XXX`) for readability. ##### Why should we use FrameLayout? As you can see on the example app, Toolbar is overlaid to the ObservableScrollView. To do this, we need to use `FrameLayout` or `RelativeLayout`. ##### What's inside of the ObservableScrollView? `ObservableScrollView` extends `ScrollView`, so it can have no more than 1 child. However we need more children, so placing a `ViewGroup` as the child of `ObservableScrollView` is required. `ImageView` is the `View` which is going to have "parallax" effect. You can replace it to other type of `View` if you want. `TextView` is the main content of the screen, you can also replace it to other type of `View`. `View` is an "anchor", I'll explain it later. We need to move the content and the image separately, so the parent of them — child of `ObservableScrollView` — should be `RelativeLayout` or `FrameLayout`. This time, we use `RelativeLayout` for that purpose. #### Don't move the content when its parent is scrolled How do you place the main content (a `TextView` for this time) under the `ImageView`? Suppose you define the position with `android:layout_below` attribute: ```xml ``` We need to move `ImageView` but if we do this, the `TextView` moves with the same speed as `ImageView` because its layout is defined with `android:layout_below="@id/image"`. So we should define the `TextView`'s position with another "anchor" view: ```xml ``` With this anchor view, we can move only `ImageView`. The anchor `View` and `TextView` will remain in their position. #### Set the content color explicitly We need to set the background color of the main content explicitly, because the image is underlying. ```xml ``` #### Complete the layout Now set the rest of the attributes of the layout, such as `android:layout_width`, `android:padding`, etc. Please see the folloing codes for details. * `res/layout/activity_parallaxtoolbarscrollview.java` ### Animation #### Basic structure of Activity We use `AppCompatActivity` of the v7 appcompat library for the base `Activity` class, and implement `ObservableScrollViewCallbacks`. ```java public class ParallaxToolbarScrollViewActivity extends AppCompatActivity implements ObservableScrollViewCallbacks { ``` #### Initialize views Then initialize the views like this. ```java // Fields private View mImageView; private View mToolbarView; private ObservableScrollView mScrollView; private int mParallaxImageHeight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_parallaxtoolbarscrollview); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); mImageView = findViewById(R.id.image); mToolbarView = findViewById(R.id.toolbar); mToolbarView.setBackgroundColor( ScrollUtils.getColorWithAlpha(0, getResources().getColor(R.color.primary))); mScrollView = (ObservableScrollView) findViewById(R.id.scroll); mScrollView.setScrollViewCallbacks(this); mParallaxImageHeight = getResources().getDimensionPixelSize( R.dimen.parallax_image_height); } ``` The Toolbar should be transparent at the beginning, so set the alpha of the background color to 0 by using the `ScrollUtils` utility class. This is optional and you can omit this if you don't use the Toolbar. #### Change the position on scrolling We use `onScrollChanged()` method, one of `ObservableScrollViewCallbacks`, to animate the view. What we need to do in this method is: 1. translate the `ImageView` in Y-axis using `scrollY` parameter 1. change the alpha value of the background color of the `Toolbar` using `scrollY` parameter The second one is optional. You can omit this if you don't use the Toolbar. ##### Translate the ImageView Just set the `translateY` property to half of `scrollY`. If you want to change the "depth" of the parallax effect, adjust this value (`scrollY / 2`). ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { ViewHelper.setTranslationY(mImageView, scrollY / 2); } ``` ##### Change the alpha of the Toolbar background color We should change the alpha value of the background color of the Toolbar, so we can write like this. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { int baseColor = getResources().getColor(R.color.primary); float alpha = 0; // TODO Fix this value mToolbarView.setBackgroundColor(ScrollUtils.getColorWithAlpha(alpha, baseColor)); ``` Changing alpha is a little complicated, so I wrote `float alpha = 0` temporarily. Let's confirm the conditions of the colors and fix the `alpha` value. * If the `ObservableScrollView` is not scrolled, Toolbar is transparent. (If `scrollY` equals to 0, alpha of the Toolbar is 0.) * If the `ObservableScrollView` is scrolled, it becomes opaque gradually, and when it's scrolled to a certain point, Toolbar is completely opaque. (If `scrollY` equals to `mParallaxImageHeight`, alpha of the Toolbar is 1.) We need to express these conditions as a formula. `alpha` should changes from 0 to 1, but `scrollY` changes from 0 to thousands, so `scrollY` should be scaled. We should divide `scrollY` with `mParallaxImageHeight` because when `alpha` becomes 1, `scrollY` should be equal to `mParallaxImageHeight`. ```java float alpha = (float) scrollY / mParallaxImageHeight; ``` Please note that `scrollY` and `mParallaxImageHeight` are both type `int`, so you need to cast one of them to `float`. But how is it when `scrollY` becomes more than `mParallaxImageHeight`? Let's simulate the result values: | `scrollY` | `mParallaxImageHeight` | `alpha` | Valid alpha? | | ---------:| ----------------------:| ---------:| ------------- | | 0 | 300 | 0 | Valid | | 150 | 300 | 0.5 | Valid | | 300 | 300 | 1.0 | Valid | | 450 | 300 | _**1.5**_ | _**Invalid**_ | As we can see in the 4th row (`scrollY == 450`), we need to control `alpha` so that it will not exceed 1.0. This time we use `Math.min()` to limit the value from 0 to 1. ```java float alpha = Math.min(1, (float) scrollY / mParallaxImageHeight); ``` Now it's done. `onScrollChanged` will be like this: ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { int baseColor = getResources().getColor(R.color.primary); float alpha = Math.min(1, (float) scrollY / mParallaxImageHeight); mToolbarView.setBackgroundColor(ScrollUtils.getColorWithAlpha(alpha, baseColor)); ViewHelper.setTranslationY(mImageView, scrollY / 2); } ``` #### Restore scroll state We need to handle one more thing: restoring scroll state when the Activity is restored. `ObservableScrollView` itself stores its scroll position, so you just need to update the view in the `onRestoreInstanceState()` method. ```java @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); onScrollChanged(mScrollView.getCurrentScrollY(), false, false); } ``` --- ## ListView Let's see the difference of the implementation between ListView version and ScrollView version. ### Layout #### Basic structure ```xml ``` We use `FrameLayout` to the root view, just like ScrollView pattern. `FrameLayout` can be used to move children views separately. `ImageView` is the view which should have "parallax" effect. The next `View` is used for different purpose from that of ScrollView. I'll explain this later. #### Why do we use different layout? Unlike ScrollView, ListView cannot have children views, so `ImageView` should be outside of the scrollable view (ListView) and we should move the `ImageView` manually. #### How do we place ImageView and ListView? `ImageView` is going to be scrolled slower than ListView (because we're going to make "parallax" effect), so `ImageView` should be underneath the ListView. Otherwise, the bottom of the `ImageView` overlaps with the top of the ListView. Also, ListView should have a big padding at the top of the ListView to make `ImageView` visible. We achieve this by adding a transparent header view to the ListView. #### Why do we need a View? As I mentioned above, ListView should have a transparent header, so its background color should be also transparent. But if we do this, not only the header view but also the items of the ListView become transparent. ![](../images/basic_1.png) To avoid this, we set a dummy background view under the ListView. ### Animation #### Basic structure of Activity It's same as `ParallaxToolbarScrollViewActivity` example. ```java public class ParallaxToolbarListViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { ``` #### Initialize views Like ScrollView, initialize the `ObservableListView`, `ImageView`, Toolbar, etc. And as I explained, ListView should have a header view. ```java private View mImageView; private View mToolbarView; private View mListBackgroundView; private ObservableListView mListView; private int mParallaxImageHeight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_parallaxtoolbarlistview) ; setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); mImageView = findViewById(R.id.image); mToolbarView = findViewById(R.id.toolbar); mToolbarView.setBackgroundColor(ScrollUtils.getColorWithAlpha(0, getResources().getColor(R.color.primary))); mParallaxImageHeight = getResources().getDimensionPixelSize(R.dimen.parallax_image_height); mListView = (ObservableListView) findViewById(R.id.list); mListView.setScrollViewCallbacks(this); // Set padding view for ListView. This is the flexible space. View paddingView = new View(this); AbsListView.LayoutParams lp = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, mParallaxImageHeight); paddingView.setLayoutParams(lp); paddingView.setClickable(true); mListView.addHeaderView(paddingView); setDummyData(mListView); mListBackgroundView = findViewById(R.id.list_background); ``` Note that following code is necessary to disable header view's list selector effect. ```java paddingView.setClickable(true); ``` `setDummyData()` should be replaced to appropriate data population codes. #### Change the position on scrolling ##### Translate the ImageView We use `onScrollChanged` method to translate views. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } ``` Basically, we should just set the translateY property to half of `scrollY`. But be careful, unlike ScrollView, when `scrollY` gets larger then `translateY` of `ImageView` should become smaller because `ImageView` is not a child of the ListView. So we should use `-scrollY / 2` as `translationY` (and you can adjust "`/ 2`" if you want). ```java ViewHelper.setTranslationY(mImageView, -scrollY / 2); ``` ##### Translate the background view The background should move with ListView, but it should have an offset `mParallaxImageHeight` so we can write like this: ```java ViewHelper.setTranslationY(mListBackgroundView, mParallaxImageHeight - scrollY); ``` But how is it when `scrollY` becomes more than `mParallaxImageHeight`? Let's simulate the result values: | `mParallaxImageHeight` | `scrollY` | `mParallaxImageHeight - scrollY` | TranslationY of `mListViewBackgroundView` should be | | ----------------------:| ---------:|---------------------------------:|----------------------------------------------------:| | 300 | 0 | 300 | 300 | | 300 | 150 | 150 | 150 | | 300 | 300 | 0 | 0 | | 300 | 450 | -150 | 0 | The 4th `mParallaxImageHeight - scrollY` becomes negative and it's invalid. So use `Math.max()` to avoid this. ```java ViewHelper.setTranslationY(mListBackgroundView, Math.max(0, -scrollY + mParallaxImageHeight)); ``` That's all. The rest of the codes are the same as `ObservableScrollView` example. [Next: Sticky header »](../../docs/basic/sticky-header.md) ================================================ FILE: docs/basic/show-hide-action-bar.md ================================================ # Show and hide the ActionBar This topic describes how to show and hide the ActionBar, which are implemented in the following examples. * ActionBarControlGridViewActivity * ActionBarControlListViewActivity * ActionBarControlRecyclerViewActivity * ActionBarControlScrollViewActivity * ActionBarControlWebViewActivity --- ## Using the basic callbacks Suppose you've already checked the "[Quick start](../../docs/quick-start/index.md)" section, you wouldn't know the meaning of the codes yet. So at first, let's see how those codes work. ### ObservableScrollViewCallbacks In the quick start guide, you wrote the implementation of `ObservableScrollViewCallbacks` (following methods). ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } ``` These are the methods of `ObservableScrollViewCallbacks` interface and all the `Observable*View`s can handle this callbacks. ### onScrollChanged callback This is called when the scroll change events occurred. This won't be called just after the view is laid out, so if you'd like to initialize the position of your views with this method, you should call this manually or invoke scroll as appropriate. You would expect it to be called after `onCreate`, but `ListView` (or other views) does not call back scroll change event, so `Observable*Views` cannot call this. Therefore you should write like this: ```java onScrollChanged(mListView.getCurrentScrollY(), false, false); ``` I know it's a bad pattern to call the "callback" methods from us, they should be called by the library when they should be. However, we cannot improve this because this behavior depends on the view classes in the Android SDK. ### onDownMotionEvent callback This is called when the down motion events occur. This can be useful if you'd like to know when the touch (or dragging) has begun. ### onUpOrCancelMotionEvent callback This is called when the dragging ended or canceled. This is useful when you move some views when the scroll ends: showing/hiding a view, sliding a view to the anchor point, etc. ## How it works: ActionBar animation As I explained in the quick start section, the main animation code is in the `onUpOrCancelMotionEvent`. What we want to do is: 1. to hide the ActionBar when we swipe up the view, because we want to see the contents. 1. to show the ActionBar when we swipe down the view, because we want to tap a button on the ActionBar (it could be sharing the contents or going back to the former screen, for example). Either way, we should get the direction of scrolling when the dragging ends. `onUpOrCancelMotionEvent` callback has a `ScrollState` parameter. This parameter indicates the direction of the scroll, so we can write like this: ```java public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (scrollState == ScrollState.UP) { // TODO show or hide the ActionBar } else if (scrollState == ScrollState.DOWN) { // TODO show or hide the ActionBar } } ``` When you move your finger from the bottom of the screen to the top (swiping up), the state will be `ScrollState.UP`. So we can write `ActionBar#hide()` in this condition. And this event occurs every time you scroll the view, so if the `ActionBar` is already hidden, you don't have to hide it anymore. Now you know how to handle the other direction. Show the ActionBar when `ScrollState.DOWN` is passed to the callback. ```java ActionBar ab = getSupportActionBar(); if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); } } else if (scrollState == ScrollState.DOWN) { if (!ab.isShowing()) { ab.show(); } } ``` ## Conclusion If you'd like to animate the ActionBar, you should write animation codes in the `onUpOrCancelMotionEvent` callback. Also, we used `ObservableListView` in the quick start, but in this pattern all types of `Observable*View`s have the same behavior, so you can write exactly the same. [Next: Translating the Toolbar »](../../docs/basic/translating-toolbar.md) ================================================ FILE: docs/basic/sticky-header.md ================================================ # Sticky header This topic describes how to keep header on top of the screen, which are implemented in the following examples. * StickyHeaderListViewActivity * StickyHeaderRecyclerViewActivity * StickyHeaderScrollViewActivity * StickyHeaderWebViewActivity --- ## Overview This is a complex version of [Toolbar translation pattern](../../docs/basic/translating-toolbar.md). We add a features to keep the half of the header view to the top of the screen. And this time I'll explain using ScrollView. Replacing it to other type of scrollable views are not so difficult. ## Using ScrollView ### Layout with ScrollView Let's look at the layout file as always. Here is the basic structure of StickyHeader pattern with ScrollView. This is a little difficult than Toolbar's one. ```xml ``` In Toolbar translation pattern, we used only `ObservableScrollView` and `Toolbar` in `FrameLayout`. This time, we need to make each views more complex. #### Create header space for ScrollView with twice the size of ActionBar At the initial state of views, ScrollView needs to have a header view with twice the size of the ActionBar. The half of this header view will be "sticky". So we simply add 2 `View`s with the height `?attr/actionBarSize` above the `TextView`. You can also add just 1 `View` with a certain size with `dp`, but it's better to use `?attr/actionBarSize` because it has multiple values for several size of screens, screen rotation and OS versions, and using the standard size is good for users. Another way to achieve this, is to set the height of the `View` programmatically. You can resolve the value of `?attr/actionBarSize` in `Activity#onCreate()`, multiply it by 2 and set it to the `View`. And please note that `TextView` is the real content of the ScrollView, so you can replace it to other view if you want. #### Create sticky part for Toolbar Toolbar is replaced to `LinearLayout`, and it contains a Toolbar and a `TextView`. `TextView` will be the "sticky" view. You can replace it to some complex views. ### Animate the views with ScrollView callbacks This time, we use two callbacks: `onScrollChanged()` and `onUpOrCancelMotionEvent()` to animate views. We are going to implement the following animation. 1. Move the Toolbar and the sticky view (we call "header views") when the ScrollView is scrolled. 1. When we scroll the ScrollView, the Toolbar will go out of the screen. But when we scroll it more, sticky view must keep its position to the top of the screen. 1. When the Toolbar is not completely hidden and we stop scrolling (touch up the ScrollView), * the Toolbar will be shown completely, if we were swiping down. * the Toolbar will be hidden completely, if we were swiping up. 1. When we swipe down the ScrollView and touch up, the header view should come out immediately. Sometimes it's called "Quick Return" pattern. #### Move the header view when ScrollView is scrolled Override the `onScrollChanged()`, and implement some codes with the condition `if (dragging)`. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (dragging) { // TODO implement the rest of the codes //} else { // ScrollView is scrolled by inertia } } ``` This is because we want to move views only when it is dragged. Without this, we cannot achieve the 3rd condition above: showing or hiding the Toolbar automatically when the scroll ended. Next step, implement the header view translation. At first, create a field with name `mHeaderView`, and initialize it in `onCreate()`: ```java mHeaderView = findViewById(R.id.header); ``` When the scrollY parameter gets increased, the translationY of `mHeaderView` should decrease. So we can write like this: ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (dragging) { ViewHelper.setTranslationY(mHeaderView, -scrollY); } } ``` #### Sticky view must keep its position to the top of the screen The header view will disappear completely, and this is not what we want. `mHeaderView` should stop after moving the height of Toolbar. ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (dragging) { int toolbarHeight = mToolbarView.getHeight(); ViewHelper.setTranslationY(mHeaderView, Math.max(-toolbarHeight, -scrollY)); } } ``` You can see the sticky view keeping its position to the top of the screen. #### When Toolbar is not completely hidden, show or hide it completely To do this, we should implement `onUpOrCancelMotionEvent`. If we swipe down, Toolbar should be shown, and if we swipe up, Toolbar should be hidden. ```java @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (scrollState == ScrollState.DOWN) { showToolbar(); } else if (scrollState == ScrollState.UP) { hideToolbar(); } } ``` But when we swipe up and scrolled less than Toolbar's height, hiding the Toolbar makes white space around the top of the ScrollView. So we should show the Toolbar if `scrollY` is less than Toolbar's height. ```java @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (scrollState == ScrollState.DOWN) { showToolbar(); } else if (scrollState == ScrollState.UP) { int toolbarHeight = mToolbarView.getHeight(); int scrollY = mScrollView.getCurrentScrollY(); if (toolbarHeight <= scrollY) { hideToolbar(); } else { showToolbar(); } } } ``` And sometimes `scrollState` becomes `STOP` (or `null`). If it becomes such values, the header view stops halfway. To avoid this behavior, write `else` clause. ```java @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (scrollState == ScrollState.DOWN) { showToolbar(); } else if (scrollState == ScrollState.UP) { int toolbarHeight = mToolbarView.getHeight(); int scrollY = mScrollView.getCurrentScrollY(); if (toolbarHeight <= scrollY) { hideToolbar(); } else { showToolbar(); } } else { // Even if onScrollChanged occurs without scrollY changing, toolbar should be adjusted if (!toolbarIsShown() && !toolbarIsHidden()) { // Toolbar is moving but doesn't know which to move: // you can change this to hideToolbar() showToolbar(); } } } ``` Then write the unimplemented methods. Unlike Toolbar translation pattern, we use `ViewPropertyAnimator.animate()` because it's simple and we don't have to change the height of views. ```java private boolean toolbarIsShown() { return ViewHelper.getTranslationY(mHeaderView) == 0; } private boolean toolbarIsHidden() { return ViewHelper.getTranslationY(mHeaderView) == -mToolbarView.getHeight(); } private void showToolbar() { float headerTranslationY = ViewHelper.getTranslationY(mHeaderView); if (headerTranslationY != 0) { ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewPropertyAnimator.animate(mHeaderView).translationY(0).setDuration(200).start(); } } private void hideToolbar() { float headerTranslationY = ViewHelper.getTranslationY(mHeaderView); int toolbarHeight = mToolbarView.getHeight(); if (headerTranslationY != -toolbarHeight) { ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewPropertyAnimator.animate(mHeaderView).translationY(-toolbarHeight).setDuration(200).start(); } } ``` Once `ViewPropertyAnimator.animate()` is called, animation will be running in the next 200ms. And if the next animation(`showToolbar()` or `hideToolbar()`) is requested while the animation is running, the current animation should be canceled. Therefore we call `ViewPropertyAnimator.animate(mHeaderView).cancel()` before calling `start()`. #### When swiping up, header view should scroll It's almost completed, and if you think it's OK, you don't have to write the following codes. When we scroll so much and swip down little, the header view will be shown. And after that, when we drag ScrollView to upper side, I think that the header view should move with ScrollView, but it doesn't. So we make the header view to scroll even when `scrollY` is larger than the Toolbar's height. To do this, we just calculate the distance from the first touch point and the current point. And the distance from the first touch point become larger than Toolbar's height, the header view should not scroll any longer. ```java // Add a field to keep the first scrollY private int mBaseTranslationY; @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (dragging) { int toolbarHeight = mToolbarView.getHeight(); if (firstScroll) { // Add this if clause float currentHeaderTranslationY = ViewHelper.getTranslationY(mHeaderView); if (-toolbarHeight < currentHeaderTranslationY) { mBaseTranslationY = scrollY; } } // Change -scrollY to -(scrollY - mBaseTranslationY) float headerTranslationY = Math.max(-toolbarHeight, -(scrollY - mBaseTranslationY)); ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewHelper.setTranslationY(mHeaderView, headerTranslationY); } } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { // Should be cleared when scroll ends mBaseTranslationY = 0; ``` It's almost done, but sometimes we can see a weird behavior: the header view leaves the top of the screen. ![](../../docs/images/basic_2.png) This is because `headerTranslationY` can become larger than 0, so it should be limited by using `Math.min()`. ```java float headerTranslationY = Math.min(0, Math.max(-toolbarHeight, -(scrollY - mBaseTranslationY)); ``` Now it's working, but don't you think it's a little complicated expression? Android-ObservableScrollView provides a small utility class `ScrollUtils`, and we can replace `Math.min(max, Math.max(min, value))` to `ScrollUtils.getFloat()`. ```java float headerTranslationY = ScrollUtils.getFloat(-(scrollY - mBaseTranslationY), -toolbarHeight, 0); ``` Of course, you can confirm the meaning of each parameters easily. Pressing `F1` key on `getFloat()` will show the javadoc window: ![](../../docs/images/basic_3.png) [Next: Flexible space on the Toolbar »](../../docs/basic/flexible-space-toolbar.md) ================================================ FILE: docs/basic/translating-toolbar.md ================================================ # Translating the Toolbar This topic describes how to translate the Toolbar, which are implemented in the following examples. * ToolbarControlBaseActivity * ToolbarControlGridViewActivity * ToolbarControlListViewActivity * ToolbarControlRecyclerViewActivity * ToolbarControlScrollViewActivity * ToolbarControlWebViewActivity --- ## About the Toolbar In this section we learn how to translate the Toolbar. Toolbar was introduced on Android 5.0, and you can also use it on pre-Lollipop devices by using [v7 appcompat library](http://developer.android.com/tools/support-library/features.html#v7-appcompat) of the Android Support Library package. ## Design of the examples The existing examples above, `ToolbarControlBaseActivity` has most of the codes to avoid writing duplicate codes. If you use one of them, you don't have to use this structure: extending Activity is not required to achieve this effect. ## Create layout file In this topic, we use `ObservableListView` and `Toolbar`, and wrap them with `FrameLayout`. `FrameLayout` and `RelativeLayout` are useful to translate views inside of it separately. ```xml ``` ## How to translate the Toolbar The basic idea about showing/hiding the Toolbar is exactly the same as the ActionBar. However, the Toolbar class does not provide any convinient methods like `show()` and `hide()` which the ActionBar class has. Therefore we should implement such methods to translate the Toolbar. Our goal is to make the following codes work: ```java @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (scrollState == ScrollState.UP) { if (toolbarIsShown()) { // TODO Not implemented hideToolbar(); // TODO Not implemented } } else if (scrollState == ScrollState.DOWN) { if (toolbarIsHidden()) { // TODO Not implemented showToolbar(); // TODO Not implemented } } } ``` ## Using NineOldAndroids Before we begin, you should confirm whether you're going to support pre-Honeycomb devices. To translate the Toolbar, we would like to use the [Property Animation APIs](http://developer.android.com/guide/topics/graphics/prop-animation.html) which are introduced in API level 11, so if you are going to support pre-Honeycomb devices, [JakeWharton/NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids/) might be useful (although it's marked as deprecated). In this project, all the examples use NineOldAndroids. So if you don't support pre-Honeycomb devices, please replace `ViewHelper.methodName(viewObject)` to `viewObject.methodName()`. ``` NineOldAndroids: ViewHelper.getTranslationY(mToolbar) Platform API: mToolbar.getTranslationY() ``` If you use NineOldAndroids, add an entry to the `dependencies` closure in your `build.gradle`: ```gradle dependencies { compile 'com.nineoldandroids:library:2.4.0' } ``` ## toolbarIsShown()/toolbarIsHidden() Now let's start from the easiest part. To avoid redundant translation, we need methods to check if the Toolbar is shown or hidden. With the property animation APIs (or NineOldAndroids), we just simply check the `translationY` property. ```java private boolean toolbarIsShown() { // Toolbar is 0 in Y-axis, so we can say it's shown. return ViewHelper.getTranslationY(mToolbar) == 0; } private boolean toolbarIsHidden() { // Toolbar is outside of the screen and absolute Y matches the height of it. // So we can say it's hidden. return ViewHelper.getTranslationY(mToolbar) == -mToolbar.getHeight(); } ``` ## Implement showToolbar()/hideToolbar() Next, let's implement methods to animate the Toolbar. Before thinking about details, write some pseudocodes to simplify the problem. To show or hide the Toolbar, we just need one method to move the Toolbar. ```java private void showToolbar() { moveToolbar(0); } private void hideToolbar() { moveToolbar(-mToolbar.getHeight()); } ``` This should work, if we implement the `moveToolbar` method correctly :) Most of the animation codes are combination of property value calculations, and I think it's very hard to keep these information in my brain or imagine correctly. And this approach is useful to implement the complex animation. ## Implement moveToolbar() Although we named the method `moveToolbar`, it's not everything we need to handle. In ActionBar examples, not only the ActionBar is moved but also the height of the view (`Observable*View`) is changed. And we need to implement this behavior for the Toolbar. To use the changing property values, we can use `ValueAnimator`. `ValueAnimator` has a callback method `onAnimationUpdate`, and we can get the animation progress from it. `ValueAnimator` itself does not animate anything, we need to animate something using a parameter of the callback. ```java ValueAnimator animator = ValueAnimator.ofFloat(0, 100).setDuration(200); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); // You can do whatever you want with the `value`. } }); ``` In the example code above, the local variable `value` changes from `0f` to `100f` in 200ms. In this case, we should change the `translationY` property of the Toolbar, and change the height of the `Observable*View` like this: ```java private void moveToolbar(float toTranslationY) { ValueAnimator animator = ValueAnimator.ofFloat(ViewHelper.getTranslationY(mToolbar), toTranslationY).setDuration(200); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float translationY = (float) animation.getAnimatedValue(); ViewHelper.setTranslationY(mToolbar, translationY); ViewHelper.setTranslationY((View) mScrollable, translationY); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) ((View) mScrollable).getLayoutParams(); lp.height = (int) -translationY + getScreenHeight() - lp.topMargin; ((View) mScrollable).requestLayout(); } }); animator.start(); } ``` The `translationY` local variable changes from `ViewHelper.getTranslationY(mToolbar)`( == current translationY) to `toTranslationY`. To translate the Toolbar, we just call `ViewHelper.setTranslationY()`. And to change the height of the wrapper view (`FrameLayout`), set the height value of `FrameLayout.LayoutParams` and update by calling `requestLayout()`. ## Avoid redundant animation We'd better check the current `translationY` value and if it's already equal to `toTranslationY`, stop the animation. ```java private void moveToolbar(float toTranslationY) { // Check the current translationY if (ViewHelper.getTranslationY(mToolbar) == toTranslationY) { return; } // Codes after that are omitted } ``` That's all. [Next: Parallax image »](../../docs/basic/parallax-image.md) ================================================ FILE: docs/contributor/_data.json ================================================ { "index": { "title": "For contributors" }, "ci": { "title": "CI" }, "update-website": { "title": "Update website" }, "release": { "title": "Release" } } ================================================ FILE: docs/contributor/ci.md ================================================ # CI Coming soon... ================================================ FILE: docs/contributor/index.md ================================================ # For contributors This section explains some operations for managing the project. 1. [CI](../../docs/contributor/ci.md) 1. [Update website](../../docs/contributor/update-website.md) 1. [Release](../../docs/contributor/release.md) ================================================ FILE: docs/contributor/release.md ================================================ # Release This is just a memo for me. ## Bump up the version Edit `gradle.properties` and commit. ``` VERSION_NAME=x.x.x ``` If you set the version suffix `-SNAPSHOT`, it will be handled as a snapshot. ## Commit changes Before the final confirmation and release, make sure that there are no uncommitted changes in your repository. ## Confirm test Check if all tests pass at your machine. ```sh ./gradlew clean :library:assemble :library:connectedCheck ``` ## Upload archives ### Set the credentials If this is the first time for uploading archives, you must write credentials to `~/.gradle/gradle.properties`. ``` NEXUS_USERNAME=xxxx NEXUS_PASSWORD=xxxx ``` ### Upload ```sh ./gradlew :library:uploadArchives ``` Or you can clean, test and upload at once. ```sh ./gradlew clean :library:assemble :library:connectedCheck :library:uploadArchives ``` ## Close the repo on Sonatype Open [Sonatype Nexus Professional](https://oss.sonatype.org/) on your browser, find your repo and close it. If there are no problems, repository will be staged to the URL like this. ``` https://oss.sonatype.org/content/repositories/TEMPORARY_REPO_NAME/GROUP/ARTIFACT_ID/VERSION/ARTIFACT_ID-VERSION.aar ``` You will receive an email with title "Nexus: Staging Completed", you can know the appropriate URL from the email. Set the URL to the `repositories` in your `build.gradle`, and sync. ```gradle repositories { maven { url uri('https://oss.sonatype.org/content/repositories/TEMPORARY_REPO_NAME/') } } ``` ## Release After that, click "Release" to promote. If it's processed successfully, you will receive an email with title "Nexus: Promotion Completed". It takes 3 or 4 hours to be synced to the Maven Central Repository. ## Create tag, update synced version and push If it's successfully published to the Maven Central Repository, * create a tag like `v1.5.0` * update the `SYNCED_VERSION_NAME` in `gradle.properties` * push the master branch and the tag to GitHub ================================================ FILE: docs/contributor/update-website.md ================================================ # Update website Coming soon... ================================================ FILE: docs/example/_data.json ================================================ { "index": { "title": "Try the example app" }, "google-play": { "title": "Download from Google Play" }, "wercker": { "title": "Download from wercker" }, "android-studio": { "title": "Build on Android Studio" }, "eclipse": { "title": "Build on Eclipse" } } ================================================ FILE: docs/example/android-studio.md ================================================ # Build on Android Studio This library and samples basically support Android Studio and Gradle. (Actually, I'm using them to develop this library.) If you're an Eclipse user, you can skip and go to the next topic. ## Prerequisites Please [check here](../../docs/reference/environment.md) to see if your enviroment satisfies the prerequisites for building the app. ## Instructions ### Get the source codes Get the source code of the library and example app, by cloning git repository or downloading archives. If you use git, execute the following command in your workspace directory. ``` $ git clone https://github.com/ksoichiro/Android-ObservableScrollView.git ``` If you are using Windows, try it on GitBash or Cygwin or something that supports git. ### Import the project to Android Studio 1. Select File > New > Import Project... from the menu. 1. Select the directory that is cloned. If you can't see your cloned directory, click "Refresh" icon and find it. 1. Android Studio will import the project and build it. This might take minutes to complete. Even when the project window is opened, wait until the Gradle tasks are finished and indexed. 1. Click "Run 'samples'" button to build and launch the app. Don't forget to connect your devices to your machine. ### Build and install using Gradle If you just want to install the app to your device, you don't have to import project to Android Studio. After cloning the project, connect your device to your machine, and execute the following command on the terminal. Mac / Linux / Git Bash, Cygwin on Windows: ```sh $ cd /path/to/Android-ObservableScrollView $ ./gradlew installDevDebug ``` Windows (Command prompt): ```sh > cd C:\path\to\Android-ObservableScrollView > gradlew installDevDebug ``` [Next: Build on Eclipse »](../../docs/example/eclipse.md) ================================================ FILE: docs/example/eclipse.md ================================================ # Build on Eclipse This library and samples basically support Android Studio and Gradle. Because they have strong power to handle dependencies and ability to configure flexibly, and this library and sample app depend on them. However, some of you might still want to build or debug the project on Eclipse. If you'd like to do that, please try the following instructions. Please note that with these instructions you could bulid project on Eclipse, but test codes, build types ('debug' or 'release') and product flavors are still not supported. ## Prerequisites Please [check here](../../docs/reference/environment.md) to see if your enviroment satisfies the prerequisites for building the app. ## Instructions ### Get the source codes Get the source code of the library and example app, by cloning git repository or downloading archives. If you use git, execute the following command in your workspace directory. ``` $ git clone https://github.com/ksoichiro/Android-ObservableScrollView.git ``` If you are using Windows, try it on GitBash or Cygwin or something that supports git. ### Define ANDROID_HOME environment variable If you haven't define the environment variable `ANDROID_HOME` yet, define it to indicate Android SDK root directory. ### Generate dependency codes for Eclipse Before trying to import projects to Eclipse, execute these command: ``` $ ./gradlew clean generateVersionInfoDebug generateEclipseDependencies ``` This will generate dependency codes from AAR files using Gradle wrapper and some metadata files (`.classpath`, `.project`, `project.properties`). ### Import projects to Eclipse and build app 1. Launch Eclipse. 1. Select `File` > `Import`. 1. Select `General` > `Existing Projects into Workspace` and click `Next`. * Warning: DO NOT `Android` > `Existing Android Code into Workspace`. 1. Click `Browse` and select project root directory (`Android-ObservableScrollView`). 1. Check `Search for nested projects`. 1. Select all projects and click next. 1. Some warning messages will be generated, but ignore them and wait until build finishes. ### Run the app 1. Confirm your device is connected. 1. Right click `observablescrollview-samples` and select `Run As` > `Android Application`. That's all! [Next: Basic techniques »](../../docs/basic/index.md) ================================================ FILE: docs/example/google-play.md ================================================ # Download from Google Play Click the following link to download the example app from Google Play. [![Get it on Google Play](https://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=com.github.ksoichiro.android.observablescrollview.samples2) Please note that the app on the Play Store is not always the latest version. If you'd like to install the latest one, * install it manually. * or if you are a wercker user, you can download the latest build artifact from wercker. [Next: Download from wercker »](../../docs/example/wercker.md) ================================================ FILE: docs/example/index.md ================================================ # Try the examples app To understand how it works, let's see the existing example app and check if there are some patterns you want to implement. 1. [Download from Google Play](../../docs/example/google-play.md) 1. [Download from wercker](../../docs/eaxmple/wercker.md) 1. [Build on Android Studio](../../docs/example/android-studio.md) 1. [Build on Eclipse](../../docs/example/eclipse.md) [Next: Download from Google Play »](../../docs/example/google-play.md) ================================================ FILE: docs/example/wercker.md ================================================ # Download from wercker [wercker](http://wercker.com/) is a CI service and this project uses wercker to provide the latest sample apk. If you're not interested in this, go to the next topic. ## Login to wercker At first, you need to be a member of [wercker](http://wercker.com/) and should login before download the app. ## Visit this repository Click the badge below to show this repository's builds. [![wercker status](https://app.wercker.com/status/8d1e27d9f4a662b25dbe70402733582b/m/master "wercker status")](https://app.wercker.com/project/bykey/8d1e27d9f4a662b25dbe70402733582b) ## Select the build Then select the commit link that you want to download. Note that green check mark in front of the link means successful builds and red ones are failure, and you can only download the app from the green ones. ![](../images/wercker_1.png) ## Open the last section Scroll the screen, and click anywhere in the "inspect build result" section to open it. ![](../images/wercker_2.png) ## Download the artifact Finally, you can download the apk file by clicking the `artifact.tar.gz` link. ![](../images/wercker_3.png) [Next: Build on Android Studio »](../../docs/example/android-studio.md) ================================================ FILE: docs/faq.md ================================================ # FAQ These are frequently asked questions from GitHub issues, emails I received from users, etc. ## Q. When do you implement the new sample? I'm waiting for it so long. ### A. Sorry and please help me if you could. First of all, I'm so grateful to all of you that you're interested in this project. And of course I'd like to respond to all of your request! But unfortunately, I don't have enough time to do that... If you're interested in implementing new samples or fixing bugs, please help me. (Pull requests are welcome!) ## Q. Does this library support Eclipse? ### A. Yes, it does partially. Please see [here](../docs/example/eclipse.md) for details. ## Q. Doesn't work! ### A. Please describe your issue as much as possible. As I wrote in [the contribution guideline](https://github.com/ksoichiro/Android-ObservableScrollView/blob/master/CONTRIBUTING.md), the library itself only provides the scroll information, and creating awesome scrolling effects depends deeply on how you use it: layout, offset calculation to animate views, etc. Therefore, if you find an issue, please describe not only the issue itself but also the related information like Activity name that has problems, operation to produce the issue, modified code (if any), etc. ## Q. Paths are too long to build! ### A. Move your projects upper to shorten the paths. On Windows environment, if you locate the project directory like `C:\Documents and Settings\user\workspace\Android-ObservableScrollView\`, Android Studio causes errors and fail to build the project because some of the paths in the output files are too long. If you see this kind of problem, please move the project directory to upper directory to shorten the paths. ## Q. Sample codes are too complex! ### A. Sorry, please help me refactor them... Yes, I know they're too complex as samples. I just aimed to show you that you can realize these kind of effects when you use this library. I'd very appreciate it if you help me refactor them. ## Q. Updates of the library in master branch seems not be synced to Maven Central. ### A. Sorry, please wait for the next release. I need to do following tasks to release the new version to the Maven Central, and it takes time, so please wait for the next release. If you're in a hurry, please send me an email. I'll release it as soon as possible. 1. Test the library, at least the tests on Travis CI should pass. 1. Check the compatibility for the past versions. 1. Release SNAPSHOT version to the Sonatype snapshot repository. 1. Release to the Sonatype repository. If it's successfully released, it will be synced to Maven Central in a couple of hours. 1. Update README to prompt to use the latest version. ## Q. Can I use this library with API level 8? ### A. It's not supported, but you can. By adding `tools:overrideLibrary` to `` tag, you can build this library with `android:minSdkVersion="8"`. ```xml ``` If you have other libraries to override, separate them with comma. ```xml ``` ================================================ FILE: docs/overview.md ================================================ # Overview Android-ObservableScrollView is a library to handle scroll position for Android apps, and contains lots of examples to demonstrate how this library works. However, creating awesome scrolling effects depends deeply on how you use it: layout, offset calculation to animate views, etc. This documentation describes how to install this library and apply to your application. ## Get started See [quick start](../docs/quick-start/index.md) section. ## See the complete examples You can try the example app, to see what this library can do. See [try the example app](../docs/example/index.md) section to know how to install the app. ## Learn from basics Now you must have seen the examples and setup your environments, then let's learn how to use it in your app. See [basic techniques](../docs/basic/index.md) section. ## Challenge complex and awesome techniques If you'd like to create complex, awesome scrolling animation using ViewPager or something, please check out [advanced techniques](../docs/advanced/index.md) section. ## If you're interested in improving this library... Please see the [contribution guideline](../CONTRIBUTING.md). Also, [for contributiors](../docs/contributor/index.md) section will be useful to understand / manage the entire project. ================================================ FILE: docs/quick-start/_data.json ================================================ { "index": { "title": "Quick start" }, "dependencies": { "title": "Dependencies" }, "layout": { "title": "Layout" }, "animation": { "title": "Animation codes" } } ================================================ FILE: docs/quick-start/animation.md ================================================ # Animation codes This time, we implement ActionBar animation using `AppCompatActivity` in the support library. ## Apply layout to the activity At first, let `Activity` extend the `AppCompatActivity` and set [the layout we wrote](../../docs/quick-start/layout.md) to it. ```java import android.support.v7.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ``` ## Initialize ObservableListView Add some initialization codes to `onCreate()`: ```java @Override protected void onCreate(Bundle savedInstanceState) { ObservableListView listView = (ObservableListView) findViewById(R.id.list); listView.setScrollViewCallbacks(this); } ``` You will see an error around `setScrollViewCallbacks(this)` because the Activity does not implement the required interface yet. So add `implements ObservableScrollViewCallbacks` to the Activity definition: ```java public class MainActivity extends AppCompatActivity implements ObservableScrollViewCallbacks { ``` Then implement required methods: ```java @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ``` Now we can handle the scroll events. ## Populate list data Before write codes to animate views, set data to ListView. ```java // Add these codes after ListView initialization ArrayList items = new ArrayList(); for (int i = 1; i <= 100; i++) { items.add("Item " + i); } listView.setAdapter(new ArrayAdapter( this, android.R.layout.simple_list_item_1, items)); ``` ## Animate with scroll events Finally, we can write the main code now. Add some code to show/hide the ActionBar in `onUpOrCancelMotionEvent` method for example. ```java @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); } } else if (scrollState == ScrollState.DOWN) { if (!ab.isShowing()) { ab.show(); } } } ``` `ScrollState` parameter indicates the direction of swiping, and this event will occur when you touch up (or cancel) the ListView. This is just an introduction so we don't use other events like `onScrollChanged`. Now let's build and launch the app. You can see the ActionBar gets hidden or shown when you swipe the ListView. As you can see, the most important codes are the animation codes in the callbacks. You can learn how to write these code in this tutorial. In the [next section](../../docs/example/index.md), we'll check the existing examples to see what we can do with this library. ## Program list Following codes are the entire Activity, just for your reference. ```java import android.support.v7.app.AppCompatActivity; // other imports and package statement are omitted public class MainActivity extends AppCompatActivity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ObservableListView listView = (ObservableListView) findViewById(R.id.list); listView.setScrollViewCallbacks(this); // TODO These are dummy. Populate your data here. ArrayList items = new ArrayList(); for (int i = 1; i <= 100; i++) { items.add("Item " + i); } listView.setAdapter(new ArrayAdapter( this, android.R.layout.simple_list_item_1, items)); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); } } else if (scrollState == ScrollState.DOWN) { if (!ab.isShowing()) { ab.show(); } } } } ``` [Next: Try the example app »](../../docs/example/index.md) ================================================ FILE: docs/quick-start/dependencies.md ================================================ # Dependencies This library is published to the Maven Central repository, so you can use it through Gradle/Maven. You can use it in Eclipse, but Android Studio (or Gradle) is recommended. In Quick start guide, we assume you're using Android Studio. ## build.gradle Write the following dependency configuration to your `build.gradle`. ```gradle repositories { mavenCentral() } dependencies { // Other dependencies are omitted compile 'com.github.ksoichiro:android-observablescrollview:VERSION' } ``` You should replace `VERSION` to the appropriate version number like `1.5.0`. Then, click "sync" button to get the library using the configuration above. To confirm the available versions, search [the Maven Central Repository](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.ksoichiro%22%20AND%20a%3A%22android-observablescrollview%22). [Next: Layout »](../../docs/quick-start/layout.md) ================================================ FILE: docs/quick-start/index.md ================================================ # Quick start Thank you for having interest in this library! In this section, I'll show some quick instructions for introducing this library into your app. 1. [Dependencies](../../docs/quick-start/dependencies.md) 1. [Layout](../../docs/quick-start/layout.md) 1. [Animation codes](../../docs/quick-start/animation.md) [Next: Dependencies »](../../docs/quick-start/dependencies.md) ================================================ FILE: docs/quick-start/layout.md ================================================ # Layout After adding the dependency, let's write layout file such as `res/layout/activity_main.xml`. This time, we'll use only one element `ObservableListView`. ```xml ``` Android-ObservableScrollView provides several types of views that are scroll-able such as `ObservableScrollView`, `ObservableGridView`, etc. And they extend the standard widget (`ScrollView`, `GridView`, ...), and they provide some callbacks to get scroll events. After writing the above layout, you can write animation codes using these callbacks. [Next: Animation codes »](../../docs/quick-start/animation.md) ================================================ FILE: docs/reference/_data.json ================================================ { "index": { "title": "Rerefence" }, "supported-widgets": { "title": "Supported widgets" }, "environment": { "title": "Environment" }, "release-notes": { "title": "Release notes" } } ================================================ FILE: docs/reference/environment.md ================================================ # Environment ## Development This project is built and tested under the following environment. * OS: Mac OS X 10.10 * IDE: Android Studio 1.2 * JDK: 1.7 ## Prerequisites for building on Android Studio * Android Studio (1.0.0+) * Oracle JDK 7 * Android SDK Tools (Rev.24.1.2) * Android SDK Build-tools (Rev.22.0.1) * Android 5.1.1 SDK Platform (Rev.2) * Android Support Repository (Rev.14) * Android Support Library (Rev.21.1.1) ## Prerequisites for building on Eclipse * [Eclipse IDE for Java Developers 4.4 (Luna) SR1](https://eclipse.org/downloads/packages/eclipse-ide-java-developers/lunasr1a) * [Eclipse ADT Plugin](http://developer.android.com/sdk/installing/installing-adt.html) * Oracle JDK 7 * Android 5.0 SDK Platform (Rev.1+) * Android Support Repository (Rev.9+) * Android Support Library (Rev.21.0.2+) ================================================ FILE: docs/reference/index.md ================================================ # Reference 1. [Supported widgets](../../docs/basic/supported-widgets.md) 1. [Environment](../../docs/reference/environment.md) 1. [Release notes](../../docs/reference/release-notes.md) ================================================ FILE: docs/reference/release-notes.md ================================================ # Release notes * v1.6.0 * Added header view feature to `ObservableGridView` (#148). * Added footer view feature to `ObservableGridView` (#183). * Updated `recyclerview-v7` library version to 22.2.0. * Fixed ViewPager swiping bug in `ObservableListView` (#185). * Fixed NPE in `ObservableRecyclerView` (#149). * v1.5.2 * Fix `ObservableGridView` to use first child of line in height calculation. * v1.5.1 * Fix `scrollY` of `onScrollChanged` in `ObservableGridView` jumps when the first visible item changes. * v1.5.0 * Add a helper class `CacheFragmentStatePagerAdapter` to implement ViewPager pattern. * Fix that swipe down (over-scroll) causes item click. * v1.4.0 * Add a custom view named `TouchInterceptionFrameLayout` and a new API `setTouchInterceptionViewGroup()` for `Scrollable`. With these class and API, you can move `Scrollable` itself using its scrolling events. * Add a helper class `ScrollUtils` for implementing scrolling effects. * v1.3.2 * Fix that `ObservableRecyclerView` causes `BadParcelableException` on `onRestoreInstanceState`. * v1.3.1 * Fix that `onDownMotionEvent` not called and parameters of `onScrollChanged` are incorrect when children views handle touch events. * v1.3.0 * Add new interface `Scrollable` to provide common API for scrollable widgets. * v1.2.1 * Fix that the scroll states and other internal information are lost after `onSaveInstanceState()`. * Fix that the scrollY is incorrect if the ListView/RecyclerView don't scroll from the top. (It's just approximating the scroll offset and not the complete solution but better than before.) * v1.2.0 * Add GridView support. * Fix ObservableListView cannot detect onScrollChanged on Android 2.3. * Fix ObservableScrollView cannot detect UP and DOWN state in onUpOrCancelMotionEvent before Android 4.4. * v1.1.0 * Add RecyclerView support. * v1.0.0 * Initial release. ================================================ FILE: docs/reference/supported-widgets.md ================================================ # Supported widgets Widgets are named with `Observable` prefix. (e.g. `ListView` → `ObservableListView`) You can handle these widgets with `Scrollable` interface. | Widget | Since | Note | |:------:|:-----:| ---- | | ListView | v1.0.0 | - | | ScrollView | v1.0.0 | - | | WebView | v1.0.0 | - | | RecyclerView | v1.1.0 | It's supported but RecyclerView provides scroll states and position with [OnScrollListener](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.OnScrollListener.html). You should use it if you don't have any reason. | | GridView | v1.2.0 | - | ================================================ FILE: gradle/gradle-mvn-push.gradle ================================================ /* * Copyright 2013 Chris Banes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'maven' apply plugin: 'signing' def isReleaseBuild() { return VERSION_NAME.contains("SNAPSHOT") == false } def getReleaseRepositoryUrl() { return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" } def getSnapshotRepositoryUrl() { return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : "https://oss.sonatype.org/content/repositories/snapshots/" } def getRepositoryUsername() { return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" } def getRepositoryPassword() { return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" } afterEvaluate { project -> uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME repository(url: getReleaseRepositoryUrl()) { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } snapshotRepository(url: getSnapshotRepositoryUrl()) { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } } } signing { required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } artifacts { archives androidSourcesJar archives androidJavadocsJar } } ================================================ FILE: gradle/version.gradle ================================================ // Generate version information to show in the sample app. // This can be achieved more easily when we use BuildConfig in Gradle and Android Studio, // but we also support Eclipse so we don't use BuildConfig. project.ext.versionInfo = new Expando() project.ext.versionInfo.srcDir = 'version' // Get git commit hash for naming the APK file if 'git' is available try { project.ext.versionInfo.build = "git rev-parse --short HEAD".execute().text.trim() } catch (ignored) { project.ext.versionInfo.build = "unknown" } project.ext.versionInfo.releaseVersionName = project.ext.versionInfo.build android.sourceSets.findAll { it in android.buildTypes }.each { it.java.srcDirs += "src/${project.ext.versionInfo.srcDir}/${it.name}/java" } android.buildTypes.each { buildType -> task "generateVersionInfo${buildType.name.capitalize()}" << { def packageName = android.defaultConfig.applicationId def dir = project.file("src/${project.ext.versionInfo.srcDir}/${buildType.name}/java/${packageName.tr('.', '/')}") if (dir.exists()) { dir.listFiles().each { project.delete(it) } } else { project.mkdir(dir) } def libraryVersion = buildType.name == 'release' ? project.ext.versionInfo.releaseVersionName : project.ext.versionInfo.build def className = 'VersionInfo' new File(dir, "${className}.java").text = """\ package ${packageName}; // DO NOT EDIT: This file is automatically generated. public class ${className} { public static final String LIBRARY_VERSION = "${libraryVersion}"; public static final String BUILD = "${project.ext.versionInfo.build}"; } """ } } task cleanVersionInfo << { def dir = project.file("src/${project.ext.versionInfo.srcDir}") if (dir.exists()) { dir.listFiles().findAll { it.isDirectory() }.each { project.delete(it) } } } clean.dependsOn(tasks['cleanVersionInfo']) afterEvaluate { android.applicationVariants.all { tasks["generate${it.name.capitalize()}Sources"].dependsOn("generateVersionInfo${it.buildType.name.capitalize()}") } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Apr 10 15:27:10 PDT 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip ================================================ FILE: gradle.properties ================================================ VERSION_NAME=1.7.0-SNAPSHOT SYNCED_VERSION_NAME=1.5.2 GROUP=com.github.ksoichiro POM_DESCRIPTION=Android library to observe scroll events on scrollable views. POM_URL=https://github.com/ksoichiro/Android-ObservableScrollView POM_SCM_URL=https://github.com/ksoichiro/Android-ObservableScrollView POM_SCM_CONNECTION=scm:git:git://github.com/ksoichiro/Android-ObservableScrollView.git POM_SCM_DEV_CONNECTION=scm:git:git@github.com:ksoichiro/Android-ObservableScrollView.git POM_LICENCE_NAME=Apache License 2.0 POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 POM_LICENCE_DIST=repo POM_DEVELOPER_ID=ksoichiro POM_DEVELOPER_NAME=Soichiro Kashima ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: library/.gitignore ================================================ /build ================================================ FILE: library/AndroidManifest.xml ================================================ ================================================ FILE: library/build.gradle ================================================ apply plugin: 'com.android.library' dependencies { compile 'com.android.support:recyclerview-v7:23.1.1' androidTestCompile ('com.android.support:appcompat-v7:23.1.1') { exclude module: 'support-v4' } androidTestCompile ('com.nineoldandroids:library:2.4.0') { exclude module: 'support-v4' } } android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { minSdkVersion 9 } buildTypes { debug { testCoverageEnabled = true } } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' res.srcDirs = ['res'] } } lintOptions { abortOnError false } } // When testing on Travis CI, // connectedCheck task doesn't output logs for more than 10 minutes often, // which causes build failure. // To avoid this, we change the log level for test tasks. // Test tasks for buildTypes will be defined on evaluation phase, // so do it on afterEvaluate. afterEvaluate { project -> tasks.withType(VerificationTask) { logging.level = LogLevel.INFO } } apply plugin: 'com.github.kt3k.coveralls' coveralls.jacocoReportPath = 'build/reports/coverage/debug/report.xml' // This is from 'https://github.com/chrisbanes/gradle-mvn-push' apply from: "${rootDir}/gradle/gradle-mvn-push.gradle" ================================================ FILE: library/gradle.properties ================================================ POM_NAME=Android-ObservableScrollView POM_ARTIFACT_ID=android-observablescrollview POM_PACKAGING=aar ================================================ FILE: library/res/.gitkeep ================================================ ================================================ FILE: library/src/androidTest/AndroidManifest.xml ================================================ ================================================ FILE: library/src/androidTest/assets/lipsum.html ================================================

Lorem ipsum dolor sit amet, ut duis lorem provident sed felis blandit, condimentum donec lectus ipsum et mauris, morbi porttitor interdum feugiat nulla donec sodales, vestibulum nisl primis a molestie vestibulum quam, sapien mauris metus risus suspendisse magnis. Augue viverra nulla faucibus egestas eu, a etiam id congue rutrum ante, arcu tincidunt donec quam felis at ornare, iaculis ligula sodales venenatis commodo volutpat neque, suspendisse elit praesent tellus felis mi amet. Inceptos amet tempor lectus lorem est non, ac donec ac libero neque mauris, tellus ante metus eget leo consequat. Scelerisque dolor curabitur pretium blandit ut feugiat, amet lacus pulvinar justo convallis ut, sed natoque ipsum urna posuere nibh eu. Sed at sed vulputate sit orci, facilisis a aliquam tellus quam aliquam, eu aliquam donec at molestie ante, pellentesque mauris lorem ultrices libero faucibus porta, imperdiet adipiscing sit hac diam ut nulla. Lacus enim elit pulvinar donec vehicula dapibus, accumsan purus officia cursus dolor sapien, eu amet dis mauris mi nulla ut. Non accusamus etiam pede non urna tempus, vestibulum aliquam tortor eget pharetra sodales, in vestibulum ut justo orci nulla, lobortis purus sem semper consectetuer magni purus. Dolor a leo vestibulum amet ut sit, arcu ut eaque urna fusce aliquet turpis, sed fermentum sed vestibulum nisl pede, tristique enim lorem posuere in laborum ut. Vestibulum id id justo leo nulla, magna lobortis ullamcorper et dignissim pellentesque, duis suspendisse quis id lorem ante. Vivamus a nullam ante adipiscing amet, mi vel consectetuer nunc aenean pede quisque, eget rhoncus dis porttitor habitant nunc vivamus, duis cubilia blandit non donec justo dictumst, praesent vitae nulla nam pulvinar urna. Adipiscing adipiscing justo urna pulvinar imperdiet nullam, vitae fusce rhoncus proin nonummy suscipit, ullamcorper amet et non potenti platea ultrices, mauris nullam sapien nunc justo vel, eu semper pellentesque arcu fusce augue. Malesuada mauris nibh sit a a scelerisque, velit sem lectus tellus convallis consectetuer, ultricies auctor a ante eros amet sed.

Risus lacus duis leo platea wisi, felis maecenas rutrum in id in donec, non id a potenti libero eget, posuere elit ea sed pellentesque quis. Sunt lacus urna lorem elit duis, nibh donec purus quisque consectetuer dolor, neque vestibulum proin ornare eros nonummy phasellus. Iaculis cras eu at egestas dolor montes, viverra quisque malesuada consectetuer semper maecenas, a sed vitae donec tempor aliqua metus, ornare mollis suscipit et erat fusce, sit orci aut auctor elementum fames aliquam. Platea dui integer magnis non metus, minus dignissimos ante massa nostra et, rutrum sapien egestas quis sapien donec donec. Erat sit a eros aenean natoque, quam libero id lorem enim proin, lorem ipsum fermentum mattis metus et. Aliquam aliquet suscipit purus conubia at neque, platea vivamus vestibulum nulla quibusdam senectus, et morbi lectus malesuada gravida donec, elementum sit convallis pellentesque velit amet. Et eveniet viverra vehicula consectetuer justo, provident sed commodo non lacinia velit, tempor phasellus vel leo nisl cras, vivamus et arcu interdum dui eu amet. Volutpat wisi rhoncus vel turpis diam quibusdam, dapibus elit est quisque cubilia mauris, nulla elit magna tempor accumsan bibendum, lorem varius sed interdum eget mattis, scelerisque egestas feugiat donec dui molestie. Leo facilisis nisl sit montes ligula sed, enim commodo consectetuer nunc est et, ut sed vehicula dolor luctus elit. Fermentum cras donec eget nibh est vel, sed justo risus et pharetra diam, eu vivamus egestas ligula risus diam, sed justo eget hac ut mauris. Vestibulum diam nec vitae mi eget suspendisse, aenean arcu purus facilisis purus class in, id aliquam sit id scelerisque sapien etiam. Ut nullam sit sed at mauris lobortis, consequat dolor autem ipsum euismod nulla, elit quis proin eget conubia varius, erat arcu massa mus in mauris, scelerisque ut eu sollicitudin libero leo urna.

Consectetuer luctus tempor elit ut dolor ligula, quis dui per dui hendrerit ante sagittis, in quisque pretium in eleifend enim. Condimentum iaculis vitae feugiat dis tellus vel, lectus dolor nec dui nulla nascetur, et pellentesque curabitur lorem leo velit eget. Id nascetur arcu lobortis suspendisse imperdiet urna, natoque nascetur ante in porta a, interdum hendrerit mi bibendum platea tellus, urna in enim ornare vestibulum faucibus enim. Leo fusce egestas ante nec volutpat, in tempor vel facilisis potenti ut, pede at non lorem a commodo, nulla dolor orci interdum vestibulum nulla. Dui nulla vestibulum quisque a pharetra porta, integer nec ipsum nec sed dui pharetra, magna et dignissim ipsum sed dictum, litora eros vivamus scelerisque libero ipsum. Sed ac ac lorem molestie adipiscing morbi, pellentesque imperdiet nunc quis morbi amet ante, libero dui ligula nec risus neque et, velit nonummy phasellus et facilisi amet, ligula in elementum non sapien pulvinar faucibus. Eu leo ut posuere sed aliquet, tincidunt vel urna volutpat tempus sem, sit felis aliquet vestibulum condimentum sit, amet nibh vel tellus purus ullamcorper libero, nulla vestibulum pede ut vestibulum pretium. Eu nulla vestibulum a neque in metus, quisquam nam sed cursus eget luctus, pede ultrices nec sed dignissim pellentesque, sit class cursus metus nulla placerat mauris, consequat mollis neque vivamus amet pede. Mauris dolor nulla diam eros bibendum, quam ante vestibulum morbi non ligula vel, molestie curabitur rhoncus nulla euismod interdum non. Nulla fringilla lorem mollis ad massa, sit molestie nibh lorem arcu volutpat, accumsan commodo lectus eu et donec, sit tempor tempus rutrum in curabitur amet. Nec urna euismod a tincidunt commodo, eu pede turpis libero vitae viverra, ante vestibulum nam non habitasse potenti, mauris imperdiet in in nunc convallis. Et nostra wisi in est accumsan vehicula, quisque vitae felis mauris sed vulputate nec, ante imperdiet sollicitudin massa iaculis massa sit.

Quam libero nulla netus eu porta curae, ut nulla bibendum facilisis et urna sed, quis congue vestibulum aliquam interdum etiam. Nulla vel lobortis ullamcorper vitae excepturi, neque urna feugiat lectus vel lacinia, massa pretium orci eu metus neque vulputate. Imperdiet ac velit rhoncus nulla malesuada nullam, nec pulvinar justo gravida lorem rutrum magna, habitasse repudiandae mi eros vestibulum ante, nec euismod dui iaculis in turpis pretium, ac id metus egestas proin lacus lectus. Laoreet lorem nec vitae risus erat arcu, vitae quam ut in ante tristique, porta dolor pede quam et odio nam, arcu lacus sem congue ante cursus massa. Et mattis sagittis erat accumsan fusce quam, vehicula ligula beatae natoque fusce sodales conubia, habitasse metus cum magnis viverra nam cursus, egestas urna wisi primis blandit eu magna, eget libero elit lacus lorem dis aliquam. Ut mauris ante natoque lacus massa, justo a lectus sodales enim adipiscing id, accumsan ut ipsum vestibulum sed enim auctor, vitae congue tincidunt id phasellus lacinia scelerisque, tincidunt sapien nulla euismod volutpat iaculis. Platea sociis nec aliquet nec molestie, in mi et augue sapien in vivamus, integer fames proin vitae in ullamcorper et. Fringilla etiam sapiente rhoncus suspendisse nec id, lobortis cras eget egestas dui ac nec, justo lacus ut lorem bibendum quia eros, eget a gravida id donec nunc suscipit, porta sed in sodales non rutrum. Lectus vel dui elementum pellentesque magna aliquam, vitae non sit pede et fusce nibh, id id deserunt ornare dui sit condimentum, in adipiscing imperdiet turpis nam aliquet, facilisis metus magna lacus wisi facilisis tortor. Vulputate elit accumsan quam amet ligula, suspendisse lacus mi nonummy integer urna, libero nulla nunc varius in odio, laoreet nulla amet placerat amet nec. Consectetuer vel massa hendrerit vitae iaculis id, sed ut ut laudantium odio in, elit vestibulum duis ante maecenas interdum in, neque vehicula ultrices varius in quam, pede tellus pellentesque sed nullam quis.

================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/SavedStateTest.java ================================================ package com.github.ksoichiro.android.observablescrollview; import android.os.Parcel; import android.test.InstrumentationTestCase; import android.util.SparseIntArray; import android.view.AbsSavedState; public class SavedStateTest extends InstrumentationTestCase { public void testGridViewSavedState() throws Throwable { Parcel parcel = Parcel.obtain(); ObservableGridView.SavedState state1 = new ObservableGridView.SavedState(AbsSavedState.EMPTY_STATE); state1.prevFirstVisiblePosition = 1; state1.prevFirstVisibleChildHeight = 2; state1.prevScrolledChildrenHeight = 3; state1.prevScrollY = 4; state1.scrollY = 5; state1.childrenHeights = new SparseIntArray(); state1.childrenHeights.put(0, 10); state1.childrenHeights.put(1, 20); state1.childrenHeights.put(2, 30); state1.writeToParcel(parcel, 0); parcel.setDataPosition(0); ObservableGridView.SavedState state2 = ObservableGridView.SavedState.CREATOR.createFromParcel(parcel); assertNotNull(state2); assertEquals(state1.prevFirstVisiblePosition, state2.prevFirstVisiblePosition); assertEquals(state1.prevFirstVisibleChildHeight, state2.prevFirstVisibleChildHeight); assertEquals(state1.prevScrolledChildrenHeight, state2.prevScrolledChildrenHeight); assertEquals(state1.prevScrollY, state2.prevScrollY); assertEquals(state1.scrollY, state2.scrollY); assertNotNull(state1.childrenHeights); assertEquals(3, state1.childrenHeights.size()); assertEquals(10, state1.childrenHeights.get(0)); assertEquals(20, state1.childrenHeights.get(1)); assertEquals(30, state1.childrenHeights.get(2)); } public void testListViewSavedState() throws Throwable { Parcel parcel = Parcel.obtain(); ObservableListView.SavedState state1 = new ObservableListView.SavedState(AbsSavedState.EMPTY_STATE); state1.prevFirstVisiblePosition = 1; state1.prevFirstVisibleChildHeight = 2; state1.prevScrolledChildrenHeight = 3; state1.prevScrollY = 4; state1.scrollY = 5; state1.childrenHeights = new SparseIntArray(); state1.childrenHeights.put(0, 10); state1.childrenHeights.put(1, 20); state1.childrenHeights.put(2, 30); state1.writeToParcel(parcel, 0); parcel.setDataPosition(0); ObservableListView.SavedState state2 = ObservableListView.SavedState.CREATOR.createFromParcel(parcel); assertNotNull(state2); assertEquals(state1.prevFirstVisiblePosition, state2.prevFirstVisiblePosition); assertEquals(state1.prevFirstVisibleChildHeight, state2.prevFirstVisibleChildHeight); assertEquals(state1.prevScrolledChildrenHeight, state2.prevScrolledChildrenHeight); assertEquals(state1.prevScrollY, state2.prevScrollY); assertEquals(state1.scrollY, state2.scrollY); assertNotNull(state1.childrenHeights); assertEquals(3, state1.childrenHeights.size()); assertEquals(10, state1.childrenHeights.get(0)); assertEquals(20, state1.childrenHeights.get(1)); assertEquals(30, state1.childrenHeights.get(2)); } public void testRecyclerViewSavedState() throws Throwable { Parcel parcel = Parcel.obtain(); ObservableRecyclerView.SavedState state1 = new ObservableRecyclerView.SavedState(AbsSavedState.EMPTY_STATE); state1.prevFirstVisiblePosition = 1; state1.prevFirstVisibleChildHeight = 2; state1.prevScrolledChildrenHeight = 3; state1.prevScrollY = 4; state1.scrollY = 5; state1.childrenHeights = new SparseIntArray(); state1.childrenHeights.put(0, 10); state1.childrenHeights.put(1, 20); state1.childrenHeights.put(2, 30); state1.writeToParcel(parcel, 0); parcel.setDataPosition(0); ObservableRecyclerView.SavedState state2 = ObservableRecyclerView.SavedState.CREATOR.createFromParcel(parcel); assertNotNull(state2); assertEquals(state1.prevFirstVisiblePosition, state2.prevFirstVisiblePosition); assertEquals(state1.prevFirstVisibleChildHeight, state2.prevFirstVisibleChildHeight); assertEquals(state1.prevScrolledChildrenHeight, state2.prevScrolledChildrenHeight); assertEquals(state1.prevScrollY, state2.prevScrollY); assertEquals(state1.scrollY, state2.scrollY); assertNotNull(state1.childrenHeights); assertEquals(3, state1.childrenHeights.size()); assertEquals(10, state1.childrenHeights.get(0)); assertEquals(20, state1.childrenHeights.get(1)); assertEquals(30, state1.childrenHeights.get(2)); } public void testScrollViewSavedState() throws Throwable { Parcel parcel = Parcel.obtain(); ObservableScrollView.SavedState state1 = new ObservableScrollView.SavedState(AbsSavedState.EMPTY_STATE); state1.prevScrollY = 1; state1.scrollY = 2; state1.writeToParcel(parcel, 0); parcel.setDataPosition(0); ObservableScrollView.SavedState state2 = ObservableScrollView.SavedState.CREATOR.createFromParcel(parcel); assertNotNull(state2); assertEquals(state1.prevScrollY, state2.prevScrollY); assertEquals(state1.scrollY, state2.scrollY); } public void testWebViewSavedState() throws Throwable { Parcel parcel = Parcel.obtain(); ObservableWebView.SavedState state1 = new ObservableWebView.SavedState(AbsSavedState.EMPTY_STATE); state1.prevScrollY = 1; state1.scrollY = 2; state1.writeToParcel(parcel, 0); parcel.setDataPosition(0); ObservableWebView.SavedState state2 = ObservableWebView.SavedState.CREATOR.createFromParcel(parcel); assertNotNull(state2); assertEquals(state1.prevScrollY, state2.prevScrollY); assertEquals(state1.scrollY, state2.scrollY); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.widget.AbsListView; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class GridViewActivity extends Activity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gridview); ObservableGridView scrollable = (ObservableGridView) findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, scrollable); scrollable.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.widget.FrameLayout; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class GridViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableGridView scrollable; private int[] callbackCounter; public GridViewActivityTest() { super(GridViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableGridView) activity.findViewById(R.id.scrollable); callbackCounter = new int[2]; } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new ObservableGridView(activity); new ObservableGridView(activity, null, 0); } }); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testScrollVerticallyTo() throws Throwable { final DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, metrics)); } }); getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo(0); } }); getInstrumentation().waitForIdleSync(); } public void testNoCallbacks() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableGridView) activity.findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(null); } }); testScroll(); } public void testCallbacks() throws Throwable { final ObservableScrollViewCallbacks[] callbacks = new ObservableScrollViewCallbacks[2]; callbackCounter[0] = 0; callbackCounter[1] = 0; runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableGridView) activity.findViewById(R.id.scrollable); callbacks[0] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[0]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[0]); callbacks[1] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[1]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[1]); } }); testScroll(); // Assert that all the callbacks are enabled and get called. assertTrue(0 < callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Remove one of the callbacks and scroll again to assert it's really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.removeScrollViewCallbacks(callbacks[0]); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Clear all callbacks and assert they're really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.clearScrollViewCallbacks(); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 == callbackCounter[1]); } public void testCannotAddHeaderOrFooterWhenAdapterIsAlreadySet() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { try { View view = new View(activity); final int flexibleSpaceImageHeight = activity.getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); view.setLayoutParams(lp); view.setClickable(true); scrollable.addHeaderView(view); fail(); } catch (IllegalStateException ignore) { } try { View view = new View(activity); final int flexibleSpaceImageHeight = activity.getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); FrameLayout.LayoutParams lpf = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); view.setLayoutParams(lpf); scrollable.addFooterView(view); fail(); } catch (IllegalStateException ignore) { } } }); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.AbsListView; import android.widget.FrameLayout; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class HeaderGridViewActivity extends Activity implements ObservableScrollViewCallbacks { public View headerView; public View footerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gridview); ObservableGridView scrollable = (ObservableGridView) findViewById(R.id.scrollable); // Set padding view for GridView. This is the flexible space. headerView = new View(this); final int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); headerView.setLayoutParams(lp); // This is required to disable header's list selector effect headerView.setClickable(true); scrollable.addHeaderView(headerView); // Footer is also available. footerView = new View(this); FrameLayout.LayoutParams lpf = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); footerView.setLayoutParams(lpf); scrollable.addFooterView(footerView); scrollable.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, scrollable); scrollable.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.test.ActivityInstrumentationTestCase2; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.widget.FrameLayout; import android.widget.ListAdapter; import android.widget.SimpleAdapter; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class HeaderGridViewActivityTest extends ActivityInstrumentationTestCase2 { private HeaderGridViewActivity activity; private ObservableGridView scrollable; public HeaderGridViewActivityTest() { super(HeaderGridViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableGridView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testScrollVerticallyTo() throws Throwable { final DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, metrics)); } }); getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo(0); } }); getInstrumentation().waitForIdleSync(); } public void testHeaderViewFeatures() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { assertEquals(1, scrollable.getHeaderViewCount()); assertEquals(1, scrollable.getFooterViewCount()); ListAdapter adapter = scrollable.getAdapter(); assertTrue(adapter instanceof ObservableGridView.HeaderViewGridAdapter); ObservableGridView.HeaderViewGridAdapter hvgAdapter = (ObservableGridView.HeaderViewGridAdapter) adapter; assertEquals(1, hvgAdapter.getHeadersCount()); assertEquals(1, hvgAdapter.getFootersCount()); assertNotNull(hvgAdapter.getWrappedAdapter()); assertTrue(hvgAdapter.areAllItemsEnabled()); assertFalse(hvgAdapter.isEmpty()); Object data = hvgAdapter.getItem(0); assertNull(data); assertNotNull(hvgAdapter.getView(0, null, scrollable)); assertNotNull(hvgAdapter.getView(1, null, scrollable)); assertNotNull(hvgAdapter.getFilter()); assertTrue(scrollable.removeHeaderView(activity.headerView)); assertEquals(0, scrollable.getHeaderViewCount()); assertEquals(0, hvgAdapter.getHeadersCount()); assertFalse(scrollable.removeHeaderView(activity.headerView)); activity.headerView = new View(activity); final int flexibleSpaceImageHeight = activity.getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); activity.headerView.setLayoutParams(lp); // This is required to disable header's list selector effect activity.headerView.setClickable(true); scrollable.addHeaderView(activity.headerView); assertEquals(100/* items */ + 2/* header */ + 2/* footer */, hvgAdapter.getCount()); assertEquals(1, hvgAdapter.getHeadersCount()); assertEquals(2, hvgAdapter.getNumColumns()); // If the header is added by addHeader(View), // HeaderViewGridAdapter doesn't contain any associated data. // headerData does NOT mean the view. // If we want to get the view, we should use getView(). assertNull(hvgAdapter.getItem(0)); assertNull(hvgAdapter.getItem(1)); assertEquals(1, hvgAdapter.getFootersCount()); assertNull(hvgAdapter.getItem(100/* items */ + 2/* header */ + 2/* footer */ - 1 - 1)); assertNull(hvgAdapter.getItem(100/* items */ + 2/* header */ + 2/* footer */ - 1)); } }); // Scroll to bottom and try removing re-adding the footer view. for (int i = 0; i < 10; i++) { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); } getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { ListAdapter adapter = scrollable.getAdapter(); ObservableGridView.HeaderViewGridAdapter hvgAdapter = (ObservableGridView.HeaderViewGridAdapter) adapter; assertTrue(scrollable.removeFooterView(activity.footerView)); assertEquals(0, scrollable.getFooterViewCount()); assertEquals(0, hvgAdapter.getFootersCount()); assertFalse(scrollable.removeFooterView(activity.footerView)); activity.footerView = new View(activity); final int flexibleSpaceImageHeight = activity.getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); FrameLayout.LayoutParams lpf = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight); activity.footerView.setLayoutParams(lpf); scrollable.addFooterView(activity.footerView); } }); } public void testHeaderViewGridExceptions() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { try { new ObservableGridView.HeaderViewGridAdapter(null, null, null); } catch (IllegalArgumentException e) { fail(); } ListAdapter adapter = scrollable.getAdapter(); ObservableGridView.HeaderViewGridAdapter hvgAdapter = (ObservableGridView.HeaderViewGridAdapter) adapter; try { hvgAdapter.setNumColumns(0); } catch (IllegalArgumentException e) { fail(); } ArrayList headerViewInfos = new ArrayList<>(); ObservableGridView.HeaderViewGridAdapter adapter1 = new ObservableGridView.HeaderViewGridAdapter(headerViewInfos, null, null); assertTrue(adapter1.isEmpty()); try { adapter1.isEnabled(-1); fail(); } catch (ArrayIndexOutOfBoundsException ignore) { } try { adapter1.getItem(-1); fail(); } catch (ArrayIndexOutOfBoundsException ignore) { } try { adapter1.getView(0, null, null); fail(); } catch (ArrayIndexOutOfBoundsException ignore) { } try { adapter1.getView(-1, null, scrollable); fail(); } catch (ArrayIndexOutOfBoundsException ignore) { } } }); } public void testHeaderViewGridAdapter() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { try { new ObservableGridView.HeaderViewGridAdapter(null, null, null); } catch (IllegalArgumentException ignore) { fail(); } } }); runTestOnUiThread(new Runnable() { @Override public void run() { ArrayList list = new ArrayList<>(); Map map = new LinkedHashMap<>(); map.put("text", "A"); List> data = new ArrayList<>(); data.add(map); ObservableGridView.HeaderViewGridAdapter adapter = new ObservableGridView.HeaderViewGridAdapter( list, null, new SimpleAdapter( activity, data, android.R.layout.simple_list_item_1, new String[]{"text"}, new int[]{android.R.id.text1})); assertFalse(adapter.removeHeader(null)); assertEquals(1, adapter.getCount()); } }); runTestOnUiThread(new Runnable() { @Override public void run() { ArrayList list = new ArrayList<>(); ObservableGridView.HeaderViewGridAdapter adapter = new ObservableGridView.HeaderViewGridAdapter( list, null, null); assertEquals(0, adapter.getCount()); try { adapter.isEnabled(1); fail(); } catch (IndexOutOfBoundsException ignore) { } try { adapter.getItem(1); fail(); } catch (IndexOutOfBoundsException ignore) { } } }); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.widget.AbsListView; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class ListViewActivity extends Activity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_listview); ObservableListView scrollable = (ObservableListView) findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, scrollable); scrollable.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.util.DisplayMetrics; import android.util.TypedValue; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class ListViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableListView scrollable; private int[] callbackCounter; public ListViewActivityTest() { super(ListViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableListView) activity.findViewById(R.id.scrollable); callbackCounter = new int[2]; } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new ObservableListView(activity); new ObservableListView(activity, null, 0); } }); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testScrollVerticallyTo() throws Throwable { final DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, metrics)); } }); getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo(0); } }); getInstrumentation().waitForIdleSync(); } public void testNoCallbacks() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableListView) activity.findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(null); } }); testScroll(); } public void testCallbacks() throws Throwable { final ObservableScrollViewCallbacks[] callbacks = new ObservableScrollViewCallbacks[2]; callbackCounter[0] = 0; callbackCounter[1] = 0; runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableListView) activity.findViewById(R.id.scrollable); callbacks[0] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[0]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[0]); callbacks[1] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[1]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[1]); } }); testScroll(); // Assert that all the callbacks are enabled and get called. assertTrue(0 < callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Remove one of the callbacks and scroll again to assert it's really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.removeScrollViewCallbacks(callbacks[0]); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Clear all callbacks and assert they're really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.clearScrollViewCallbacks(); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 == callbackCounter[1]); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.os.Bundle; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; public class ListViewScrollFromBottomActivity extends ListViewActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ObservableListView scrollable = (ObservableListView) findViewById(R.id.scrollable); ScrollUtils.addOnGlobalLayoutListener(scrollable, new Runnable() { @Override public void run() { int count = scrollable.getAdapter().getCount() - 1; int position = count == 0 ? 1 : count > 0 ? count : 0; scrollable.smoothScrollToPosition(position); scrollable.setSelection(position); } }); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import com.github.ksoichiro.android.observablescrollview.ObservableListView; public class ListViewScrollFromBottomActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableListView scrollable; public ListViewScrollFromBottomActivityTest() { super(ListViewScrollFromBottomActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableListView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class RecyclerViewActivity extends Activity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recyclerview); ObservableRecyclerView recyclerView = (ObservableRecyclerView) findViewById(R.id.scrollable); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setHasFixedSize(true); recyclerView.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, recyclerView); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.util.DisplayMetrics; import android.util.TypedValue; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class RecyclerViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableRecyclerView scrollable; private int[] callbackCounter; public RecyclerViewActivityTest() { super(RecyclerViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableRecyclerView) activity.findViewById(R.id.scrollable); callbackCounter = new int[2]; getInstrumentation().waitForIdleSync(); } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new ObservableRecyclerView(activity); new ObservableRecyclerView(activity, null, 0); } }); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testScrollVerticallyTo() throws Throwable { final DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, metrics)); } }); getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo(0); } }); getInstrumentation().waitForIdleSync(); } public void testNoCallbacks() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableRecyclerView) activity.findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(null); } }); testScroll(); } public void testCallbacks() throws Throwable { final ObservableScrollViewCallbacks[] callbacks = new ObservableScrollViewCallbacks[2]; callbackCounter[0] = 0; callbackCounter[1] = 0; runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableRecyclerView) activity.findViewById(R.id.scrollable); callbacks[0] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[0]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[0]); callbacks[1] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[1]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[1]); } }); testScroll(); // Assert that all the callbacks are enabled and get called. assertTrue(0 < callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Remove one of the callbacks and scroll again to assert it's really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.removeScrollViewCallbacks(callbacks[0]); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Clear all callbacks and assert they're really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.clearScrollViewCallbacks(); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 == callbackCounter[1]); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.os.Bundle; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; public class RecyclerViewScrollFromBottomActivity extends RecyclerViewActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ObservableRecyclerView scrollable = (ObservableRecyclerView) findViewById(R.id.scrollable); ScrollUtils.addOnGlobalLayoutListener(scrollable, new Runnable() { @Override public void run() { int count = scrollable.getAdapter().getItemCount() - 1; int position = count == 0 ? 1 : count > 0 ? count : 0; scrollable.scrollToPosition(position); } }); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; public class RecyclerViewScrollFromBottomActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableRecyclerView scrollable; public RecyclerViewScrollFromBottomActivityTest() { super(RecyclerViewScrollFromBottomActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableRecyclerView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollUtilsTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.graphics.Color; import android.test.InstrumentationTestCase; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; import junit.framework.Assert; public class ScrollUtilsTest extends InstrumentationTestCase { public void testGetFloat() { Assert.assertEquals(1.0f, ScrollUtils.getFloat(1, 0, 2)); assertEquals(0.0f, ScrollUtils.getFloat(-1, 0, 2)); assertEquals(2.0f, ScrollUtils.getFloat(3, 0, 2)); } public void testGetColorWithAlpha() { assertEquals(Color.parseColor("#00123456"), ScrollUtils.getColorWithAlpha(0, Color.parseColor("#FF123456"))); assertEquals(Color.parseColor("#FF123456"), ScrollUtils.getColorWithAlpha(1, Color.parseColor("#FF123456"))); } public void testMixColors() { assertEquals(Color.parseColor("#000000"), ScrollUtils.mixColors(Color.parseColor("#000000"), Color.parseColor("#FFFFFF"), 0)); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; public class ScrollViewActivity extends Activity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scrollview); ((Scrollable) findViewById(R.id.scrollable)).setScrollViewCallbacks(this); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class ScrollViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableScrollView scrollable; private int[] callbackCounter; public ScrollViewActivityTest() { super(ScrollViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableScrollView) activity.findViewById(R.id.scrollable); callbackCounter = new int[2]; } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new ObservableScrollView(activity); new ObservableScrollView(activity, null, 0); } }); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testNoCallbacks() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableScrollView) activity.findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(null); } }); testScroll(); } public void testCallbacks() throws Throwable { final ObservableScrollViewCallbacks[] callbacks = new ObservableScrollViewCallbacks[2]; callbackCounter[0] = 0; callbackCounter[1] = 0; runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableScrollView) activity.findViewById(R.id.scrollable); callbacks[0] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[0]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[0]); callbacks[1] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[1]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[1]); } }); testScroll(); // Assert that all the callbacks are enabled and get called. assertTrue(0 < callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Remove one of the callbacks and scroll again to assert it's really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.removeScrollViewCallbacks(callbacks[0]); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Clear all callbacks and assert they're really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.clearScrollViewCallbacks(); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 == callbackCounter[1]); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleHeaderRecyclerAdapter.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.ArrayList; public class SimpleHeaderRecyclerAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_HEADER = 0; private static final int VIEW_TYPE_ITEM = 1; private LayoutInflater mInflater; private ArrayList mItems; private View mHeaderView; public SimpleHeaderRecyclerAdapter(Context context, ArrayList items, View headerView) { mInflater = LayoutInflater.from(context); mItems = items; mHeaderView = headerView; } @Override public int getItemCount() { if (mHeaderView == null) { return mItems.size(); } else { return mItems.size() + 1; } } @Override public int getItemViewType(int position) { return (position == 0) ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_HEADER) { return new HeaderViewHolder(mHeaderView); } else { return new ItemViewHolder(mInflater.inflate(android.R.layout.simple_list_item_1, parent, false)); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (viewHolder instanceof ItemViewHolder) { ((ItemViewHolder) viewHolder).textView.setText(mItems.get(position - 1)); } } static class HeaderViewHolder extends RecyclerView.ViewHolder { public HeaderViewHolder(View view) { super(view); } } static class ItemViewHolder extends RecyclerView.ViewHolder { TextView textView; public ItemViewHolder(View view) { super(view); textView = (TextView) view.findViewById(android.R.id.text1); } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleRecyclerAdapter.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.ArrayList; public class SimpleRecyclerAdapter extends RecyclerView.Adapter { private LayoutInflater mInflater; private ArrayList mItems; public SimpleRecyclerAdapter(Context context, ArrayList items) { mInflater = LayoutInflater.from(context); mItems = items; } @Override public int getItemCount() { return mItems.size(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(android.R.layout.simple_list_item_1, parent, false)); } @Override public void onBindViewHolder(ViewHolder viewHolder, int position) { viewHolder.textView.setText(mItems.get(position)); } static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; public ViewHolder(View view) { super(view); textView = (TextView) view.findViewById(android.R.id.text1); } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.TextView; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.nineoldandroids.view.ViewHelper; public class TouchInterceptionGridViewActivity extends Activity implements ObservableScrollViewCallbacks { private TouchInterceptionFrameLayout mInterceptionLayout; private Scrollable mScrollable; private int mIntersectionHeight; private int mHeaderBarHeight; private float mScrollYOnDownMotion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touchinterception_gridview); ((TextView) findViewById(R.id.title)).setText(getClass().getSimpleName()); mScrollable = (Scrollable) findViewById(R.id.scrollable); mScrollable.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, (ObservableGridView) mScrollable); mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { final int minInterceptionLayoutY = -mIntersectionHeight; return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) || (moving && mScrollable.getCurrentScrollY() - diffY < 0); } @Override public void onDownMotionEvent(MotionEvent ev) { mScrollYOnDownMotion = mScrollable.getCurrentScrollY(); } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY; if (translationY < -mIntersectionHeight) { translationY = -mIntersectionHeight; } else if (getScreenHeight() - mHeaderBarHeight < translationY) { translationY = getScreenHeight() - mHeaderBarHeight; } slideTo(translationY, true); } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { } }; private void slideTo(float translationY, final boolean animated) { ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) -translationY + getScreenHeight(); mInterceptionLayout.requestLayout(); } } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; public class TouchInterceptionGridViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableGridView scrollable; public TouchInterceptionGridViewActivityTest() { super(TouchInterceptionGridViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableGridView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { TouchUtils.touchAndCancelView(this, scrollable); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.TextView; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.nineoldandroids.view.ViewHelper; public class TouchInterceptionListViewActivity extends Activity implements ObservableScrollViewCallbacks { private TouchInterceptionFrameLayout mInterceptionLayout; private Scrollable mScrollable; private int mIntersectionHeight; private int mHeaderBarHeight; private float mScrollYOnDownMotion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touchinterception_listview); ((TextView) findViewById(R.id.title)).setText(getClass().getSimpleName()); mScrollable = (Scrollable) findViewById(R.id.scrollable); mScrollable.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, (ObservableListView) mScrollable); mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { final int minInterceptionLayoutY = -mIntersectionHeight; return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) || (moving && mScrollable.getCurrentScrollY() - diffY < 0); } @Override public void onDownMotionEvent(MotionEvent ev) { mScrollYOnDownMotion = mScrollable.getCurrentScrollY(); } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY; if (translationY < -mIntersectionHeight) { translationY = -mIntersectionHeight; } else if (getScreenHeight() - mHeaderBarHeight < translationY) { translationY = getScreenHeight() - mHeaderBarHeight; } slideTo(translationY, true); } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { } }; private void slideTo(float translationY, final boolean animated) { ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) -translationY + getScreenHeight(); mInterceptionLayout.requestLayout(); } } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; import com.github.ksoichiro.android.observablescrollview.ObservableListView; public class TouchInterceptionListViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableListView scrollable; public TouchInterceptionListViewActivityTest() { super(TouchInterceptionListViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableListView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { TouchUtils.touchAndCancelView(this, scrollable); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.TextView; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.nineoldandroids.view.ViewHelper; public class TouchInterceptionRecyclerViewActivity extends Activity implements ObservableScrollViewCallbacks { private TouchInterceptionFrameLayout mInterceptionLayout; private Scrollable mScrollable; private int mIntersectionHeight; private int mHeaderBarHeight; private float mScrollYOnDownMotion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touchinterception_recyclerview); ((TextView) findViewById(R.id.title)).setText(getClass().getSimpleName()); mScrollable = (Scrollable) findViewById(R.id.scrollable); mScrollable.setScrollViewCallbacks(this); ObservableRecyclerView recyclerView = (ObservableRecyclerView) mScrollable; recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setHasFixedSize(true); recyclerView.setScrollViewCallbacks(this); UiTestUtils.setDummyData(this, recyclerView); mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { final int minInterceptionLayoutY = -mIntersectionHeight; return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) || (moving && mScrollable.getCurrentScrollY() - diffY < 0); } @Override public void onDownMotionEvent(MotionEvent ev) { mScrollYOnDownMotion = mScrollable.getCurrentScrollY(); } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY; if (translationY < -mIntersectionHeight) { translationY = -mIntersectionHeight; } else if (getScreenHeight() - mHeaderBarHeight < translationY) { translationY = getScreenHeight() - mHeaderBarHeight; } slideTo(translationY, true); } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { } }; private void slideTo(float translationY, final boolean animated) { ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) -translationY + getScreenHeight(); mInterceptionLayout.requestLayout(); } } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; public class TouchInterceptionRecyclerViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableRecyclerView scrollable; public TouchInterceptionRecyclerViewActivityTest() { super(TouchInterceptionRecyclerViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableRecyclerView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { TouchUtils.touchAndCancelView(this, scrollable); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.TextView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.nineoldandroids.view.ViewHelper; public class TouchInterceptionScrollViewActivity extends Activity implements ObservableScrollViewCallbacks { private TouchInterceptionFrameLayout mInterceptionLayout; private Scrollable mScrollable; private int mIntersectionHeight; private int mHeaderBarHeight; private float mScrollYOnDownMotion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touchinterception_scrollview); ((TextView) findViewById(R.id.title)).setText(getClass().getSimpleName()); mScrollable = (Scrollable) findViewById(R.id.scrollable); mScrollable.setScrollViewCallbacks(this); mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { final int minInterceptionLayoutY = -mIntersectionHeight; return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) || (moving && mScrollable.getCurrentScrollY() - diffY < 0); } @Override public void onDownMotionEvent(MotionEvent ev) { mScrollYOnDownMotion = mScrollable.getCurrentScrollY(); } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY; if (translationY < -mIntersectionHeight) { translationY = -mIntersectionHeight; } else if (getScreenHeight() - mHeaderBarHeight < translationY) { translationY = getScreenHeight() - mHeaderBarHeight; } slideTo(translationY, true); } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { } }; private void slideTo(float translationY, final boolean animated) { ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) -translationY + getScreenHeight(); mInterceptionLayout.requestLayout(); } } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Build; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; public class TouchInterceptionScrollViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableScrollView scrollable; public TouchInterceptionScrollViewActivityTest() { super(TouchInterceptionScrollViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableScrollView) activity.findViewById(R.id.scrollable); } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new TouchInterceptionFrameLayout(activity); new TouchInterceptionFrameLayout(activity, null, 0); if (Build.VERSION_CODES.LOLLIPOP <= Build.VERSION.SDK_INT) { new TouchInterceptionFrameLayout(activity, null, 0, 0); } } }); } public void testScroll() throws Throwable { TouchUtils.touchAndCancelView(this, scrollable); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.view.MotionEvent; import android.webkit.WebView; import android.widget.FrameLayout; import android.widget.TextView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.nineoldandroids.view.ViewHelper; public class TouchInterceptionWebViewActivity extends Activity implements ObservableScrollViewCallbacks { private TouchInterceptionFrameLayout mInterceptionLayout; private Scrollable mScrollable; private int mIntersectionHeight; private int mHeaderBarHeight; private float mScrollYOnDownMotion; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_touchinterception_webview); ((TextView) findViewById(R.id.title)).setText(getClass().getSimpleName()); mScrollable = (Scrollable) findViewById(R.id.scrollable); mScrollable.setScrollViewCallbacks(this); ((WebView) mScrollable).loadUrl("file:///android_asset/lipsum.html"); mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); mHeaderBarHeight = getResources().getDimensionPixelSize(R.dimen.header_bar_height); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { final int minInterceptionLayoutY = -mIntersectionHeight; return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) || (moving && mScrollable.getCurrentScrollY() - diffY < 0); } @Override public void onDownMotionEvent(MotionEvent ev) { mScrollYOnDownMotion = mScrollable.getCurrentScrollY(); } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ViewHelper.getTranslationY(mInterceptionLayout) - mScrollYOnDownMotion + diffY; if (translationY < -mIntersectionHeight) { translationY = -mIntersectionHeight; } else if (getScreenHeight() - mHeaderBarHeight < translationY) { translationY = getScreenHeight() - mHeaderBarHeight; } slideTo(translationY, true); } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { } }; private void slideTo(float translationY, final boolean animated) { ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) -translationY + getScreenHeight(); mInterceptionLayout.requestLayout(); } } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.test.TouchUtils; import com.github.ksoichiro.android.observablescrollview.ObservableWebView; public class TouchInterceptionWebViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableWebView scrollable; public TouchInterceptionWebViewActivityTest() { super(TouchInterceptionWebViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableWebView) activity.findViewById(R.id.scrollable); } public void testScroll() throws Throwable { TouchUtils.touchAndCancelView(this, scrollable); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/UiTestUtils.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.test.InstrumentationTestCase; import android.test.TouchUtils; import android.util.TypedValue; import android.view.View; import android.widget.ArrayAdapter; import android.widget.GridView; import android.widget.ListView; import java.util.ArrayList; public class UiTestUtils { private static final int NUM_OF_ITEMS = 100; private static final int NUM_OF_ITEMS_FEW = 3; private static final int DRAG_STEP_COUNT = 50; public enum Direction { LEFT, RIGHT, UP, DOWN } private UiTestUtils() { } public static void saveAndRestoreInstanceState(final InstrumentationTestCase test, final Activity activity) throws Throwable { test.runTestOnUiThread(new Runnable() { @Override public void run() { Bundle outState = new Bundle(); test.getInstrumentation().callActivityOnSaveInstanceState(activity, outState); test.getInstrumentation().callActivityOnPause(activity); test.getInstrumentation().callActivityOnResume(activity); test.getInstrumentation().callActivityOnRestoreInstanceState(activity, outState); } }); test.getInstrumentation().waitForIdleSync(); } public static void swipeHorizontally(InstrumentationTestCase test, View v, Direction direction) { int[] xy = new int[2]; v.getLocationOnScreen(xy); final int viewWidth = v.getWidth(); final int viewHeight = v.getHeight(); float distanceFromEdge = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, v.getResources().getDisplayMetrics()); float xStart = xy[0] + ((direction == Direction.LEFT) ? (viewWidth - distanceFromEdge) : distanceFromEdge); float xEnd = xy[0] + ((direction == Direction.LEFT) ? distanceFromEdge : (viewWidth - distanceFromEdge)); float y = xy[1] + (viewHeight / 2.0f); TouchUtils.drag(test, xStart, xEnd, y, y, DRAG_STEP_COUNT); } public static void swipeVertically(InstrumentationTestCase test, View v, Direction direction) { int[] xy = new int[2]; v.getLocationOnScreen(xy); final int viewWidth = v.getWidth(); final int viewHeight = v.getHeight(); float distanceFromEdge = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, v.getResources().getDisplayMetrics()); float x = xy[0] + (viewWidth / 2.0f); float yStart = xy[1] + ((direction == Direction.UP) ? (viewHeight - distanceFromEdge) : distanceFromEdge); float yEnd = xy[1] + ((direction == Direction.UP) ? distanceFromEdge : (viewHeight - distanceFromEdge)); TouchUtils.drag(test, x, x, yStart, yEnd, DRAG_STEP_COUNT); } public static ArrayList getDummyData() { return getDummyData(NUM_OF_ITEMS); } public static ArrayList getDummyData(int num) { ArrayList items = new ArrayList(); for (int i = 1; i <= num; i++) { items.add("Item " + i); } return items; } public static void setDummyData(Context context, GridView gridView) { gridView.setAdapter(new ArrayAdapter(context, android.R.layout.simple_list_item_1, getDummyData())); } public static void setDummyData(Context context, ListView listView) { setDummyData(context, listView, NUM_OF_ITEMS); } public static void setDummyDataFew(Context context, ListView listView) { setDummyData(context, listView, NUM_OF_ITEMS_FEW); } public static void setDummyData(Context context, ListView listView, int num) { listView.setAdapter(new ArrayAdapter(context, android.R.layout.simple_list_item_1, getDummyData(num))); } public static void setDummyDataWithHeader(Context context, ListView listView, View headerView) { listView.addHeaderView(headerView); setDummyData(context, listView); } public static void setDummyData(Context context, RecyclerView recyclerView) { setDummyData(context, recyclerView, NUM_OF_ITEMS); } public static void setDummyDataFew(Context context, RecyclerView recyclerView) { setDummyData(context, recyclerView, NUM_OF_ITEMS_FEW); } public static void setDummyData(Context context, RecyclerView recyclerView, int num) { recyclerView.setAdapter(new SimpleRecyclerAdapter(context, getDummyData(num))); } public static void setDummyDataWithHeader(Context context, RecyclerView recyclerView, View headerView) { recyclerView.setAdapter(new SimpleHeaderRecyclerAdapter(context, getDummyData(), headerView)); } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2Activity.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.content.res.TypedArray; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.Toolbar; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.FrameLayout; import com.github.ksoichiro.android.observablescrollview.CacheFragmentStatePagerAdapter; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout; import com.google.samples.apps.iosched.ui.widget.SlidingTabLayout; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.view.ViewHelper; public class ViewPagerTab2Activity extends ActionBarActivity implements ObservableScrollViewCallbacks { private View mToolbarView; private TouchInterceptionFrameLayout mInterceptionLayout; private ViewPager mPager; private NavigationAdapter mPagerAdapter; private int mSlop; private boolean mScrolled; private ScrollState mLastScrollState; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_viewpagertab2); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); mToolbarView = findViewById(R.id.toolbar); mPagerAdapter = new NavigationAdapter(getSupportFragmentManager()); mPager = (ViewPager) findViewById(R.id.pager); mPager.setAdapter(mPagerAdapter); // Padding for ViewPager must be set outside the ViewPager itself // because with padding, EdgeEffect of ViewPager become strange. final int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); findViewById(R.id.pager_wrapper).setPadding(0, getActionBarSize() + tabHeight, 0, 0); SlidingTabLayout slidingTabLayout = (SlidingTabLayout) findViewById(R.id.sliding_tabs); slidingTabLayout.setCustomTabView(R.layout.tab_indicator, android.R.id.text1); slidingTabLayout.setSelectedIndicatorColors(getResources().getColor(R.color.accent)); slidingTabLayout.setDistributeEvenly(true); slidingTabLayout.setViewPager(mPager); ViewConfiguration vc = ViewConfiguration.get(this); mSlop = vc.getScaledTouchSlop(); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.container); mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { if (!mScrolled) { // This event can be used only when TouchInterceptionFrameLayout // doesn't handle the consecutive events. adjustToolbar(scrollState); } } private TouchInterceptionFrameLayout.TouchInterceptionListener mInterceptionListener = new TouchInterceptionFrameLayout.TouchInterceptionListener() { @Override public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) { if (!mScrolled && mSlop < Math.abs(diffX) && Math.abs(diffY) < Math.abs(diffX)) { // Horizontal scroll is maybe handled by ViewPager return false; } Scrollable scrollable = getCurrentScrollable(); if (scrollable == null) { mScrolled = false; return false; } // If interceptionLayout can move, it should intercept. // And once it begins to move, horizontal scroll shouldn't work any longer. int toolbarHeight = mToolbarView.getHeight(); int translationY = (int) ViewHelper.getTranslationY(mInterceptionLayout); boolean scrollingUp = 0 < diffY; boolean scrollingDown = diffY < 0; if (scrollingUp) { if (translationY < 0) { mScrolled = true; mLastScrollState = ScrollState.UP; return true; } } else if (scrollingDown) { if (-toolbarHeight < translationY) { mScrolled = true; mLastScrollState = ScrollState.DOWN; return true; } } mScrolled = false; return false; } @Override public void onDownMotionEvent(MotionEvent ev) { } @Override public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { float translationY = ScrollUtils.getFloat(ViewHelper.getTranslationY(mInterceptionLayout) + diffY, -mToolbarView.getHeight(), 0); ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) (-translationY + getScreenHeight()); mInterceptionLayout.requestLayout(); } } @Override public void onUpOrCancelMotionEvent(MotionEvent ev) { mScrolled = false; adjustToolbar(mLastScrollState); } }; public Scrollable getCurrentScrollable() { Fragment fragment = getCurrentFragment(); if (fragment == null) { return null; } View view = fragment.getView(); if (view == null) { return null; } return (Scrollable) view.findViewById(R.id.scroll); } private void adjustToolbar(ScrollState scrollState) { int toolbarHeight = mToolbarView.getHeight(); final Scrollable scrollable = getCurrentScrollable(); if (scrollable == null) { return; } int scrollY = scrollable.getCurrentScrollY(); if (scrollState == ScrollState.DOWN) { showToolbar(); } else if (scrollState == ScrollState.UP) { if (toolbarHeight <= scrollY) { hideToolbar(); } else { showToolbar(); } } else if (!toolbarIsShown() && !toolbarIsHidden()) { // Toolbar is moving but doesn't know which to move: // you can change this to hideToolbar() showToolbar(); } } private Fragment getCurrentFragment() { return mPagerAdapter.getItemAt(mPager.getCurrentItem()); } private boolean toolbarIsShown() { return ViewHelper.getTranslationY(mInterceptionLayout) == 0; } private boolean toolbarIsHidden() { return ViewHelper.getTranslationY(mInterceptionLayout) == -mToolbarView.getHeight(); } private void showToolbar() { animateToolbar(0); } private void hideToolbar() { animateToolbar(-mToolbarView.getHeight()); } private void animateToolbar(final float toY) { float layoutTranslationY = ViewHelper.getTranslationY(mInterceptionLayout); if (layoutTranslationY != toY) { ValueAnimator animator = ValueAnimator.ofFloat(ViewHelper.getTranslationY(mInterceptionLayout), toY).setDuration(200); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float translationY = (float) animation.getAnimatedValue(); ViewHelper.setTranslationY(mInterceptionLayout, translationY); if (translationY < 0) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); lp.height = (int) (-translationY + getScreenHeight()); mInterceptionLayout.requestLayout(); } } }); animator.start(); } } private int getActionBarSize() { TypedValue typedValue = new TypedValue(); int[] textSizeAttr = new int[]{R.attr.actionBarSize}; int indexOfAttrTextSize = 0; TypedArray a = obtainStyledAttributes(typedValue.data, textSizeAttr); int actionBarSize = a.getDimensionPixelSize(indexOfAttrTextSize, -1); a.recycle(); return actionBarSize; } private int getScreenHeight() { return findViewById(android.R.id.content).getHeight(); } /** * This adapter provides two types of fragments as an example. * {@linkplain #createItem(int)} should be modified if you use this example for your app. */ private static class NavigationAdapter extends CacheFragmentStatePagerAdapter { private static final String[] TITLES = new String[]{"Applepie", "Butter Cookie", "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb", "Ice Cream Sandwich", "Jelly Bean", "KitKat", "Lollipop"}; public NavigationAdapter(FragmentManager fm) { super(fm); } @Override protected Fragment createItem(int position) { Fragment f; final int pattern = position % 5; switch (pattern) { case 0: f = new ViewPagerTab2ScrollViewFragment(); break; case 1: f = new ViewPagerTab2ListViewFragment(); break; case 2: f = new ViewPagerTab2RecyclerViewFragment(); break; case 3: f = new ViewPagerTab2GridViewFragment(); break; case 4: default: f = new ViewPagerTab2WebViewFragment(); break; } return f; } @Override public int getCount() { return TITLES.length; } @Override public CharSequence getPageTitle(int position) { return TITLES[position]; } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.test.ActivityInstrumentationTestCase2; import android.view.View; public class ViewPagerTab2ActivityTest extends ActivityInstrumentationTestCase2 { private ViewPagerTab2Activity activity; public ViewPagerTab2ActivityTest() { super(ViewPagerTab2Activity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); } public void testScroll() throws Throwable { for (int i = 0; i < 5; i++) { UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.LEFT); getInstrumentation().waitForIdleSync(); scroll(); } for (int i = 0; i < 5; i++) { UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.RIGHT); getInstrumentation().waitForIdleSync(); scroll(); } } public void scroll() throws Throwable { View scrollable = ((View) activity.getCurrentScrollable()).findViewById(R.id.scroll); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { for (int i = 0; i < 5; i++) { UiTestUtils.saveAndRestoreInstanceState(this, activity); scroll(); UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.LEFT); getInstrumentation().waitForIdleSync(); } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2GridViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableGridView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; public class ViewPagerTab2GridViewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_gridview, container, false); Activity parentActivity = getActivity(); final ObservableGridView gridView = (ObservableGridView) view.findViewById(R.id.scroll); UiTestUtils.setDummyData(getActivity(), gridView); gridView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.container)); if (parentActivity instanceof ObservableScrollViewCallbacks) { gridView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ListViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; public class ViewPagerTab2ListViewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_listview, container, false); Activity parentActivity = getActivity(); final ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); UiTestUtils.setDummyData(getActivity(), listView); listView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.container)); if (parentActivity instanceof ObservableScrollViewCallbacks) { listView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2RecyclerViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; public class ViewPagerTab2RecyclerViewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_recyclerview, container, false); Activity parentActivity = getActivity(); final ObservableRecyclerView recyclerView = (ObservableRecyclerView) view.findViewById(R.id.scroll); recyclerView.setLayoutManager(new LinearLayoutManager(parentActivity)); recyclerView.setHasFixedSize(false); UiTestUtils.setDummyData(getActivity(), recyclerView); recyclerView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.container)); if (parentActivity instanceof ObservableScrollViewCallbacks) { recyclerView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ScrollViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; public class ViewPagerTab2ScrollViewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_scrollview_noheader, container, false); final ObservableScrollView scrollView = (ObservableScrollView) view.findViewById(R.id.scroll); Activity parentActivity = getActivity(); scrollView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.container)); if (parentActivity instanceof ObservableScrollViewCallbacks) { scrollView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2WebViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ObservableWebView; public class ViewPagerTab2WebViewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_webview, container, false); final ObservableWebView webView = (ObservableWebView) view.findViewById(R.id.scroll); webView.loadUrl("file:///android_asset/lipsum.html"); Activity parentActivity = getActivity(); webView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.container)); if (parentActivity instanceof ObservableScrollViewCallbacks) { webView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.Toolbar; import android.view.View; import com.github.ksoichiro.android.observablescrollview.CacheFragmentStatePagerAdapter; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; import com.github.ksoichiro.android.observablescrollview.Scrollable; import com.google.samples.apps.iosched.ui.widget.SlidingTabLayout; import com.nineoldandroids.view.ViewHelper; import com.nineoldandroids.view.ViewPropertyAnimator; public class ViewPagerTabActivity extends ActionBarActivity implements ObservableScrollViewCallbacks { private View mHeaderView; private View mToolbarView; private int mBaseTranslationY; private ViewPager mPager; private NavigationAdapter mPagerAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_viewpagertab); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); mHeaderView = findViewById(R.id.header); mToolbarView = findViewById(R.id.toolbar); mPagerAdapter = new NavigationAdapter(getSupportFragmentManager()); mPager = (ViewPager) findViewById(R.id.pager); mPager.setAdapter(mPagerAdapter); SlidingTabLayout slidingTabLayout = (SlidingTabLayout) findViewById(R.id.sliding_tabs); slidingTabLayout.setCustomTabView(R.layout.tab_indicator, android.R.id.text1); slidingTabLayout.setSelectedIndicatorColors(getResources().getColor(R.color.accent)); slidingTabLayout.setDistributeEvenly(true); slidingTabLayout.setViewPager(mPager); // When the page is selected, other fragments' scrollY should be adjusted // according to the toolbar status(shown/hidden) slidingTabLayout.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int i, float v, int i2) { } @Override public void onPageSelected(int i) { propagateToolbarState(toolbarIsShown()); } @Override public void onPageScrollStateChanged(int i) { } }); propagateToolbarState(toolbarIsShown()); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (dragging) { int toolbarHeight = mToolbarView.getHeight(); float currentHeaderTranslationY = ViewHelper.getTranslationY(mHeaderView); if (firstScroll) { if (-toolbarHeight < currentHeaderTranslationY) { mBaseTranslationY = scrollY; } } float headerTranslationY = ScrollUtils.getFloat(-(scrollY - mBaseTranslationY), -toolbarHeight, 0); ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewHelper.setTranslationY(mHeaderView, headerTranslationY); } } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { mBaseTranslationY = 0; Fragment fragment = getCurrentFragment(); if (fragment == null) { return; } View view = fragment.getView(); if (view == null) { return; } // ObservableXxxViews have same API // but currently they don't have any common interfaces. adjustToolbar(scrollState, view); } public Scrollable getCurrentScrollable() { Fragment fragment = getCurrentFragment(); if (fragment == null) { return null; } View view = fragment.getView(); if (view == null) { return null; } return (Scrollable) view.findViewById(R.id.scroll); } private void adjustToolbar(ScrollState scrollState, View view) { int toolbarHeight = mToolbarView.getHeight(); final Scrollable scrollView = (Scrollable) view.findViewById(R.id.scroll); if (scrollView == null) { return; } int scrollY = scrollView.getCurrentScrollY(); if (scrollState == ScrollState.DOWN) { showToolbar(); } else if (scrollState == ScrollState.UP) { if (toolbarHeight <= scrollY) { hideToolbar(); } else { showToolbar(); } } else { // Even if onScrollChanged occurs without scrollY changing, toolbar should be adjusted if (toolbarIsShown() || toolbarIsHidden()) { // Toolbar is completely moved, so just keep its state // and propagate it to other pages propagateToolbarState(toolbarIsShown()); } else { // Toolbar is moving but doesn't know which to move: // you can change this to hideToolbar() showToolbar(); } } } public Fragment getCurrentFragment() { return mPagerAdapter.getItemAt(mPager.getCurrentItem()); } private void propagateToolbarState(boolean isShown) { int toolbarHeight = mToolbarView.getHeight(); // Set scrollY for the fragments that are not created yet mPagerAdapter.setScrollY(isShown ? 0 : toolbarHeight); // Set scrollY for the active fragments for (int i = 0; i < mPagerAdapter.getCount(); i++) { // Skip current item if (i == mPager.getCurrentItem()) { continue; } // Skip destroyed or not created item Fragment f = mPagerAdapter.getItemAt(i); if (f == null) { continue; } View view = f.getView(); if (view == null) { continue; } propagateToolbarState(isShown, view, toolbarHeight); } } private void propagateToolbarState(boolean isShown, View view, int toolbarHeight) { Scrollable scrollView = (Scrollable) view.findViewById(R.id.scroll); if (scrollView == null) { return; } if (isShown) { // Scroll up if (0 < scrollView.getCurrentScrollY()) { scrollView.scrollVerticallyTo(0); } } else { // Scroll down (to hide padding) if (scrollView.getCurrentScrollY() < toolbarHeight) { scrollView.scrollVerticallyTo(toolbarHeight); } } } private boolean toolbarIsShown() { return ViewHelper.getTranslationY(mHeaderView) == 0; } private boolean toolbarIsHidden() { return ViewHelper.getTranslationY(mHeaderView) == -mToolbarView.getHeight(); } private void showToolbar() { float headerTranslationY = ViewHelper.getTranslationY(mHeaderView); if (headerTranslationY != 0) { ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewPropertyAnimator.animate(mHeaderView).translationY(0).setDuration(200).start(); } propagateToolbarState(true); } private void hideToolbar() { float headerTranslationY = ViewHelper.getTranslationY(mHeaderView); int toolbarHeight = mToolbarView.getHeight(); if (headerTranslationY != -toolbarHeight) { ViewPropertyAnimator.animate(mHeaderView).cancel(); ViewPropertyAnimator.animate(mHeaderView).translationY(-toolbarHeight).setDuration(200).start(); } propagateToolbarState(false); } /** * This adapter provides two types of fragments as an example. * {@linkplain #createItem(int)} should be modified if you use this example for your app. */ private static class NavigationAdapter extends CacheFragmentStatePagerAdapter { private static final String[] TITLES = new String[]{"Applepie", "Butter Cookie", "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb", "Ice Cream Sandwich", "Jelly Bean", "KitKat", "Lollipop"}; private int mScrollY; public NavigationAdapter(FragmentManager fm) { super(fm); } public void setScrollY(int scrollY) { mScrollY = scrollY; } @Override protected Fragment createItem(int position) { // Initialize fragments. // Please be sure to pass scroll position to each fragments using setArguments. Fragment f; final int pattern = position % 3; switch (pattern) { case 0: { f = new ViewPagerTabScrollViewFragment(); if (0 <= mScrollY) { Bundle args = new Bundle(); args.putInt(ViewPagerTabScrollViewFragment.ARG_SCROLL_Y, mScrollY); f.setArguments(args); } break; } case 1: { f = new ViewPagerTabListViewFragment(); if (0 < mScrollY) { Bundle args = new Bundle(); args.putInt(ViewPagerTabListViewFragment.ARG_INITIAL_POSITION, 1); f.setArguments(args); } break; } case 2: default: { f = new ViewPagerTabRecyclerViewFragment(); if (0 < mScrollY) { Bundle args = new Bundle(); args.putInt(ViewPagerTabRecyclerViewFragment.ARG_INITIAL_POSITION, 1); f.setArguments(args); } break; } } return f; } @Override public int getCount() { return TITLES.length; } @Override public CharSequence getPageTitle(int position) { return TITLES[position]; } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.test.ActivityInstrumentationTestCase2; import android.view.View; public class ViewPagerTabActivityTest extends ActivityInstrumentationTestCase2 { private ViewPagerTabActivity activity; public ViewPagerTabActivityTest() { super(ViewPagerTabActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); } public void testScroll() throws Throwable { for (int i = 0; i < 3; i++) { UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.LEFT); getInstrumentation().waitForIdleSync(); scroll(); } for (int i = 0; i < 3; i++) { UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.RIGHT); getInstrumentation().waitForIdleSync(); scroll(); } } public void scroll() throws Throwable { View scrollable = ((View) activity.getCurrentScrollable()).findViewById(R.id.scroll); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { for (int i = 0; i < 3; i++) { UiTestUtils.saveAndRestoreInstanceState(this, activity); scroll(); UiTestUtils.swipeHorizontally(this, activity.findViewById(R.id.pager), UiTestUtils.Direction.LEFT); getInstrumentation().waitForIdleSync(); } } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabListViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; public class ViewPagerTabListViewFragment extends Fragment { public static final String ARG_INITIAL_POSITION = "ARG_INITIAL_POSITION"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_listview, container, false); Activity parentActivity = getActivity(); final ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); UiTestUtils.setDummyDataWithHeader(getActivity(), listView, inflater.inflate(R.layout.padding, null)); if (parentActivity instanceof ObservableScrollViewCallbacks) { // Scroll to the specified position after layout Bundle args = getArguments(); if (args != null && args.containsKey(ARG_INITIAL_POSITION)) { final int initialPosition = args.getInt(ARG_INITIAL_POSITION, 0); ScrollUtils.addOnGlobalLayoutListener(listView, new Runnable() { @Override public void run() { // scrollTo() doesn't work, should use setSelection() listView.setSelection(initialPosition); } }); } listView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabRecyclerViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; public class ViewPagerTabRecyclerViewFragment extends Fragment { public static final String ARG_INITIAL_POSITION = "ARG_INITIAL_POSITION"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_recyclerview, container, false); Activity parentActivity = getActivity(); final ObservableRecyclerView recyclerView = (ObservableRecyclerView) view.findViewById(R.id.scroll); recyclerView.setLayoutManager(new LinearLayoutManager(parentActivity)); recyclerView.setHasFixedSize(false); View headerView = LayoutInflater.from(parentActivity).inflate(R.layout.padding, null); UiTestUtils.setDummyDataWithHeader(getActivity(), recyclerView, headerView); if (parentActivity instanceof ObservableScrollViewCallbacks) { // Scroll to the specified offset after layout Bundle args = getArguments(); if (args != null && args.containsKey(ARG_INITIAL_POSITION)) { final int initialPosition = args.getInt(ARG_INITIAL_POSITION, 0); ScrollUtils.addOnGlobalLayoutListener(recyclerView, new Runnable() { @Override public void run() { recyclerView.scrollVerticallyToPosition(initialPosition); } }); } recyclerView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabScrollViewFragment.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; public class ViewPagerTabScrollViewFragment extends Fragment { public static final String ARG_SCROLL_Y = "ARG_SCROLL_Y"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_scrollview, container, false); final ObservableScrollView scrollView = (ObservableScrollView) view.findViewById(R.id.scroll); Activity parentActivity = getActivity(); if (parentActivity instanceof ObservableScrollViewCallbacks) { // Scroll to the specified offset after layout Bundle args = getArguments(); if (args != null && args.containsKey(ARG_SCROLL_Y)) { final int scrollY = args.getInt(ARG_SCROLL_Y, 0); ScrollUtils.addOnGlobalLayoutListener(scrollView, new Runnable() { @Override public void run() { scrollView.scrollTo(0, scrollY); } }); } scrollView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); } return view; } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivity.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.os.Bundle; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ObservableWebView; import com.github.ksoichiro.android.observablescrollview.ScrollState; import com.github.ksoichiro.android.observablescrollview.Scrollable; public class WebViewActivity extends Activity implements ObservableScrollViewCallbacks { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_webview); ObservableWebView scrollable = (ObservableWebView) findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(this); scrollable.loadUrl("file:///android_asset/lipsum.html"); } @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } } ================================================ FILE: library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivityTest.java ================================================ package com.github.ksoichiro.android.observablescrollview.test; import android.app.Activity; import android.test.ActivityInstrumentationTestCase2; import android.util.DisplayMetrics; import android.util.TypedValue; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ObservableWebView; import com.github.ksoichiro.android.observablescrollview.ScrollState; public class WebViewActivityTest extends ActivityInstrumentationTestCase2 { private Activity activity; private ObservableWebView scrollable; private int[] callbackCounter; public WebViewActivityTest() { super(WebViewActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); activity = getActivity(); scrollable = (ObservableWebView) activity.findViewById(R.id.scrollable); callbackCounter = new int[2]; } public void testInitialize() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { new ObservableWebView(activity); new ObservableWebView(activity, null, 0); } }); } public void testScroll() throws Throwable { UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.UP); getInstrumentation().waitForIdleSync(); UiTestUtils.swipeVertically(this, scrollable, UiTestUtils.Direction.DOWN); getInstrumentation().waitForIdleSync(); } public void testSaveAndRestoreInstanceState() throws Throwable { UiTestUtils.saveAndRestoreInstanceState(this, activity); testScroll(); } public void testScrollVerticallyTo() throws Throwable { final DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, metrics)); } }); getInstrumentation().waitForIdleSync(); runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.scrollVerticallyTo(0); } }); getInstrumentation().waitForIdleSync(); } public void testNoCallbacks() throws Throwable { runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableWebView) activity.findViewById(R.id.scrollable); scrollable.setScrollViewCallbacks(null); } }); testScroll(); } public void testCallbacks() throws Throwable { final ObservableScrollViewCallbacks[] callbacks = new ObservableScrollViewCallbacks[2]; callbackCounter[0] = 0; callbackCounter[1] = 0; runTestOnUiThread(new Runnable() { @Override public void run() { scrollable = (ObservableWebView) activity.findViewById(R.id.scrollable); callbacks[0] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[0]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[0]); callbacks[1] = new ObservableScrollViewCallbacks() { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { callbackCounter[1]++; } @Override public void onDownMotionEvent() { } @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { } }; scrollable.addScrollViewCallbacks(callbacks[1]); } }); testScroll(); // Assert that all the callbacks are enabled and get called. assertTrue(0 < callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Remove one of the callbacks and scroll again to assert it's really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.removeScrollViewCallbacks(callbacks[0]); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 < callbackCounter[1]); // Clear all callbacks and assert they're really removed. runTestOnUiThread(new Runnable() { @Override public void run() { scrollable.clearScrollViewCallbacks(); } }); callbackCounter[0] = 0; callbackCounter[1] = 0; testScroll(); assertTrue(0 == callbackCounter[0]); assertTrue(0 == callbackCounter[1]); } } ================================================ FILE: library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java ================================================ /* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.ui.widget; import android.content.Context; import android.graphics.Typeface; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TextView; /** * To be used with ViewPager to provide a tab indicator component which give constant feedback as to * the user's scroll progress. *

* To use the component, simply add it to your view hierarchy. Then in your * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call * {@link #setViewPager(android.support.v4.view.ViewPager)} providing it the ViewPager this layout is being used for. *

* The colors can be customized in two ways. The first and simplest is to provide an array of colors * via {@link #setSelectedIndicatorColors(int...)}. The * alternative is via the {@link com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer} interface which provides you complete control over * which color is used for any individual position. *

* The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, * providing the layout ID of your custom layout. */ public class SlidingTabLayout extends HorizontalScrollView { /** * Allows complete control over the colors drawn in the tab layout. Set with * {@link #setCustomTabColorizer(com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer)}. */ public interface TabColorizer { /** * @return return the color of the indicator used when {@code position} is selected. */ int getIndicatorColor(int position); } private static final int TITLE_OFFSET_DIPS = 24; private static final int TAB_VIEW_PADDING_DIPS = 16; private static final int TAB_VIEW_TEXT_SIZE_SP = 12; private int mTitleOffset; private int mTabViewLayoutId; private int mTabViewTextViewId; private boolean mDistributeEvenly; private ViewPager mViewPager; private SparseArray mContentDescriptions = new SparseArray(); private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; private final SlidingTabStrip mTabStrip; public SlidingTabLayout(Context context) { this(context, null); } public SlidingTabLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // Disable the Scroll Bar setHorizontalScrollBarEnabled(false); // Make sure that the Tab Strips fills this View setFillViewport(true); mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); mTabStrip = new SlidingTabStrip(context); addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } /** * Set the custom {@link com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer} to be used. * * If you only require simple custmisation then you can use * {@link #setSelectedIndicatorColors(int...)} to achieve * similar effects. */ public void setCustomTabColorizer(TabColorizer tabColorizer) { mTabStrip.setCustomTabColorizer(tabColorizer); } public void setDistributeEvenly(boolean distributeEvenly) { mDistributeEvenly = distributeEvenly; } /** * Sets the colors to be used for indicating the selected tab. These colors are treated as a * circular array. Providing one color will mean that all tabs are indicated with the same color. */ public void setSelectedIndicatorColors(int... colors) { mTabStrip.setSelectedIndicatorColors(colors); } /** * Set the {@link android.support.v4.view.ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are * required to set any {@link android.support.v4.view.ViewPager.OnPageChangeListener} through this method. This is so * that the layout can update it's scroll position correctly. * * @see android.support.v4.view.ViewPager#setOnPageChangeListener(android.support.v4.view.ViewPager.OnPageChangeListener) */ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { mViewPagerPageChangeListener = listener; } /** * Set the custom layout to be inflated for the tab views. * * @param layoutResId Layout id to be inflated * @param textViewId id of the {@link android.widget.TextView} in the inflated view */ public void setCustomTabView(int layoutResId, int textViewId) { mTabViewLayoutId = layoutResId; mTabViewTextViewId = textViewId; } /** * Sets the associated view pager. Note that the assumption here is that the pager content * (number of tabs and tab titles) does not change after this call has been made. */ public void setViewPager(ViewPager viewPager) { mTabStrip.removeAllViews(); mViewPager = viewPager; if (viewPager != null) { viewPager.setOnPageChangeListener(new InternalViewPagerListener()); populateTabStrip(); } } /** * Create a default view to be used for tabs. This is called if a custom tab view is not set via * {@link #setCustomTabView(int, int)}. */ protected TextView createDefaultTabView(Context context) { TextView textView = new TextView(context); textView.setGravity(Gravity.CENTER); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); textView.setTypeface(Typeface.DEFAULT_BOLD); textView.setLayoutParams(new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); TypedValue outValue = new TypedValue(); getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); textView.setBackgroundResource(outValue.resourceId); textView.setAllCaps(true); int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); textView.setPadding(padding, padding, padding, padding); return textView; } private void populateTabStrip() { final PagerAdapter adapter = mViewPager.getAdapter(); final OnClickListener tabClickListener = new TabClickListener(); for (int i = 0; i < adapter.getCount(); i++) { View tabView = null; TextView tabTitleView = null; if (mTabViewLayoutId != 0) { // If there is a custom tab view layout id set, try and inflate it tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, false); tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); } if (tabView == null) { tabView = createDefaultTabView(getContext()); } if (tabTitleView == null && TextView.class.isInstance(tabView)) { tabTitleView = (TextView) tabView; } if (mDistributeEvenly) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); lp.width = 0; lp.weight = 1; } tabTitleView.setText(adapter.getPageTitle(i)); tabView.setOnClickListener(tabClickListener); String desc = mContentDescriptions.get(i, null); if (desc != null) { tabView.setContentDescription(desc); } mTabStrip.addView(tabView); if (i == mViewPager.getCurrentItem()) { tabView.setSelected(true); } } } public void setContentDescription(int i, String desc) { mContentDescriptions.put(i, desc); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mViewPager != null) { scrollToTab(mViewPager.getCurrentItem(), 0); } } private void scrollToTab(int tabIndex, int positionOffset) { final int tabStripChildCount = mTabStrip.getChildCount(); if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { return; } View selectedChild = mTabStrip.getChildAt(tabIndex); if (selectedChild != null) { int targetScrollX = selectedChild.getLeft() + positionOffset; if (tabIndex > 0 || positionOffset > 0) { // If we're not at the first child and are mid-scroll, make sure we obey the offset targetScrollX -= mTitleOffset; } scrollTo(targetScrollX, 0); } } private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { private int mScrollState; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { int tabStripChildCount = mTabStrip.getChildCount(); if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { return; } mTabStrip.onViewPagerPageChanged(position, positionOffset); View selectedTitle = mTabStrip.getChildAt(position); int extraOffset = (selectedTitle != null) ? (int) (positionOffset * selectedTitle.getWidth()) : 0; scrollToTab(position, extraOffset); if (mViewPagerPageChangeListener != null) { mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageScrollStateChanged(int state) { mScrollState = state; if (mViewPagerPageChangeListener != null) { mViewPagerPageChangeListener.onPageScrollStateChanged(state); } } @Override public void onPageSelected(int position) { if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { mTabStrip.onViewPagerPageChanged(position, 0f); scrollToTab(position, 0); } for (int i = 0; i < mTabStrip.getChildCount(); i++) { mTabStrip.getChildAt(i).setSelected(position == i); } if (mViewPagerPageChangeListener != null) { mViewPagerPageChangeListener.onPageSelected(position); } } } private class TabClickListener implements OnClickListener { @Override public void onClick(View v) { for (int i = 0; i < mTabStrip.getChildCount(); i++) { if (v == mTabStrip.getChildAt(i)) { mViewPager.setCurrentItem(i); return; } } } } } ================================================ FILE: library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java ================================================ /* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.ui.widget; import android.R; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; class SlidingTabStrip extends LinearLayout { private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; private final int mBottomBorderThickness; private final Paint mBottomBorderPaint; private final int mSelectedIndicatorThickness; private final Paint mSelectedIndicatorPaint; private final int mDefaultBottomBorderColor; private int mSelectedPosition; private float mSelectionOffset; private SlidingTabLayout.TabColorizer mCustomTabColorizer; private final SimpleTabColorizer mDefaultTabColorizer; SlidingTabStrip(Context context) { this(context, null); } SlidingTabStrip(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); final float density = getResources().getDisplayMetrics().density; TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); final int themeForegroundColor = outValue.data; mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); mDefaultTabColorizer = new SimpleTabColorizer(); mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); mBottomBorderPaint = new Paint(); mBottomBorderPaint.setColor(mDefaultBottomBorderColor); mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); mSelectedIndicatorPaint = new Paint(); } void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { mCustomTabColorizer = customTabColorizer; invalidate(); } void setSelectedIndicatorColors(int... colors) { // Make sure that the custom colorizer is removed mCustomTabColorizer = null; mDefaultTabColorizer.setIndicatorColors(colors); invalidate(); } void onViewPagerPageChanged(int position, float positionOffset) { mSelectedPosition = position; mSelectionOffset = positionOffset; invalidate(); } @Override protected void onDraw(Canvas canvas) { final int height = getHeight(); final int childCount = getChildCount(); final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null ? mCustomTabColorizer : mDefaultTabColorizer; // Thick colored underline below the current selection if (childCount > 0) { View selectedTitle = getChildAt(mSelectedPosition); int left = selectedTitle.getLeft(); int right = selectedTitle.getRight(); int color = tabColorizer.getIndicatorColor(mSelectedPosition); if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); if (color != nextColor) { color = blendColors(nextColor, color, mSelectionOffset); } // Draw the selection partway between the tabs View nextTitle = getChildAt(mSelectedPosition + 1); left = (int) (mSelectionOffset * nextTitle.getLeft() + (1.0f - mSelectionOffset) * left); right = (int) (mSelectionOffset * nextTitle.getRight() + (1.0f - mSelectionOffset) * right); } mSelectedIndicatorPaint.setColor(color); canvas.drawRect(left, height - mSelectedIndicatorThickness, right, height, mSelectedIndicatorPaint); } // Thin underline along the entire bottom edge canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); } /** * Set the alpha value of the {@code color} to be the given {@code alpha} value. */ private static int setColorAlpha(int color, byte alpha) { return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); } /** * Blend {@code color1} and {@code color2} using the given ratio. * * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, * 0.0 will return {@code color2}. */ private static int blendColors(int color1, int color2, float ratio) { final float inverseRation = 1f - ratio; float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); return Color.rgb((int) r, (int) g, (int) b); } private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { private int[] mIndicatorColors; @Override public final int getIndicatorColor(int position) { return mIndicatorColors[position % mIndicatorColors.length]; } void setIndicatorColors(int... colors) { mIndicatorColors = colors; } } } ================================================ FILE: library/src/androidTest/res/color/tab_text_color.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_gridview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_listview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_recyclerview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_scrollview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_touchinterception_gridview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_touchinterception_listview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_touchinterception_recyclerview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_touchinterception_scrollview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_touchinterception_webview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_viewpagertab.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_viewpagertab2.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/activity_webview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_gridview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_listview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_recyclerview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_scrollview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_scrollview_noheader.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/fragment_webview.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/padding.xml ================================================ ================================================ FILE: library/src/androidTest/res/layout/tab_indicator.xml ================================================ ================================================ FILE: library/src/androidTest/res/values/colors.xml ================================================ #009688 #00796b #eeff41 ================================================ FILE: library/src/androidTest/res/values/dimens.xml ================================================ 48dp 72dp 56dp 16dp 240dp ================================================ FILE: library/src/androidTest/res/values/strings.xml ================================================ Lorem ipsum dolor sit amet, ut duis lorem provident sed felis blandit, condimentum donec lectus ipsum et mauris, morbi porttitor interdum feugiat nulla donec sodales, vestibulum nisl primis a molestie vestibulum quam, sapien mauris metus risus suspendisse magnis. Augue viverra nulla faucibus egestas eu, a etiam id congue rutrum ante, arcu tincidunt donec quam felis at ornare, iaculis ligula sodales venenatis commodo volutpat neque, suspendisse elit praesent tellus felis mi amet. Inceptos amet tempor lectus lorem est non, ac donec ac libero neque mauris, tellus ante metus eget leo consequat. Scelerisque dolor curabitur pretium blandit ut feugiat, amet lacus pulvinar justo convallis ut, sed natoque ipsum urna posuere nibh eu. Sed at sed vulputate sit orci, facilisis a aliquam tellus quam aliquam, eu aliquam donec at molestie ante, pellentesque mauris lorem ultrices libero faucibus porta, imperdiet adipiscing sit hac diam ut nulla. Lacus enim elit pulvinar donec vehicula dapibus, accumsan purus officia cursus dolor sapien, eu amet dis mauris mi nulla ut. Non accusamus etiam pede non urna tempus, vestibulum aliquam tortor eget pharetra sodales, in vestibulum ut justo orci nulla, lobortis purus sem semper consectetuer magni purus. Dolor a leo vestibulum amet ut sit, arcu ut eaque urna fusce aliquet turpis, sed fermentum sed vestibulum nisl pede, tristique enim lorem posuere in laborum ut. Vestibulum id id justo leo nulla, magna lobortis ullamcorper et dignissim pellentesque, duis suspendisse quis id lorem ante. Vivamus a nullam ante adipiscing amet, mi vel consectetuer nunc aenean pede quisque, eget rhoncus dis porttitor habitant nunc vivamus, duis cubilia blandit non donec justo dictumst, praesent vitae nulla nam pulvinar urna. Adipiscing adipiscing justo urna pulvinar imperdiet nullam, vitae fusce rhoncus proin nonummy suscipit, ullamcorper amet et non potenti platea ultrices, mauris nullam sapien nunc justo vel, eu semper pellentesque arcu fusce augue. Malesuada mauris nibh sit a a scelerisque, velit sem lectus tellus convallis consectetuer, ultricies auctor a ante eros amet sed.\n\n Risus lacus duis leo platea wisi, felis maecenas rutrum in id in donec, non id a potenti libero eget, posuere elit ea sed pellentesque quis. Sunt lacus urna lorem elit duis, nibh donec purus quisque consectetuer dolor, neque vestibulum proin ornare eros nonummy phasellus. Iaculis cras eu at egestas dolor montes, viverra quisque malesuada consectetuer semper maecenas, a sed vitae donec tempor aliqua metus, ornare mollis suscipit et erat fusce, sit orci aut auctor elementum fames aliquam. Platea dui integer magnis non metus, minus dignissimos ante massa nostra et, rutrum sapien egestas quis sapien donec donec. Erat sit a eros aenean natoque, quam libero id lorem enim proin, lorem ipsum fermentum mattis metus et. Aliquam aliquet suscipit purus conubia at neque, platea vivamus vestibulum nulla quibusdam senectus, et morbi lectus malesuada gravida donec, elementum sit convallis pellentesque velit amet. Et eveniet viverra vehicula consectetuer justo, provident sed commodo non lacinia velit, tempor phasellus vel leo nisl cras, vivamus et arcu interdum dui eu amet. Volutpat wisi rhoncus vel turpis diam quibusdam, dapibus elit est quisque cubilia mauris, nulla elit magna tempor accumsan bibendum, lorem varius sed interdum eget mattis, scelerisque egestas feugiat donec dui molestie. Leo facilisis nisl sit montes ligula sed, enim commodo consectetuer nunc est et, ut sed vehicula dolor luctus elit. Fermentum cras donec eget nibh est vel, sed justo risus et pharetra diam, eu vivamus egestas ligula risus diam, sed justo eget hac ut mauris. Vestibulum diam nec vitae mi eget suspendisse, aenean arcu purus facilisis purus class in, id aliquam sit id scelerisque sapien etiam. Ut nullam sit sed at mauris lobortis, consequat dolor autem ipsum euismod nulla, elit quis proin eget conubia varius, erat arcu massa mus in mauris, scelerisque ut eu sollicitudin libero leo urna.\n\n Consectetuer luctus tempor elit ut dolor ligula, quis dui per dui hendrerit ante sagittis, in quisque pretium in eleifend enim. Condimentum iaculis vitae feugiat dis tellus vel, lectus dolor nec dui nulla nascetur, et pellentesque curabitur lorem leo velit eget. Id nascetur arcu lobortis suspendisse imperdiet urna, natoque nascetur ante in porta a, interdum hendrerit mi bibendum platea tellus, urna in enim ornare vestibulum faucibus enim. Leo fusce egestas ante nec volutpat, in tempor vel facilisis potenti ut, pede at non lorem a commodo, nulla dolor orci interdum vestibulum nulla. Dui nulla vestibulum quisque a pharetra porta, integer nec ipsum nec sed dui pharetra, magna et dignissim ipsum sed dictum, litora eros vivamus scelerisque libero ipsum. Sed ac ac lorem molestie adipiscing morbi, pellentesque imperdiet nunc quis morbi amet ante, libero dui ligula nec risus neque et, velit nonummy phasellus et facilisi amet, ligula in elementum non sapien pulvinar faucibus. Eu leo ut posuere sed aliquet, tincidunt vel urna volutpat tempus sem, sit felis aliquet vestibulum condimentum sit, amet nibh vel tellus purus ullamcorper libero, nulla vestibulum pede ut vestibulum pretium. Eu nulla vestibulum a neque in metus, quisquam nam sed cursus eget luctus, pede ultrices nec sed dignissim pellentesque, sit class cursus metus nulla placerat mauris, consequat mollis neque vivamus amet pede. Mauris dolor nulla diam eros bibendum, quam ante vestibulum morbi non ligula vel, molestie curabitur rhoncus nulla euismod interdum non. Nulla fringilla lorem mollis ad massa, sit molestie nibh lorem arcu volutpat, accumsan commodo lectus eu et donec, sit tempor tempus rutrum in curabitur amet. Nec urna euismod a tincidunt commodo, eu pede turpis libero vitae viverra, ante vestibulum nam non habitasse potenti, mauris imperdiet in in nunc convallis. Et nostra wisi in est accumsan vehicula, quisque vitae felis mauris sed vulputate nec, ante imperdiet sollicitudin massa iaculis massa sit.\n\n Quam libero nulla netus eu porta curae, ut nulla bibendum facilisis et urna sed, quis congue vestibulum aliquam interdum etiam. Nulla vel lobortis ullamcorper vitae excepturi, neque urna feugiat lectus vel lacinia, massa pretium orci eu metus neque vulputate. Imperdiet ac velit rhoncus nulla malesuada nullam, nec pulvinar justo gravida lorem rutrum magna, habitasse repudiandae mi eros vestibulum ante, nec euismod dui iaculis in turpis pretium, ac id metus egestas proin lacus lectus. Laoreet lorem nec vitae risus erat arcu, vitae quam ut in ante tristique, porta dolor pede quam et odio nam, arcu lacus sem congue ante cursus massa. Et mattis sagittis erat accumsan fusce quam, vehicula ligula beatae natoque fusce sodales conubia, habitasse metus cum magnis viverra nam cursus, egestas urna wisi primis blandit eu magna, eget libero elit lacus lorem dis aliquam. Ut mauris ante natoque lacus massa, justo a lectus sodales enim adipiscing id, accumsan ut ipsum vestibulum sed enim auctor, vitae congue tincidunt id phasellus lacinia scelerisque, tincidunt sapien nulla euismod volutpat iaculis. Platea sociis nec aliquet nec molestie, in mi et augue sapien in vivamus, integer fames proin vitae in ullamcorper et. Fringilla etiam sapiente rhoncus suspendisse nec id, lobortis cras eget egestas dui ac nec, justo lacus ut lorem bibendum quia eros, eget a gravida id donec nunc suscipit, porta sed in sodales non rutrum. Lectus vel dui elementum pellentesque magna aliquam, vitae non sit pede et fusce nibh, id id deserunt ornare dui sit condimentum, in adipiscing imperdiet turpis nam aliquet, facilisis metus magna lacus wisi facilisis tortor. Vulputate elit accumsan quam amet ligula, suspendisse lacus mi nonummy integer urna, libero nulla nunc varius in odio, laoreet nulla amet placerat amet nec. Consectetuer vel massa hendrerit vitae iaculis id, sed ut ut laudantium odio in, elit vestibulum duis ante maecenas interdum in, neque vehicula ultrices varius in quam, pede tellus pellentesque sed nullam quis. ================================================ FILE: library/src/androidTest/res/values/styles.xml ================================================ ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.util.SparseArray; import android.view.ViewGroup; /** * FragmentStatePagerAdapter that caches each pages. *

FragmentStatePagerAdapter is also originally caches pages, * but its keys are not public nor documented, so depending * on how it create cache key is dangerous.

*

This adapter caches pages by itself and provide getter method to the cache.

*/ public abstract class CacheFragmentStatePagerAdapter extends FragmentStatePagerAdapter { private static final String STATE_SUPER_STATE = "superState"; private static final String STATE_PAGES = "pages"; private static final String STATE_PAGE_INDEX_PREFIX = "pageIndex:"; private static final String STATE_PAGE_KEY_PREFIX = "page:"; private FragmentManager mFm; private SparseArray mPages; public CacheFragmentStatePagerAdapter(FragmentManager fm) { super(fm); mPages = new SparseArray<>(); mFm = fm; } @Override public Parcelable saveState() { Parcelable p = super.saveState(); Bundle bundle = new Bundle(); bundle.putParcelable(STATE_SUPER_STATE, p); bundle.putInt(STATE_PAGES, mPages.size()); if (0 < mPages.size()) { for (int i = 0; i < mPages.size(); i++) { int position = mPages.keyAt(i); bundle.putInt(createCacheIndex(i), position); Fragment f = mPages.get(position); mFm.putFragment(bundle, createCacheKey(position), f); } } return bundle; } @Override public void restoreState(Parcelable state, ClassLoader loader) { Bundle bundle = (Bundle) state; int pages = bundle.getInt(STATE_PAGES); if (0 < pages) { for (int i = 0; i < pages; i++) { int position = bundle.getInt(createCacheIndex(i)); Fragment f = mFm.getFragment(bundle, createCacheKey(position)); mPages.put(position, f); } } Parcelable p = bundle.getParcelable(STATE_SUPER_STATE); super.restoreState(p, loader); } /** * Get a new Fragment instance. *

Each fragments are automatically cached in this method, * so you don't have to do it by yourself. * If you want to implement instantiation of Fragments, * you should override {@link #createItem(int)} instead.

*

* {@inheritDoc} * * @param position Position of the item in the adapter. * @return Fragment instance. */ @Override public Fragment getItem(int position) { Fragment f = createItem(position); // We should cache fragments manually to access to them later mPages.put(position, f); return f; } @Override public void destroyItem(ViewGroup container, int position, Object object) { if (0 <= mPages.indexOfKey(position)) { mPages.remove(position); } super.destroyItem(container, position, object); } /** * Get the item at the specified position in the adapter. * * @param position Position of the item in the adapter. * @return Fragment instance. */ public Fragment getItemAt(int position) { return mPages.get(position); } /** * Create a new Fragment instance. * This is called inside {@link #getItem(int)}. * * @param position Position of the item in the adapter. * @return Fragment instance. */ protected abstract Fragment createItem(int position); /** * Create an index string for caching Fragment pages. * * @param index Index of the item in the adapter. * @return Key string for caching Fragment pages. */ protected String createCacheIndex(int index) { return STATE_PAGE_INDEX_PREFIX + index; } /** * Create a key string for caching Fragment pages. * * @param position Position of the item in the adapter. * @return Key string for caching Fragment pages. */ protected String createCacheKey(int position) { return STATE_PAGE_KEY_PREFIX + position; } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * Copyright (C) 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.content.Context; import android.database.DataSetObservable; import android.database.DataSetObserver; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.Filter; import android.widget.Filterable; import android.widget.FrameLayout; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.WrapperListAdapter; import java.util.ArrayList; import java.util.List; /** * GridView that its scroll position can be observed. */ public class ObservableGridView extends GridView implements Scrollable { // Fields that should be saved onSaveInstanceState private int mPrevFirstVisiblePosition; private int mPrevFirstVisibleChildHeight = -1; private int mPrevScrolledChildrenHeight; private int mPrevScrollY; private int mScrollY; private SparseIntArray mChildrenHeights; // Fields that don't need to be saved onSaveInstanceState private ObservableScrollViewCallbacks mCallbacks; private List mCallbackCollection; private ScrollState mScrollState; private boolean mFirstScroll; private boolean mDragging; private boolean mIntercepted; private MotionEvent mPrevMoveEvent; private ViewGroup mTouchInterceptionViewGroup; private ArrayList mHeaderViewInfos; private ArrayList mFooterViewInfos; private OnScrollListener mOriginalScrollListener; private OnScrollListener mScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mOriginalScrollListener != null) { mOriginalScrollListener.onScrollStateChanged(view, scrollState); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mOriginalScrollListener != null) { mOriginalScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } // AbsListView#invokeOnItemScrollListener calls onScrollChanged(0, 0, 0, 0) // on Android 4.0+, but Android 2.3 is not. (Android 3.0 is unknown) // So call it with onScrollListener. onScrollChanged(); } }; public ObservableGridView(Context context) { super(context); init(); } public ObservableGridView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ObservableGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; mPrevFirstVisiblePosition = ss.prevFirstVisiblePosition; mPrevFirstVisibleChildHeight = ss.prevFirstVisibleChildHeight; mPrevScrolledChildrenHeight = ss.prevScrolledChildrenHeight; mPrevScrollY = ss.prevScrollY; mScrollY = ss.scrollY; mChildrenHeights = ss.childrenHeights; super.onRestoreInstanceState(ss.getSuperState()); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.prevFirstVisiblePosition = mPrevFirstVisiblePosition; ss.prevFirstVisibleChildHeight = mPrevFirstVisibleChildHeight; ss.prevScrolledChildrenHeight = mPrevScrolledChildrenHeight; ss.prevScrollY = mPrevScrollY; ss.scrollY = mScrollY; ss.childrenHeights = mChildrenHeights; return ss; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onInterceptTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Whether or not motion events are consumed by children, // flag initializations which are related to ACTION_DOWN events should be executed. // Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are // passed to parent (this view), the flags will be invalid. // Also, applications might implement initialization codes to onDownMotionEvent, // so call it here. mFirstScroll = mDragging = true; dispatchOnDownMotionEvent(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIntercepted = false; mDragging = false; dispatchOnUpOrCancelMotionEvent(mScrollState); break; case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { // Can't scroll anymore. if (mIntercepted) { // Already dispatched ACTION_DOWN event to parents, so stop here. return false; } // Apps can set the interception target other than the direct parent. final ViewGroup parent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } // Get offset to parents. If the parent is not the direct parent, // we should aggregate offsets from all of the parents. float offsetX = 0; float offsetY = 0; for (View v = this; v != null && v != parent; v = (View) v.getParent()) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); } final MotionEvent event = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; // If the parent wants to intercept ACTION_MOVE events, // we pass ACTION_DOWN event to the parent // as if these touch events just have began now. event.setAction(MotionEvent.ACTION_DOWN); // Return this onTouchEvent() first and set ACTION_DOWN event for parent // to the queue, to keep events sequence. post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); } }); return false; } // Even when this can't be scrolled anymore, // simply returning false here may cause subView's click, // so delegate it to super. return super.onTouchEvent(ev); } break; } return super.onTouchEvent(ev); } public void addFooterView(View v) { addFooterView(v, null, true); } public void addFooterView(View v, Object data, boolean isSelectable) { ListAdapter mAdapter = getAdapter(); if (mAdapter != null && !(mAdapter instanceof HeaderViewGridAdapter)) { throw new IllegalStateException( "Cannot add header view to grid -- setAdapter has already been called."); } ViewGroup.LayoutParams lyp = v.getLayoutParams(); FixedViewInfo info = new FixedViewInfo(); FrameLayout fl = new FullWidthFixedViewLayout(getContext()); if (lyp != null) { v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); fl.setLayoutParams(new AbsListView.LayoutParams(lyp.width, lyp.height)); } fl.addView(v); info.view = v; info.viewContainer = fl; info.data = data; info.isSelectable = isSelectable; mFooterViewInfos.add(info); if (mAdapter != null) { ((HeaderViewGridAdapter) mAdapter).notifyDataSetChanged(); } } public int getFooterViewCount() { return mFooterViewInfos.size(); } public boolean removeFooterView(View v) { if (mFooterViewInfos.size() > 0) { boolean result = false; ListAdapter adapter = getAdapter(); if (adapter != null && ((HeaderViewGridAdapter) adapter).removeFooter(v)) { result = true; } removeFixedViewInfo(v, mFooterViewInfos); return result; } return false; } @Override public void setOnScrollListener(OnScrollListener l) { // Don't set l to super.setOnScrollListener(). // l receives all events through mScrollListener. mOriginalScrollListener = l; } @Override public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection == null) { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection != null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection != null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { scrollTo(0, y); } @Override public int getCurrentScrollY() { return mScrollY; } @Override public void setClipChildren(boolean clipChildren) { // Ignore, since the header rows depend on not being clipped } @Override public void setAdapter(ListAdapter adapter) { if (0 < mHeaderViewInfos.size()) { HeaderViewGridAdapter headerViewGridAdapter = new HeaderViewGridAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); int numColumns = getNumColumnsCompat(); if (1 < numColumns) { headerViewGridAdapter.setNumColumns(numColumns); } super.setAdapter(headerViewGridAdapter); } else { super.setAdapter(adapter); } } public void addHeaderView(View v, Object data, boolean isSelectable) { ListAdapter adapter = getAdapter(); if (adapter != null && !(adapter instanceof HeaderViewGridAdapter)) { throw new IllegalStateException("Cannot add header view to grid -- setAdapter has already been called."); } FixedViewInfo info = new FixedViewInfo(); FrameLayout fl = new FullWidthFixedViewLayout(getContext()); fl.addView(v); info.view = v; info.viewContainer = fl; info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); // in the case of re-adding a header view, or adding one later on, // we need to notify the observer if (adapter != null) { ((HeaderViewGridAdapter) adapter).notifyDataSetChanged(); } } public void addHeaderView(View v) { addHeaderView(v, null, true); } public int getHeaderViewCount() { return mHeaderViewInfos.size(); } public boolean removeHeaderView(View v) { if (mHeaderViewInfos.size() > 0) { boolean result = false; ListAdapter adapter = getAdapter(); if (adapter != null && adapter instanceof HeaderViewGridAdapter && ((HeaderViewGridAdapter) adapter).removeHeader(v)) { result = true; } removeFixedViewInfo(v, mHeaderViewInfos); return result; } return false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ListAdapter adapter = getAdapter(); if (adapter != null && adapter instanceof HeaderViewGridAdapter) { ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumnsCompat()); } } private void init() { mChildrenHeights = new SparseIntArray(); mHeaderViewInfos = new ArrayList<>(); mFooterViewInfos = new ArrayList<>(); super.setClipChildren(false); super.setOnScrollListener(mScrollListener); } private int getNumColumnsCompat() { if (Build.VERSION.SDK_INT >= 11) { return getNumColumns(); } else { int columns = 0; if (getChildCount() > 0) { int width = getChildAt(0).getMeasuredWidth(); if (width > 0) { columns = getWidth() / width; } } return columns > 0 ? columns : AUTO_FIT; } } private void onScrollChanged() { if (hasNoCallbacks()) { return; } if (getChildCount() > 0) { int firstVisiblePosition = getFirstVisiblePosition(); for (int i = getFirstVisiblePosition(), j = 0; i <= getLastVisiblePosition(); i++, j++) { if (mChildrenHeights.indexOfKey(i) < 0 || getChildAt(j).getHeight() != mChildrenHeights.get(i)) { if (i % getNumColumnsCompat() == 0) { mChildrenHeights.put(i, getChildAt(j).getHeight()); } } } View firstVisibleChild = getChildAt(0); if (firstVisibleChild != null) { if (mPrevFirstVisiblePosition < firstVisiblePosition) { // scroll down int skippedChildrenHeight = 0; if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) { for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } } } mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition < mPrevFirstVisiblePosition) { // scroll up int skippedChildrenHeight = 0; if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) { for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } } } mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition == 0) { mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); mPrevScrolledChildrenHeight = 0; } if (mPrevFirstVisibleChildHeight < 0) { mPrevFirstVisibleChildHeight = 0; } mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop() + getPaddingTop(); mPrevFirstVisiblePosition = firstVisiblePosition; dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < mScrollY) { mScrollState = ScrollState.UP; } else if (mScrollY < mPrevScrollY) { mScrollState = ScrollState.DOWN; } else { mScrollState = ScrollState.STOP; } mPrevScrollY = mScrollY; } } } private void removeFixedViewInfo(View v, ArrayList where) { int len = where.size(); for (int i = 0; i < len; ++i) { FixedViewInfo info = where.get(i); if (info.view == v) { where.remove(i); break; } } } private boolean hasNoCallbacks() { return mCallbacks == null && mCallbackCollection == null; } private class FullWidthFixedViewLayout extends FrameLayout { public FullWidthFixedViewLayout(Context context) { super(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int targetWidth = ObservableGridView.this.getMeasuredWidth() - ObservableGridView.this.getPaddingLeft() - ObservableGridView.this.getPaddingRight(); widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.getMode(widthMeasureSpec)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } static class SavedState extends BaseSavedState { int prevFirstVisiblePosition; int prevFirstVisibleChildHeight = -1; int prevScrolledChildrenHeight; int prevScrollY; int scrollY; SparseIntArray childrenHeights; /** * Called by onSaveInstanceState. */ SavedState(Parcelable superState) { super(superState); } /** * Called by CREATOR. */ private SavedState(Parcel in) { super(in); prevFirstVisiblePosition = in.readInt(); prevFirstVisibleChildHeight = in.readInt(); prevScrolledChildrenHeight = in.readInt(); prevScrollY = in.readInt(); scrollY = in.readInt(); childrenHeights = new SparseIntArray(); final int numOfChildren = in.readInt(); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { final int key = in.readInt(); final int value = in.readInt(); childrenHeights.put(key, value); } } } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(prevFirstVisiblePosition); out.writeInt(prevFirstVisibleChildHeight); out.writeInt(prevScrolledChildrenHeight); out.writeInt(prevScrollY); out.writeInt(scrollY); final int numOfChildren = childrenHeights == null ? 0 : childrenHeights.size(); out.writeInt(numOfChildren); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { out.writeInt(childrenHeights.keyAt(i)); out.writeInt(childrenHeights.valueAt(i)); } } } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } private void dispatchOnDownMotionEvent() { if (mCallbacks != null) { mCallbacks.onDownMotionEvent(); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onDownMotionEvent(); } } } private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (mCallbacks != null) { mCallbacks.onScrollChanged(scrollY, firstScroll, dragging); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onScrollChanged(scrollY, firstScroll, dragging); } } } private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) { if (mCallbacks != null) { mCallbacks.onUpOrCancelMotionEvent(scrollState); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onUpOrCancelMotionEvent(scrollState); } } } public static class FixedViewInfo { public View view; public ViewGroup viewContainer; public Object data; public boolean isSelectable; } public static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable { private final DataSetObservable mDataSetObservable = new DataSetObservable(); private final ListAdapter mAdapter; static final ArrayList EMPTY_INFO_LIST = new ArrayList<>(); // This ArrayList is assumed to NOT be null. ArrayList mHeaderViewInfos; ArrayList mFooterViewInfos; private int mNumColumns = 1; private int mRowHeight = -1; boolean mAreAllFixedViewsSelectable; private final boolean mIsFilterable; private boolean mCachePlaceHoldView = true; // From Recycle Bin or calling getView, this a question... private boolean mCacheFirstHeaderView = false; public HeaderViewGridAdapter(ArrayList headerViewInfos, ArrayList footViewInfos, ListAdapter adapter) { mAdapter = adapter; mIsFilterable = adapter instanceof Filterable; if (headerViewInfos == null) { mHeaderViewInfos = EMPTY_INFO_LIST; } else { mHeaderViewInfos = headerViewInfos; } if (footViewInfos == null) { mFooterViewInfos = EMPTY_INFO_LIST; } else { mFooterViewInfos = footViewInfos; } mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); } public int getNumColumns() { return mNumColumns; } public void setNumColumns(int numColumns) { if (numColumns < 1) { return; } if (mNumColumns != numColumns) { mNumColumns = numColumns; notifyDataSetChanged(); } } public void setRowHeight(int height) { mRowHeight = height; } public int getHeadersCount() { return mHeaderViewInfos.size(); } public int getFootersCount() { return mFooterViewInfos.size(); } /** * @return True if this adapter doesn't contain any data. This is used to determine * whether the empty view should be displayed. A typical implementation will return * getCount() == 0 but since getCount() includes the headers and footers, specialized * adapters might want a different behavior. */ @Override public boolean isEmpty() { return (mAdapter == null || mAdapter.isEmpty()); } private boolean areAllListInfosSelectable(ArrayList infos) { if (infos != null) { for (FixedViewInfo info : infos) { if (!info.isSelectable) { return false; } } } return true; } public boolean removeHeader(View v) { for (int i = 0; i < mHeaderViewInfos.size(); i++) { FixedViewInfo info = mHeaderViewInfos.get(i); if (info.view == v) { mHeaderViewInfos.remove(i); mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); mDataSetObservable.notifyChanged(); return true; } } return false; } public boolean removeFooter(View v) { for (int i = 0; i < mFooterViewInfos.size(); i++) { FixedViewInfo info = mFooterViewInfos.get(i); if (info.view == v) { mFooterViewInfos.remove(i); mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); mDataSetObservable.notifyChanged(); return true; } } return false; } @Override public int getCount() { if (mAdapter != null) { return (getFootersCount() + getHeadersCount()) * mNumColumns + getAdapterAndPlaceHolderCount(); } else { return (getFootersCount() + getHeadersCount()) * mNumColumns; } } @Override public boolean areAllItemsEnabled() { return mAdapter == null || mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); } private int getAdapterAndPlaceHolderCount() { return (int) (Math.ceil(1f * mAdapter.getCount() / mNumColumns) * mNumColumns); } @Override public boolean isEnabled(int position) { // Header (negative positions will throw an IndexOutOfBoundsException) int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; if (position < numHeadersAndPlaceholders) { return position % mNumColumns == 0 && mHeaderViewInfos.get(position / mNumColumns).isSelectable; } // Adapter final int adjPosition = position - numHeadersAndPlaceholders; int adapterCount = 0; if (mAdapter != null) { adapterCount = getAdapterAndPlaceHolderCount(); if (adjPosition < adapterCount) { return adjPosition < mAdapter.getCount() && mAdapter.isEnabled(adjPosition); } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) final int footerPosition = adjPosition - adapterCount; return footerPosition % mNumColumns == 0 && mFooterViewInfos.get(footerPosition / mNumColumns).isSelectable; } @Override public Object getItem(int position) { // Header (negative positions will throw an ArrayIndexOutOfBoundsException) int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; if (position < numHeadersAndPlaceholders) { if (position % mNumColumns == 0) { return mHeaderViewInfos.get(position / mNumColumns).data; } return null; } // Adapter final int adjPosition = position - numHeadersAndPlaceholders; int adapterCount = 0; if (mAdapter != null) { adapterCount = getAdapterAndPlaceHolderCount(); if (adjPosition < adapterCount) { if (adjPosition < mAdapter.getCount()) { return mAdapter.getItem(adjPosition); } else { return null; } } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) final int footerPosition = adjPosition - adapterCount; if (footerPosition % mNumColumns == 0) { return mFooterViewInfos.get(footerPosition).data; } else { return null; } } @Override public long getItemId(int position) { int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; if (mAdapter != null && position >= numHeadersAndPlaceholders) { int adjPosition = position - numHeadersAndPlaceholders; int adapterCount = mAdapter.getCount(); if (adjPosition < adapterCount) { return mAdapter.getItemId(adjPosition); } } return -1; } @Override public boolean hasStableIds() { return mAdapter != null && mAdapter.hasStableIds(); } @Override public View getView(int position, View convertView, ViewGroup parent) { // Header (negative positions will throw an ArrayIndexOutOfBoundsException) int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; if (position < numHeadersAndPlaceholders) { View headerViewContainer = mHeaderViewInfos .get(position / mNumColumns).viewContainer; if (position % mNumColumns == 0) { return headerViewContainer; } else { if (convertView == null) { convertView = new View(parent.getContext()); } // We need to do this because GridView uses the height of the last item // in a row to determine the height for the entire row. convertView.setVisibility(View.INVISIBLE); convertView.setMinimumHeight(headerViewContainer.getHeight()); return convertView; } } // Adapter final int adjPosition = position - numHeadersAndPlaceholders; int adapterCount = 0; if (mAdapter != null) { adapterCount = getAdapterAndPlaceHolderCount(); if (adjPosition < adapterCount) { if (adjPosition < mAdapter.getCount()) { return mAdapter.getView(adjPosition, convertView, parent); } else { if (convertView == null) { convertView = new View(parent.getContext()); } convertView.setVisibility(View.INVISIBLE); convertView.setMinimumHeight(mRowHeight); return convertView; } } } // Footer final int footerPosition = adjPosition - adapterCount; if (footerPosition < getCount()) { View footViewContainer = mFooterViewInfos .get(footerPosition / mNumColumns).viewContainer; if (position % mNumColumns == 0) { return footViewContainer; } else { if (convertView == null) { convertView = new View(parent.getContext()); } // We need to do this because GridView uses the height of the last item // in a row to determine the height for the entire row. convertView.setVisibility(View.INVISIBLE); convertView.setMinimumHeight(footViewContainer.getHeight()); return convertView; } } throw new ArrayIndexOutOfBoundsException(position); } @Override public int getItemViewType(int position) { final int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; final int adapterViewTypeStart = mAdapter == null ? 0 : mAdapter.getViewTypeCount() - 1; int type = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; if (mCachePlaceHoldView) { // Header if (position < numHeadersAndPlaceholders) { if (position == 0) { if (mCacheFirstHeaderView) { type = adapterViewTypeStart + mHeaderViewInfos.size() + mFooterViewInfos.size() + 1 + 1; } } if (position % mNumColumns != 0) { type = adapterViewTypeStart + (position / mNumColumns + 1); } } } // Adapter final int adjPosition = position - numHeadersAndPlaceholders; int adapterCount = 0; if (mAdapter != null) { adapterCount = getAdapterAndPlaceHolderCount(); if (adjPosition >= 0 && adjPosition < adapterCount) { if (adjPosition < mAdapter.getCount()) { type = mAdapter.getItemViewType(adjPosition); } else { if (mCachePlaceHoldView) { type = adapterViewTypeStart + mHeaderViewInfos.size() + 1; } } } } if (mCachePlaceHoldView) { // Footer final int footerPosition = adjPosition - adapterCount; if (footerPosition >= 0 && footerPosition < getCount() && (footerPosition % mNumColumns) != 0) { type = adapterViewTypeStart + mHeaderViewInfos.size() + 1 + (footerPosition / mNumColumns + 1); } } return type; } /** * Content view, content view holder, header[0], header and footer placeholder(s). */ @Override public int getViewTypeCount() { int count = mAdapter == null ? 1 : mAdapter.getViewTypeCount(); if (mCachePlaceHoldView) { int offset = mHeaderViewInfos.size() + 1 + mFooterViewInfos.size(); if (mCacheFirstHeaderView) { offset += 1; } count += offset; } return count; } @Override public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); if (mAdapter != null) { mAdapter.registerDataSetObserver(observer); } } @Override public void unregisterDataSetObserver(DataSetObserver observer) { mDataSetObservable.unregisterObserver(observer); if (mAdapter != null) { mAdapter.unregisterDataSetObserver(observer); } } @Override public Filter getFilter() { if (mIsFilterable) { return ((Filterable) mAdapter).getFilter(); } return null; } @Override public ListAdapter getWrappedAdapter() { return mAdapter; } public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); } } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import java.util.ArrayList; import java.util.List; /** * ListView that its scroll position can be observed. */ public class ObservableListView extends ListView implements Scrollable { // Fields that should be saved onSaveInstanceState private int mPrevFirstVisiblePosition; private int mPrevFirstVisibleChildHeight = -1; private int mPrevScrolledChildrenHeight; private int mPrevScrollY; private int mScrollY; private SparseIntArray mChildrenHeights; // Fields that don't need to be saved onSaveInstanceState private ObservableScrollViewCallbacks mCallbacks; private List mCallbackCollection; private ScrollState mScrollState; private boolean mFirstScroll; private boolean mDragging; private boolean mIntercepted; private MotionEvent mPrevMoveEvent; private ViewGroup mTouchInterceptionViewGroup; private OnScrollListener mOriginalScrollListener; private OnScrollListener mScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mOriginalScrollListener != null) { mOriginalScrollListener.onScrollStateChanged(view, scrollState); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mOriginalScrollListener != null) { mOriginalScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } // AbsListView#invokeOnItemScrollListener calls onScrollChanged(0, 0, 0, 0) // on Android 4.0+, but Android 2.3 is not. (Android 3.0 is unknown) // So call it with onScrollListener. onScrollChanged(); } }; public ObservableListView(Context context) { super(context); init(); } public ObservableListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ObservableListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; mPrevFirstVisiblePosition = ss.prevFirstVisiblePosition; mPrevFirstVisibleChildHeight = ss.prevFirstVisibleChildHeight; mPrevScrolledChildrenHeight = ss.prevScrolledChildrenHeight; mPrevScrollY = ss.prevScrollY; mScrollY = ss.scrollY; mChildrenHeights = ss.childrenHeights; super.onRestoreInstanceState(ss.getSuperState()); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.prevFirstVisiblePosition = mPrevFirstVisiblePosition; ss.prevFirstVisibleChildHeight = mPrevFirstVisibleChildHeight; ss.prevScrolledChildrenHeight = mPrevScrolledChildrenHeight; ss.prevScrollY = mPrevScrollY; ss.scrollY = mScrollY; ss.childrenHeights = mChildrenHeights; return ss; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onInterceptTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Whether or not motion events are consumed by children, // flag initializations which are related to ACTION_DOWN events should be executed. // Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are // passed to parent (this view), the flags will be invalid. // Also, applications might implement initialization codes to onDownMotionEvent, // so call it here. mFirstScroll = mDragging = true; dispatchOnDownMotionEvent(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIntercepted = false; mDragging = false; dispatchOnUpOrCancelMotionEvent(mScrollState); break; case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { // Can't scroll anymore. if (mIntercepted) { // Already dispatched ACTION_DOWN event to parents, so stop here. return false; } // Apps can set the interception target other than the direct parent. final ViewGroup parent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } // Get offset to parents. If the parent is not the direct parent, // we should aggregate offsets from all of the parents. float offsetX = 0; float offsetY = 0; for (View v = this; v != null && v != parent; ) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); try { v = (View) v.getParent(); } catch (ClassCastException ex) { break; } } final MotionEvent event = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; // If the parent wants to intercept ACTION_MOVE events, // we pass ACTION_DOWN event to the parent // as if these touch events just have began now. event.setAction(MotionEvent.ACTION_DOWN); // Return this onTouchEvent() first and set ACTION_DOWN event for parent // to the queue, to keep events sequence. post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); } }); return false; } // Even when this can't be scrolled anymore, // simply returning false here may cause subView's click, // so delegate it to super. return super.onTouchEvent(ev); } break; } return super.onTouchEvent(ev); } @Override public void setOnScrollListener(OnScrollListener l) { // Don't set l to super.setOnScrollListener(). // l receives all events through mScrollListener. mOriginalScrollListener = l; } @Override public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection == null) { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection != null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection != null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { View firstVisibleChild = getChildAt(0); if (firstVisibleChild != null) { int baseHeight = firstVisibleChild.getHeight(); int position = y / baseHeight; setSelection(position); } } @Override public int getCurrentScrollY() { return mScrollY; } private void init() { mChildrenHeights = new SparseIntArray(); super.setOnScrollListener(mScrollListener); } private void onScrollChanged() { if (hasNoCallbacks()) { return; } if (getChildCount() > 0) { int firstVisiblePosition = getFirstVisiblePosition(); for (int i = getFirstVisiblePosition(), j = 0; i <= getLastVisiblePosition(); i++, j++) { if (mChildrenHeights.indexOfKey(i) < 0 || getChildAt(j).getHeight() != mChildrenHeights.get(i)) { mChildrenHeights.put(i, getChildAt(j).getHeight()); } } View firstVisibleChild = getChildAt(0); if (firstVisibleChild != null) { if (mPrevFirstVisiblePosition < firstVisiblePosition) { // scroll down int skippedChildrenHeight = 0; if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) { for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else { // Approximate each item's height to the first visible child. // It may be incorrect, but without this, scrollY will be broken // when scrolling from the bottom. skippedChildrenHeight += firstVisibleChild.getHeight(); } } } mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition < mPrevFirstVisiblePosition) { // scroll up int skippedChildrenHeight = 0; if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) { for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else { // Approximate each item's height to the first visible child. // It may be incorrect, but without this, scrollY will be broken // when scrolling from the bottom. skippedChildrenHeight += firstVisibleChild.getHeight(); } } } mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition == 0) { mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); mPrevScrolledChildrenHeight = 0; } if (mPrevFirstVisibleChildHeight < 0) { mPrevFirstVisibleChildHeight = 0; } mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop() + firstVisiblePosition * getDividerHeight() + getPaddingTop(); mPrevFirstVisiblePosition = firstVisiblePosition; dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < mScrollY) { mScrollState = ScrollState.UP; } else if (mScrollY < mPrevScrollY) { mScrollState = ScrollState.DOWN; } else { mScrollState = ScrollState.STOP; } mPrevScrollY = mScrollY; } } } private void dispatchOnDownMotionEvent() { if (mCallbacks != null) { mCallbacks.onDownMotionEvent(); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onDownMotionEvent(); } } } private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (mCallbacks != null) { mCallbacks.onScrollChanged(scrollY, firstScroll, dragging); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onScrollChanged(scrollY, firstScroll, dragging); } } } private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) { if (mCallbacks != null) { mCallbacks.onUpOrCancelMotionEvent(scrollState); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onUpOrCancelMotionEvent(scrollState); } } } private boolean hasNoCallbacks() { return mCallbacks == null && mCallbackCollection == null; } static class SavedState extends BaseSavedState { int prevFirstVisiblePosition; int prevFirstVisibleChildHeight = -1; int prevScrolledChildrenHeight; int prevScrollY; int scrollY; SparseIntArray childrenHeights; /** * Called by onSaveInstanceState. */ SavedState(Parcelable superState) { super(superState); } /** * Called by CREATOR. */ private SavedState(Parcel in) { super(in); prevFirstVisiblePosition = in.readInt(); prevFirstVisibleChildHeight = in.readInt(); prevScrolledChildrenHeight = in.readInt(); prevScrollY = in.readInt(); scrollY = in.readInt(); childrenHeights = new SparseIntArray(); final int numOfChildren = in.readInt(); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { final int key = in.readInt(); final int value = in.readInt(); childrenHeights.put(key, value); } } } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(prevFirstVisiblePosition); out.writeInt(prevFirstVisibleChildHeight); out.writeInt(prevScrolledChildrenHeight); out.writeInt(prevScrollY); out.writeInt(scrollY); final int numOfChildren = childrenHeights == null ? 0 : childrenHeights.size(); out.writeInt(numOfChildren); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { out.writeInt(childrenHeights.keyAt(i)); out.writeInt(childrenHeights.valueAt(i)); } } } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * RecyclerView that its scroll position can be observed. * Before using this, please consider to use the RecyclerView.OnScrollListener * provided by the support library officially. */ public class ObservableRecyclerView extends RecyclerView implements Scrollable { private static int recyclerViewLibraryVersion = 22; // Fields that should be saved onSaveInstanceState private int mPrevFirstVisiblePosition; private int mPrevFirstVisibleChildHeight = -1; private int mPrevScrolledChildrenHeight; private int mPrevScrollY; private int mScrollY; private SparseIntArray mChildrenHeights; // Fields that don't need to be saved onSaveInstanceState private ObservableScrollViewCallbacks mCallbacks; private List mCallbackCollection; private ScrollState mScrollState; private boolean mFirstScroll; private boolean mDragging; private boolean mIntercepted; private MotionEvent mPrevMoveEvent; private ViewGroup mTouchInterceptionViewGroup; public ObservableRecyclerView(Context context) { super(context); init(); } public ObservableRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ObservableRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; mPrevFirstVisiblePosition = ss.prevFirstVisiblePosition; mPrevFirstVisibleChildHeight = ss.prevFirstVisibleChildHeight; mPrevScrolledChildrenHeight = ss.prevScrolledChildrenHeight; mPrevScrollY = ss.prevScrollY; mScrollY = ss.scrollY; mChildrenHeights = ss.childrenHeights; super.onRestoreInstanceState(ss.getSuperState()); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.prevFirstVisiblePosition = mPrevFirstVisiblePosition; ss.prevFirstVisibleChildHeight = mPrevFirstVisibleChildHeight; ss.prevScrolledChildrenHeight = mPrevScrolledChildrenHeight; ss.prevScrollY = mPrevScrollY; ss.scrollY = mScrollY; ss.childrenHeights = mChildrenHeights; return ss; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (hasNoCallbacks()) { return; } if (getChildCount() > 0) { int firstVisiblePosition = getChildAdapterPosition(getChildAt(0)); int lastVisiblePosition = getChildAdapterPosition(getChildAt(getChildCount() - 1)); for (int i = firstVisiblePosition, j = 0; i <= lastVisiblePosition; i++, j++) { int childHeight = 0; View child = getChildAt(j); if (child != null) { if (mChildrenHeights.indexOfKey(i) < 0 || (child.getHeight() != mChildrenHeights.get(i))) { childHeight = child.getHeight(); } } mChildrenHeights.put(i, childHeight); } View firstVisibleChild = getChildAt(0); if (firstVisibleChild != null) { if (mPrevFirstVisiblePosition < firstVisiblePosition) { // scroll down int skippedChildrenHeight = 0; if (firstVisiblePosition - mPrevFirstVisiblePosition != 1) { for (int i = firstVisiblePosition - 1; i > mPrevFirstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else { // Approximate each item's height to the first visible child. // It may be incorrect, but without this, scrollY will be broken // when scrolling from the bottom. skippedChildrenHeight += firstVisibleChild.getHeight(); } } } mPrevScrolledChildrenHeight += mPrevFirstVisibleChildHeight + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition < mPrevFirstVisiblePosition) { // scroll up int skippedChildrenHeight = 0; if (mPrevFirstVisiblePosition - firstVisiblePosition != 1) { for (int i = mPrevFirstVisiblePosition - 1; i > firstVisiblePosition; i--) { if (0 < mChildrenHeights.indexOfKey(i)) { skippedChildrenHeight += mChildrenHeights.get(i); } else { // Approximate each item's height to the first visible child. // It may be incorrect, but without this, scrollY will be broken // when scrolling from the bottom. skippedChildrenHeight += firstVisibleChild.getHeight(); } } } mPrevScrolledChildrenHeight -= firstVisibleChild.getHeight() + skippedChildrenHeight; mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); } else if (firstVisiblePosition == 0) { mPrevFirstVisibleChildHeight = firstVisibleChild.getHeight(); mPrevScrolledChildrenHeight = 0; } if (mPrevFirstVisibleChildHeight < 0) { mPrevFirstVisibleChildHeight = 0; } mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop() + getPaddingTop(); mPrevFirstVisiblePosition = firstVisiblePosition; dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < mScrollY) { //down mScrollState = ScrollState.UP; } else if (mScrollY < mPrevScrollY) { //up mScrollState = ScrollState.DOWN; } else { mScrollState = ScrollState.STOP; } mPrevScrollY = mScrollY; } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onInterceptTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Whether or not motion events are consumed by children, // flag initializations which are related to ACTION_DOWN events should be executed. // Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are // passed to parent (this view), the flags will be invalid. // Also, applications might implement initialization codes to onDownMotionEvent, // so call it here. mFirstScroll = mDragging = true; dispatchOnDownMotionEvent(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIntercepted = false; mDragging = false; dispatchOnUpOrCancelMotionEvent(mScrollState); break; case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { // Can't scroll anymore. if (mIntercepted) { // Already dispatched ACTION_DOWN event to parents, so stop here. return false; } // Apps can set the interception target other than the direct parent. final ViewGroup parent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } // Get offset to parents. If the parent is not the direct parent, // we should aggregate offsets from all of the parents. float offsetX = 0; float offsetY = 0; for (View v = this; v != null && v != parent; v = (View) v.getParent()) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); } final MotionEvent event = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; // If the parent wants to intercept ACTION_MOVE events, // we pass ACTION_DOWN event to the parent // as if these touch events just have began now. event.setAction(MotionEvent.ACTION_DOWN); // Return this onTouchEvent() first and set ACTION_DOWN event for parent // to the queue, to keep events sequence. post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); } }); return false; } // Even when this can't be scrolled anymore, // simply returning false here may cause subView's click, // so delegate it to super. return super.onTouchEvent(ev); } break; } return super.onTouchEvent(ev); } @Override public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection == null) { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection != null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection != null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { View firstVisibleChild = getChildAt(0); if (firstVisibleChild != null) { int baseHeight = firstVisibleChild.getHeight(); int position = y / baseHeight; scrollVerticallyToPosition(position); } } /** *

Same as {@linkplain #scrollToPosition(int)} but it scrolls to the position not only make * the position visible.

*

It depends on {@code LayoutManager} how {@linkplain #scrollToPosition(int)} works, * and currently we know that {@linkplain LinearLayoutManager#scrollToPosition(int)} just * make the position visible.

*

In LinearLayoutManager, scrollToPositionWithOffset() is provided for scrolling to the position. * This method checks which LayoutManager is set, * and handles which method should be called for scrolling.

*

Other know classes (StaggeredGridLayoutManager and GridLayoutManager) are not tested.

* * @param position Position to scroll. */ public void scrollVerticallyToPosition(int position) { LayoutManager lm = getLayoutManager(); if (lm != null && lm instanceof LinearLayoutManager) { ((LinearLayoutManager) lm).scrollToPositionWithOffset(position, 0); } else { scrollToPosition(position); } } @Override public int getCurrentScrollY() { return mScrollY; } @SuppressWarnings("deprecation") public int getChildAdapterPosition(View child) { if (22 <= recyclerViewLibraryVersion) { return super.getChildAdapterPosition(child); } return getChildPosition(child); } private void init() { mChildrenHeights = new SparseIntArray(); checkLibraryVersion(); } private void checkLibraryVersion() { try { super.getChildAdapterPosition(null); } catch (NoSuchMethodError e) { recyclerViewLibraryVersion = 21; } } private void dispatchOnDownMotionEvent() { if (mCallbacks != null) { mCallbacks.onDownMotionEvent(); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onDownMotionEvent(); } } } private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (mCallbacks != null) { mCallbacks.onScrollChanged(scrollY, firstScroll, dragging); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onScrollChanged(scrollY, firstScroll, dragging); } } } private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) { if (mCallbacks != null) { mCallbacks.onUpOrCancelMotionEvent(scrollState); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onUpOrCancelMotionEvent(scrollState); } } } private boolean hasNoCallbacks() { return mCallbacks == null && mCallbackCollection == null; } /** * This saved state class is a Parcelable and should not extend * {@link android.view.View.BaseSavedState} nor {@link android.view.AbsSavedState} * because its super class AbsSavedState's constructor * {@link android.view.AbsSavedState#AbsSavedState(Parcel)} currently passes null * as a class loader to read its superstate from Parcelable. * This causes {@link android.os.BadParcelableException} when restoring saved states. *

* The super class "RecyclerView" is a part of the support library, * and restoring its saved state requires the class loader that loaded the RecyclerView. * It seems that the class loader is not required when restoring from RecyclerView itself, * but it is required when restoring from RecyclerView's subclasses. */ static class SavedState implements Parcelable { public static final SavedState EMPTY_STATE = new SavedState() { }; int prevFirstVisiblePosition; int prevFirstVisibleChildHeight = -1; int prevScrolledChildrenHeight; int prevScrollY; int scrollY; SparseIntArray childrenHeights; // This keeps the parent(RecyclerView)'s state Parcelable superState; /** * Called by EMPTY_STATE instantiation. */ private SavedState() { superState = null; } /** * Called by onSaveInstanceState. */ SavedState(Parcelable superState) { this.superState = superState != EMPTY_STATE ? superState : null; } /** * Called by CREATOR. */ private SavedState(Parcel in) { // Parcel 'in' has its parent(RecyclerView)'s saved state. // To restore it, class loader that loaded RecyclerView is required. Parcelable superState = in.readParcelable(RecyclerView.class.getClassLoader()); this.superState = superState != null ? superState : EMPTY_STATE; prevFirstVisiblePosition = in.readInt(); prevFirstVisibleChildHeight = in.readInt(); prevScrolledChildrenHeight = in.readInt(); prevScrollY = in.readInt(); scrollY = in.readInt(); childrenHeights = new SparseIntArray(); final int numOfChildren = in.readInt(); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { final int key = in.readInt(); final int value = in.readInt(); childrenHeights.put(key, value); } } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { out.writeParcelable(superState, flags); out.writeInt(prevFirstVisiblePosition); out.writeInt(prevFirstVisibleChildHeight); out.writeInt(prevScrolledChildrenHeight); out.writeInt(prevScrollY); out.writeInt(scrollY); final int numOfChildren = childrenHeights == null ? 0 : childrenHeights.size(); out.writeInt(numOfChildren); if (0 < numOfChildren) { for (int i = 0; i < numOfChildren; i++) { out.writeInt(childrenHeights.keyAt(i)); out.writeInt(childrenHeights.valueAt(i)); } } } public Parcelable getSuperState() { return superState; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; import java.util.ArrayList; import java.util.List; /** * ScrollView that its scroll position can be observed. */ public class ObservableScrollView extends ScrollView implements Scrollable { // Fields that should be saved onSaveInstanceState private int mPrevScrollY; private int mScrollY; // Fields that don't need to be saved onSaveInstanceState private ObservableScrollViewCallbacks mCallbacks; private List mCallbackCollection; private ScrollState mScrollState; private boolean mFirstScroll; private boolean mDragging; private boolean mIntercepted; private MotionEvent mPrevMoveEvent; private ViewGroup mTouchInterceptionViewGroup; public ObservableScrollView(Context context) { super(context); } public ObservableScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public ObservableScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; mPrevScrollY = ss.prevScrollY; mScrollY = ss.scrollY; super.onRestoreInstanceState(ss.getSuperState()); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.prevScrollY = mPrevScrollY; ss.scrollY = mScrollY; return ss; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (hasNoCallbacks()) { return; } mScrollY = t; dispatchOnScrollChanged(t, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < t) { mScrollState = ScrollState.UP; } else if (t < mPrevScrollY) { mScrollState = ScrollState.DOWN; //} else { // Keep previous state while dragging. // Never makes it STOP even if scrollY not changed. // Before Android 4.4, onTouchEvent calls onScrollChanged directly for ACTION_MOVE, // which makes mScrollState always STOP when onUpOrCancelMotionEvent is called. // STOP state is now meaningless for ScrollView. } mPrevScrollY = t; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onInterceptTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Whether or not motion events are consumed by children, // flag initializations which are related to ACTION_DOWN events should be executed. // Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are // passed to parent (this view), the flags will be invalid. // Also, applications might implement initialization codes to onDownMotionEvent, // so call it here. mFirstScroll = mDragging = true; dispatchOnDownMotionEvent(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIntercepted = false; mDragging = false; dispatchOnUpOrCancelMotionEvent(mScrollState); break; case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { // Can't scroll anymore. if (mIntercepted) { // Already dispatched ACTION_DOWN event to parents, so stop here. return false; } // Apps can set the interception target other than the direct parent. final ViewGroup parent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } // Get offset to parents. If the parent is not the direct parent, // we should aggregate offsets from all of the parents. float offsetX = 0; float offsetY = 0; for (View v = this; v != null && v != parent; v = (View) v.getParent()) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); } final MotionEvent event = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; // If the parent wants to intercept ACTION_MOVE events, // we pass ACTION_DOWN event to the parent // as if these touch events just have began now. event.setAction(MotionEvent.ACTION_DOWN); // Return this onTouchEvent() first and set ACTION_DOWN event for parent // to the queue, to keep events sequence. post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); } }); return false; } // Even when this can't be scrolled anymore, // simply returning false here may cause subView's click, // so delegate it to super. return super.onTouchEvent(ev); } break; } return super.onTouchEvent(ev); } @Override public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection == null) { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection != null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection != null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { scrollTo(0, y); } @Override public int getCurrentScrollY() { return mScrollY; } private void dispatchOnDownMotionEvent() { if (mCallbacks != null) { mCallbacks.onDownMotionEvent(); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onDownMotionEvent(); } } } private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (mCallbacks != null) { mCallbacks.onScrollChanged(scrollY, firstScroll, dragging); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onScrollChanged(scrollY, firstScroll, dragging); } } } private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) { if (mCallbacks != null) { mCallbacks.onUpOrCancelMotionEvent(scrollState); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onUpOrCancelMotionEvent(scrollState); } } } private boolean hasNoCallbacks() { return mCallbacks == null && mCallbackCollection == null; } static class SavedState extends BaseSavedState { int prevScrollY; int scrollY; /** * Called by onSaveInstanceState. */ SavedState(Parcelable superState) { super(superState); } /** * Called by CREATOR. */ private SavedState(Parcel in) { super(in); prevScrollY = in.readInt(); scrollY = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(prevScrollY); out.writeInt(scrollY); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; /** * Callbacks for Scrollable widgets. */ public interface ObservableScrollViewCallbacks { /** * Called when the scroll change events occurred. *

This won't be called just after the view is laid out, so if you'd like to * initialize the position of your views with this method, you should call this manually * or invoke scroll as appropriate.

* * @param scrollY Scroll position in Y axis. * @param firstScroll True when this is called for the first time in the consecutive motion events. * @param dragging True when the view is dragged and false when the view is scrolled in the inertia. */ void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging); /** * Called when the down motion event occurred. */ void onDownMotionEvent(); /** * Called when the dragging ended or canceled. * * @param scrollState State to indicate the scroll direction. */ void onUpOrCancelMotionEvent(ScrollState scrollState); } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import java.util.ArrayList; import java.util.List; /** * WebView that its scroll position can be observed. */ public class ObservableWebView extends WebView implements Scrollable { // Fields that should be saved onSaveInstanceState private int mPrevScrollY; private int mScrollY; // Fields that don't need to be saved onSaveInstanceState private ObservableScrollViewCallbacks mCallbacks; private List mCallbackCollection; private ScrollState mScrollState; private boolean mFirstScroll; private boolean mDragging; private boolean mIntercepted; private MotionEvent mPrevMoveEvent; private ViewGroup mTouchInterceptionViewGroup; public ObservableWebView(Context context) { super(context); } public ObservableWebView(Context context, AttributeSet attrs) { super(context, attrs); } public ObservableWebView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; mPrevScrollY = ss.prevScrollY; mScrollY = ss.scrollY; super.onRestoreInstanceState(ss.getSuperState()); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.prevScrollY = mPrevScrollY; ss.scrollY = mScrollY; return ss; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (hasNoCallbacks()) { return; } mScrollY = t; dispatchOnScrollChanged(mScrollY, mFirstScroll, mDragging); if (mFirstScroll) { mFirstScroll = false; } if (mPrevScrollY < t) { mScrollState = ScrollState.UP; } else if (t < mPrevScrollY) { mScrollState = ScrollState.DOWN; } else { mScrollState = ScrollState.STOP; } mPrevScrollY = t; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onInterceptTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // Whether or not motion events are consumed by children, // flag initializations which are related to ACTION_DOWN events should be executed. // Because if the ACTION_DOWN is consumed by children and only ACTION_MOVEs are // passed to parent (this view), the flags will be invalid. // Also, applications might implement initialization codes to onDownMotionEvent, // so call it here. mFirstScroll = mDragging = true; dispatchOnDownMotionEvent(); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (hasNoCallbacks()) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIntercepted = false; mDragging = false; dispatchOnUpOrCancelMotionEvent(mScrollState); break; case MotionEvent.ACTION_MOVE: if (mPrevMoveEvent == null) { mPrevMoveEvent = ev; } float diffY = ev.getY() - mPrevMoveEvent.getY(); mPrevMoveEvent = MotionEvent.obtainNoHistory(ev); if (getCurrentScrollY() - diffY <= 0) { // Can't scroll anymore. if (mIntercepted) { // Already dispatched ACTION_DOWN event to parents, so stop here. return false; } // Apps can set the interception target other than the direct parent. final ViewGroup parent; if (mTouchInterceptionViewGroup == null) { parent = (ViewGroup) getParent(); } else { parent = mTouchInterceptionViewGroup; } // Get offset to parents. If the parent is not the direct parent, // we should aggregate offsets from all of the parents. float offsetX = 0; float offsetY = 0; for (View v = this; v != null && v != parent; v = (View) v.getParent()) { offsetX += v.getLeft() - v.getScrollX(); offsetY += v.getTop() - v.getScrollY(); } final MotionEvent event = MotionEvent.obtainNoHistory(ev); event.offsetLocation(offsetX, offsetY); if (parent.onInterceptTouchEvent(event)) { mIntercepted = true; // If the parent wants to intercept ACTION_MOVE events, // we pass ACTION_DOWN event to the parent // as if these touch events just have began now. event.setAction(MotionEvent.ACTION_DOWN); // Return this onTouchEvent() first and set ACTION_DOWN event for parent // to the queue, to keep events sequence. post(new Runnable() { @Override public void run() { parent.dispatchTouchEvent(event); } }); return false; } // Even when this can't be scrolled anymore, // simply returning false here may cause subView's click, // so delegate it to super. return super.onTouchEvent(ev); } break; } return super.onTouchEvent(ev); } @Override public void setScrollViewCallbacks(ObservableScrollViewCallbacks listener) { mCallbacks = listener; } @Override public void addScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection == null) { mCallbackCollection = new ArrayList<>(); } mCallbackCollection.add(listener); } @Override public void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener) { if (mCallbackCollection != null) { mCallbackCollection.remove(listener); } } @Override public void clearScrollViewCallbacks() { if (mCallbackCollection != null) { mCallbackCollection.clear(); } } @Override public void setTouchInterceptionViewGroup(ViewGroup viewGroup) { mTouchInterceptionViewGroup = viewGroup; } @Override public void scrollVerticallyTo(int y) { scrollTo(0, y); } @Override public int getCurrentScrollY() { return mScrollY; } private void dispatchOnDownMotionEvent() { if (mCallbacks != null) { mCallbacks.onDownMotionEvent(); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onDownMotionEvent(); } } } private void dispatchOnScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { if (mCallbacks != null) { mCallbacks.onScrollChanged(scrollY, firstScroll, dragging); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onScrollChanged(scrollY, firstScroll, dragging); } } } private void dispatchOnUpOrCancelMotionEvent(ScrollState scrollState) { if (mCallbacks != null) { mCallbacks.onUpOrCancelMotionEvent(scrollState); } if (mCallbackCollection != null) { for (int i = 0; i < mCallbackCollection.size(); i++) { ObservableScrollViewCallbacks callbacks = mCallbackCollection.get(i); callbacks.onUpOrCancelMotionEvent(scrollState); } } } private boolean hasNoCallbacks() { return mCallbacks == null && mCallbackCollection == null; } static class SavedState extends BaseSavedState { int prevScrollY; int scrollY; /** * Called by onSaveInstanceState. */ SavedState(Parcelable superState) { super(superState); } /** * Called by CREATOR. */ private SavedState(Parcel in) { super(in); prevScrollY = in.readInt(); scrollY = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(prevScrollY); out.writeInt(scrollY); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollState.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; /** * Constants that indicates the scroll state of the Scrollable widgets. */ public enum ScrollState { /** * Widget is stopped. * This state does not always mean that this widget have never been scrolled. */ STOP, /** * Widget is scrolled up by swiping it down. */ UP, /** * Widget is scrolled down by swiping it up. */ DOWN, } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.os.Build; import android.view.View; import android.view.ViewTreeObserver; /** * Utilities for creating scrolling effects. */ public final class ScrollUtils { private ScrollUtils() { } /** * Return a float value within the range. *

This is just a wrapper for Math.min() and Math.max(). * This may be useful if you feel it confusing ("Which is min and which is max?").

* * @param value The target value. * @param minValue Minimum value. If value is less than this, minValue will be returned. * @param maxValue Maximum value. If value is greater than this, maxValue will be returned. * @return Float value limited to the range. */ public static float getFloat(final float value, final float minValue, final float maxValue) { return Math.min(maxValue, Math.max(minValue, value)); } /** * Create a color integer value with specified alpha. *

This may be useful to change alpha value of background color.

* * @param alpha Alpha value from 0.0f to 1.0f. * @param baseColor Base color. alpha value will be ignored. * @return A color with alpha made from base color. */ public static int getColorWithAlpha(float alpha, int baseColor) { int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24; int rgb = 0x00ffffff & baseColor; return a + rgb; } /** * Add an OnGlobalLayoutListener for the view. *

This is just a convenience method for using {@code ViewTreeObserver.OnGlobalLayoutListener()}. * This also handles removing listener when onGlobalLayout is called.

* * @param view The target view to add global layout listener. * @param runnable Runnable to be executed after the view is laid out. */ public static void addOnGlobalLayoutListener(final View view, final Runnable runnable) { ViewTreeObserver vto = view.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @SuppressWarnings("deprecation") @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } runnable.run(); } }); } /** * Mix two colors. *

{@code toColor} will be {@code toAlpha/1} percent, * and {@code fromColor} will be {@code (1-toAlpha)/1} percent.

* * @param fromColor First color to be mixed. * @param toColor Second color to be mixed. * @param toAlpha Alpha value of toColor, 0.0f to 1.0f. * @return Mixed color value in ARGB. Alpha is fixed value (255). */ public static int mixColors(int fromColor, int toColor, float toAlpha) { float[] fromCmyk = ScrollUtils.cmykFromRgb(fromColor); float[] toCmyk = ScrollUtils.cmykFromRgb(toColor); float[] result = new float[4]; for (int i = 0; i < 4; i++) { result[i] = Math.min(1, fromCmyk[i] * (1 - toAlpha) + toCmyk[i] * toAlpha); } return 0xff000000 + (0x00ffffff & ScrollUtils.rgbFromCmyk(result)); } /** * Convert RGB color to CMYK color. * * @param rgbColor Target color. * @return CMYK array. */ public static float[] cmykFromRgb(int rgbColor) { int red = (0xff0000 & rgbColor) >> 16; int green = (0xff00 & rgbColor) >> 8; int blue = (0xff & rgbColor); float black = Math.min(1.0f - red / 255.0f, Math.min(1.0f - green / 255.0f, 1.0f - blue / 255.0f)); float cyan = 1.0f; float magenta = 1.0f; float yellow = 1.0f; if (black != 1.0f) { // black 1.0 causes zero divide cyan = (1.0f - (red / 255.0f) - black) / (1.0f - black); magenta = (1.0f - (green / 255.0f) - black) / (1.0f - black); yellow = (1.0f - (blue / 255.0f) - black) / (1.0f - black); } return new float[]{cyan, magenta, yellow, black}; } /** * Convert CYMK color to RGB color. * This method doesn't check if cmyk is not null or have 4 elements in array. * * @param cmyk Target CYMK color. Each value should be between 0.0f to 1.0f, * and should be set in this order: cyan, magenta, yellow, black. * @return ARGB color. Alpha is fixed value (255). */ public static int rgbFromCmyk(float[] cmyk) { float cyan = cmyk[0]; float magenta = cmyk[1]; float yellow = cmyk[2]; float black = cmyk[3]; int red = (int) ((1.0f - Math.min(1.0f, cyan * (1.0f - black) + black)) * 255); int green = (int) ((1.0f - Math.min(1.0f, magenta * (1.0f - black) + black)) * 255); int blue = (int) ((1.0f - Math.min(1.0f, yellow * (1.0f - black) + black)) * 255); return ((0xff & red) << 16) + ((0xff & green) << 8) + (0xff & blue); } } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.view.ViewGroup; /** * Interface for providing common API for observable and scrollable widgets. */ public interface Scrollable { /** * Set a callback listener.
* Developers should use {@link #addScrollViewCallbacks(ObservableScrollViewCallbacks)} * and {@link #removeScrollViewCallbacks(ObservableScrollViewCallbacks)}. * * @param listener Listener to set. */ @Deprecated void setScrollViewCallbacks(ObservableScrollViewCallbacks listener); /** * Add a callback listener. * * @param listener Listener to add. * @since 1.7.0 */ void addScrollViewCallbacks(ObservableScrollViewCallbacks listener); /** * Remove a callback listener. * * @param listener Listener to remove. * @since 1.7.0 */ void removeScrollViewCallbacks(ObservableScrollViewCallbacks listener); /** * Clear callback listeners. * * @since 1.7.0 */ void clearScrollViewCallbacks(); /** * Scroll vertically to the absolute Y.
* Implemented classes are expected to scroll to the exact Y pixels from the top, * but it depends on the type of the widget. * * @param y Vertical position to scroll to. */ void scrollVerticallyTo(int y); /** * Return the current Y of the scrollable view. * * @return Current Y pixel. */ int getCurrentScrollY(); /** * Set a touch motion event delegation ViewGroup.
* This is used to pass motion events back to parent view. * It's up to the implementation classes whether or not it works. * * @param viewGroup ViewGroup object to dispatch motion events. */ void setTouchInterceptionViewGroup(ViewGroup viewGroup); } ================================================ FILE: library/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java ================================================ /* * Copyright 2014 Soichiro Kashima * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.ksoichiro.android.observablescrollview; import android.annotation.TargetApi; import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; /** * A layout that delegates interception of touch motion events. * This layout is provided to move the container of Scrollable views using scroll position. * Please note that this class overrides or uses touch events API such as onTouchEvent, * onInterceptTouchEvent and dispatchTouchEvent, * so be careful when you handle touches with this layout. */ public class TouchInterceptionFrameLayout extends FrameLayout { /** * Callbacks for TouchInterceptionFrameLayout. */ public interface TouchInterceptionListener { /** * Determine whether the layout should intercept this event. * * @param ev Motion event. * @param moving True if this event is ACTION_MOVE type. * @param diffX Difference between previous X and current X, if moving is true. * @param diffY Difference between previous Y and current Y, if moving is true. * @return True if the layout should intercept. */ boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY); /** * Called if the down motion event is intercepted by this layout. * * @param ev Motion event. */ void onDownMotionEvent(MotionEvent ev); /** * Called if the move motion event is intercepted by this layout. * * @param ev Motion event. * @param diffX Difference between previous X and current X. * @param diffY Difference between previous Y and current Y. */ void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY); /** * Called if the up (or cancel) motion event is intercepted by this layout. * * @param ev Motion event. */ void onUpOrCancelMotionEvent(MotionEvent ev); } private boolean mIntercepting; private boolean mDownMotionEventPended; private boolean mBeganFromDownMotionEvent; private boolean mChildrenEventsCanceled; private PointF mInitialPoint; private MotionEvent mPendingDownMotionEvent; private TouchInterceptionListener mTouchInterceptionListener; public TouchInterceptionFrameLayout(Context context) { super(context); } public TouchInterceptionFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public TouchInterceptionFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public void setScrollInterceptionListener(TouchInterceptionListener listener) { mTouchInterceptionListener = listener; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mTouchInterceptionListener == null) { return false; } // In here, we must initialize touch state variables // and ask if we should intercept this event. // Whether we should intercept or not is kept for the later event handling. switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: mInitialPoint = new PointF(ev.getX(), ev.getY()); mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev); mDownMotionEventPended = true; mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0); mBeganFromDownMotionEvent = mIntercepting; mChildrenEventsCanceled = false; return mIntercepting; case MotionEvent.ACTION_MOVE: // ACTION_MOVE will be passed suddenly, so initialize to avoid exception. if (mInitialPoint == null) { mInitialPoint = new PointF(ev.getX(), ev.getY()); } // diffX and diffY are the origin of the motion, and should be difference // from the position of the ACTION_DOWN event occurred. float diffX = ev.getX() - mInitialPoint.x; float diffY = ev.getY() - mInitialPoint.y; mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY); return mIntercepting; } return false; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mTouchInterceptionListener != null) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (mIntercepting) { mTouchInterceptionListener.onDownMotionEvent(ev); duplicateTouchEventForChildren(ev); return true; } break; case MotionEvent.ACTION_MOVE: // ACTION_MOVE will be passed suddenly, so initialize to avoid exception. if (mInitialPoint == null) { mInitialPoint = new PointF(ev.getX(), ev.getY()); } // diffX and diffY are the origin of the motion, and should be difference // from the position of the ACTION_DOWN event occurred. float diffX = ev.getX() - mInitialPoint.x; float diffY = ev.getY() - mInitialPoint.y; mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY); if (mIntercepting) { // If this layout didn't receive ACTION_DOWN motion event, // we should generate ACTION_DOWN event with current position. if (!mBeganFromDownMotionEvent) { mBeganFromDownMotionEvent = true; MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); event.setLocation(ev.getX(), ev.getY()); mTouchInterceptionListener.onDownMotionEvent(event); mInitialPoint = new PointF(ev.getX(), ev.getY()); diffX = diffY = 0; } // Children's touches should be canceled if (!mChildrenEventsCanceled) { mChildrenEventsCanceled = true; duplicateTouchEventForChildren(obtainMotionEvent(ev, MotionEvent.ACTION_CANCEL)); } mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY); // If next mIntercepting become false, // then we should generate fake ACTION_DOWN event. // Therefore we set pending flag to true as if this is a down motion event. mDownMotionEventPended = true; // Whether or not this event is consumed by the listener, // assume it consumed because we declared to intercept the event. return true; } else { if (mDownMotionEventPended) { mDownMotionEventPended = false; MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); event.setLocation(ev.getX(), ev.getY()); duplicateTouchEventForChildren(ev, event); } else { duplicateTouchEventForChildren(ev); } // If next mIntercepting become true, // then we should generate fake ACTION_DOWN event. // Therefore we set beganFromDownMotionEvent flag to false // as if we haven't received a down motion event. mBeganFromDownMotionEvent = false; // Reserve children's click cancellation here if they've already canceled mChildrenEventsCanceled = false; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mBeganFromDownMotionEvent = false; if (mIntercepting) { mTouchInterceptionListener.onUpOrCancelMotionEvent(ev); } // Children's touches should be canceled regardless of // whether or not this layout intercepted the consecutive motion events. if (!mChildrenEventsCanceled) { mChildrenEventsCanceled = true; if (mDownMotionEventPended) { mDownMotionEventPended = false; MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); event.setLocation(ev.getX(), ev.getY()); duplicateTouchEventForChildren(ev, event); } else { duplicateTouchEventForChildren(ev); } } return true; } } return super.onTouchEvent(ev); } private MotionEvent obtainMotionEvent(MotionEvent base, int action) { MotionEvent ev = MotionEvent.obtainNoHistory(base); ev.setAction(action); return ev; } /** * Duplicate touch events to child views. * We want to dispatch a down motion event and the move events to * child views, but calling dispatchTouchEvent() causes StackOverflowError. * Therefore we do it manually. * * @param ev Motion event to be passed to children. * @param pendingEvents Pending events like ACTION_DOWN. This will be passed to the children before ev. */ private void duplicateTouchEventForChildren(MotionEvent ev, MotionEvent... pendingEvents) { if (ev == null) { return; } for (int i = getChildCount() - 1; 0 <= i; i--) { View childView = getChildAt(i); if (childView != null) { Rect childRect = new Rect(); childView.getHitRect(childRect); MotionEvent event = MotionEvent.obtainNoHistory(ev); if (!childRect.contains((int) event.getX(), (int) event.getY())) { continue; } float offsetX = -childView.getLeft(); float offsetY = -childView.getTop(); boolean consumed = false; if (pendingEvents != null) { for (MotionEvent pe : pendingEvents) { if (pe != null) { MotionEvent peAdjusted = MotionEvent.obtainNoHistory(pe); peAdjusted.offsetLocation(offsetX, offsetY); consumed |= childView.dispatchTouchEvent(peAdjusted); } } } event.offsetLocation(offsetX, offsetY); consumed |= childView.dispatchTouchEvent(event); if (consumed) { break; } } } } } ================================================ FILE: samples/.gitignore ================================================ /build /src/version/ ================================================ FILE: samples/AndroidManifest.xml ================================================ ================================================ FILE: samples/README.md ================================================ # Samples This sample project demonstrates how the Android-ObservableScrollView works. This document's goal is to lead you to run the sample app and help understanding how to use this library. Please note that this document is still work in progress. Although I've built the app on Android Studio, Eclipse, Gradle on Mac and Gradle on Linux of Travis CI, there might be some implicit dependencies which I haven't noticed and you couldn't build it correctly. Therefore I'd greatly appreciate it if you report it to me. ## How to build ### on Android Studio TODO ### on Eclipse TODO ### on Gradle Windows: ```sh > gradlew installDevDebug ``` Linux/Mac: ```sh $ ./gradlew installDevDebug ``` ================================================ FILE: samples/assets/handletouch.html ================================================

Lorem ipsum dolor sit amet, ut duis lorem provident sed felis blandit, condimentum donec lectus ipsum et mauris, morbi porttitor interdum feugiat nulla donec sodales, vestibulum nisl primis a molestie vestibulum quam, sapien mauris metus risus suspendisse magnis. Augue viverra nulla faucibus egestas eu, a etiam id congue rutrum ante, arcu tincidunt donec quam felis at ornare, iaculis ligula sodales venenatis commodo volutpat neque, suspendisse elit praesent tellus felis mi amet. Inceptos amet tempor lectus lorem est non, ac donec ac libero neque mauris, tellus ante metus eget leo consequat. Scelerisque dolor curabitur pretium blandit ut feugiat, amet lacus pulvinar justo convallis ut, sed natoque ipsum urna posuere nibh eu. Sed at sed vulputate sit orci, facilisis a aliquam tellus quam aliquam, eu aliquam donec at molestie ante, pellentesque mauris lorem ultrices libero faucibus porta, imperdiet adipiscing sit hac diam ut nulla. Lacus enim elit pulvinar donec vehicula dapibus, accumsan purus officia cursus dolor sapien, eu amet dis mauris mi nulla ut. Non accusamus etiam pede non urna tempus, vestibulum aliquam tortor eget pharetra sodales, in vestibulum ut justo orci nulla, lobortis purus sem semper consectetuer magni purus. Dolor a leo vestibulum amet ut sit, arcu ut eaque urna fusce aliquet turpis, sed fermentum sed vestibulum nisl pede, tristique enim lorem posuere in laborum ut. Vestibulum id id justo leo nulla, magna lobortis ullamcorper et dignissim pellentesque, duis suspendisse quis id lorem ante. Vivamus a nullam ante adipiscing amet, mi vel consectetuer nunc aenean pede quisque, eget rhoncus dis porttitor habitant nunc vivamus, duis cubilia blandit non donec justo dictumst, praesent vitae nulla nam pulvinar urna. Adipiscing adipiscing justo urna pulvinar imperdiet nullam, vitae fusce rhoncus proin nonummy suscipit, ullamcorper amet et non potenti platea ultrices, mauris nullam sapien nunc justo vel, eu semper pellentesque arcu fusce augue. Malesuada mauris nibh sit a a scelerisque, velit sem lectus tellus convallis consectetuer, ultricies auctor a ante eros amet sed.

Android-ObservableScrollView

Risus lacus duis leo platea wisi, felis maecenas rutrum in id in donec, non id a potenti libero eget, posuere elit ea sed pellentesque quis. Sunt lacus urna lorem elit duis, nibh donec purus quisque consectetuer dolor, neque vestibulum proin ornare eros nonummy phasellus. Iaculis cras eu at egestas dolor montes, viverra quisque malesuada consectetuer semper maecenas, a sed vitae donec tempor aliqua metus, ornare mollis suscipit et erat fusce, sit orci aut auctor elementum fames aliquam. Platea dui integer magnis non metus, minus dignissimos ante massa nostra et, rutrum sapien egestas quis sapien donec donec. Erat sit a eros aenean natoque, quam libero id lorem enim proin, lorem ipsum fermentum mattis metus et. Aliquam aliquet suscipit purus conubia at neque, platea vivamus vestibulum nulla quibusdam senectus, et morbi lectus malesuada gravida donec, elementum sit convallis pellentesque velit amet. Et eveniet viverra vehicula consectetuer justo, provident sed commodo non lacinia velit, tempor phasellus vel leo nisl cras, vivamus et arcu interdum dui eu amet. Volutpat wisi rhoncus vel turpis diam quibusdam, dapibus elit est quisque cubilia mauris, nulla elit magna tempor accumsan bibendum, lorem varius sed interdum eget mattis, scelerisque egestas feugiat donec dui molestie. Leo facilisis nisl sit montes ligula sed, enim commodo consectetuer nunc est et, ut sed vehicula dolor luctus elit. Fermentum cras donec eget nibh est vel, sed justo risus et pharetra diam, eu vivamus egestas ligula risus diam, sed justo eget hac ut mauris. Vestibulum diam nec vitae mi eget suspendisse, aenean arcu purus facilisis purus class in, id aliquam sit id scelerisque sapien etiam. Ut nullam sit sed at mauris lobortis, consequat dolor autem ipsum euismod nulla, elit quis proin eget conubia varius, erat arcu massa mus in mauris, scelerisque ut eu sollicitudin libero leo urna.

Android-ObservableScrollView

Consectetuer luctus tempor elit ut dolor ligula, quis dui per dui hendrerit ante sagittis, in quisque pretium in eleifend enim. Condimentum iaculis vitae feugiat dis tellus vel, lectus dolor nec dui nulla nascetur, et pellentesque curabitur lorem leo velit eget. Id nascetur arcu lobortis suspendisse imperdiet urna, natoque nascetur ante in porta a, interdum hendrerit mi bibendum platea tellus, urna in enim ornare vestibulum faucibus enim. Leo fusce egestas ante nec volutpat, in tempor vel facilisis potenti ut, pede at non lorem a commodo, nulla dolor orci interdum vestibulum nulla. Dui nulla vestibulum quisque a pharetra porta, integer nec ipsum nec sed dui pharetra, magna et dignissim ipsum sed dictum, litora eros vivamus scelerisque libero ipsum. Sed ac ac lorem molestie adipiscing morbi, pellentesque imperdiet nunc quis morbi amet ante, libero dui ligula nec risus neque et, velit nonummy phasellus et facilisi amet, ligula in elementum non sapien pulvinar faucibus. Eu leo ut posuere sed aliquet, tincidunt vel urna volutpat tempus sem, sit felis aliquet vestibulum condimentum sit, amet nibh vel tellus purus ullamcorper libero, nulla vestibulum pede ut vestibulum pretium. Eu nulla vestibulum a neque in metus, quisquam nam sed cursus eget luctus, pede ultrices nec sed dignissim pellentesque, sit class cursus metus nulla placerat mauris, consequat mollis neque vivamus amet pede. Mauris dolor nulla diam eros bibendum, quam ante vestibulum morbi non ligula vel, molestie curabitur rhoncus nulla euismod interdum non. Nulla fringilla lorem mollis ad massa, sit molestie nibh lorem arcu volutpat, accumsan commodo lectus eu et donec, sit tempor tempus rutrum in curabitur amet. Nec urna euismod a tincidunt commodo, eu pede turpis libero vitae viverra, ante vestibulum nam non habitasse potenti, mauris imperdiet in in nunc convallis. Et nostra wisi in est accumsan vehicula, quisque vitae felis mauris sed vulputate nec, ante imperdiet sollicitudin massa iaculis massa sit.

Android-ObservableScrollView

Quam libero nulla netus eu porta curae, ut nulla bibendum facilisis et urna sed, quis congue vestibulum aliquam interdum etiam. Nulla vel lobortis ullamcorper vitae excepturi, neque urna feugiat lectus vel lacinia, massa pretium orci eu metus neque vulputate. Imperdiet ac velit rhoncus nulla malesuada nullam, nec pulvinar justo gravida lorem rutrum magna, habitasse repudiandae mi eros vestibulum ante, nec euismod dui iaculis in turpis pretium, ac id metus egestas proin lacus lectus. Laoreet lorem nec vitae risus erat arcu, vitae quam ut in ante tristique, porta dolor pede quam et odio nam, arcu lacus sem congue ante cursus massa. Et mattis sagittis erat accumsan fusce quam, vehicula ligula beatae natoque fusce sodales conubia, habitasse metus cum magnis viverra nam cursus, egestas urna wisi primis blandit eu magna, eget libero elit lacus lorem dis aliquam. Ut mauris ante natoque lacus massa, justo a lectus sodales enim adipiscing id, accumsan ut ipsum vestibulum sed enim auctor, vitae congue tincidunt id phasellus lacinia scelerisque, tincidunt sapien nulla euismod volutpat iaculis. Platea sociis nec aliquet nec molestie, in mi et augue sapien in vivamus, integer fames proin vitae in ullamcorper et. Fringilla etiam sapiente rhoncus suspendisse nec id, lobortis cras eget egestas dui ac nec, justo lacus ut lorem bibendum quia eros, eget a gravida id donec nunc suscipit, porta sed in sodales non rutrum. Lectus vel dui elementum pellentesque magna aliquam, vitae non sit pede et fusce nibh, id id deserunt ornare dui sit condimentum, in adipiscing imperdiet turpis nam aliquet, facilisis metus magna lacus wisi facilisis tortor. Vulputate elit accumsan quam amet ligula, suspendisse lacus mi nonummy integer urna, libero nulla nunc varius in odio, laoreet nulla amet placerat amet nec. Consectetuer vel massa hendrerit vitae iaculis id, sed ut ut laudantium odio in, elit vestibulum duis ante maecenas interdum in, neque vehicula ultrices varius in quam, pede tellus pellentesque sed nullam quis.

================================================ FILE: samples/assets/lipsum.html ================================================

Lorem ipsum dolor sit amet, ut duis lorem provident sed felis blandit, condimentum donec lectus ipsum et mauris, morbi porttitor interdum feugiat nulla donec sodales, vestibulum nisl primis a molestie vestibulum quam, sapien mauris metus risus suspendisse magnis. Augue viverra nulla faucibus egestas eu, a etiam id congue rutrum ante, arcu tincidunt donec quam felis at ornare, iaculis ligula sodales venenatis commodo volutpat neque, suspendisse elit praesent tellus felis mi amet. Inceptos amet tempor lectus lorem est non, ac donec ac libero neque mauris, tellus ante metus eget leo consequat. Scelerisque dolor curabitur pretium blandit ut feugiat, amet lacus pulvinar justo convallis ut, sed natoque ipsum urna posuere nibh eu. Sed at sed vulputate sit orci, facilisis a aliquam tellus quam aliquam, eu aliquam donec at molestie ante, pellentesque mauris lorem ultrices libero faucibus porta, imperdiet adipiscing sit hac diam ut nulla. Lacus enim elit pulvinar donec vehicula dapibus, accumsan purus officia cursus dolor sapien, eu amet dis mauris mi nulla ut. Non accusamus etiam pede non urna tempus, vestibulum aliquam tortor eget pharetra sodales, in vestibulum ut justo orci nulla, lobortis purus sem semper consectetuer magni purus. Dolor a leo vestibulum amet ut sit, arcu ut eaque urna fusce aliquet turpis, sed fermentum sed vestibulum nisl pede, tristique enim lorem posuere in laborum ut. Vestibulum id id justo leo nulla, magna lobortis ullamcorper et dignissim pellentesque, duis suspendisse quis id lorem ante. Vivamus a nullam ante adipiscing amet, mi vel consectetuer nunc aenean pede quisque, eget rhoncus dis porttitor habitant nunc vivamus, duis cubilia blandit non donec justo dictumst, praesent vitae nulla nam pulvinar urna. Adipiscing adipiscing justo urna pulvinar imperdiet nullam, vitae fusce rhoncus proin nonummy suscipit, ullamcorper amet et non potenti platea ultrices, mauris nullam sapien nunc justo vel, eu semper pellentesque arcu fusce augue. Malesuada mauris nibh sit a a scelerisque, velit sem lectus tellus convallis consectetuer, ultricies auctor a ante eros amet sed.

Risus lacus duis leo platea wisi, felis maecenas rutrum in id in donec, non id a potenti libero eget, posuere elit ea sed pellentesque quis. Sunt lacus urna lorem elit duis, nibh donec purus quisque consectetuer dolor, neque vestibulum proin ornare eros nonummy phasellus. Iaculis cras eu at egestas dolor montes, viverra quisque malesuada consectetuer semper maecenas, a sed vitae donec tempor aliqua metus, ornare mollis suscipit et erat fusce, sit orci aut auctor elementum fames aliquam. Platea dui integer magnis non metus, minus dignissimos ante massa nostra et, rutrum sapien egestas quis sapien donec donec. Erat sit a eros aenean natoque, quam libero id lorem enim proin, lorem ipsum fermentum mattis metus et. Aliquam aliquet suscipit purus conubia at neque, platea vivamus vestibulum nulla quibusdam senectus, et morbi lectus malesuada gravida donec, elementum sit convallis pellentesque velit amet. Et eveniet viverra vehicula consectetuer justo, provident sed commodo non lacinia velit, tempor phasellus vel leo nisl cras, vivamus et arcu interdum dui eu amet. Volutpat wisi rhoncus vel turpis diam quibusdam, dapibus elit est quisque cubilia mauris, nulla elit magna tempor accumsan bibendum, lorem varius sed interdum eget mattis, scelerisque egestas feugiat donec dui molestie. Leo facilisis nisl sit montes ligula sed, enim commodo consectetuer nunc est et, ut sed vehicula dolor luctus elit. Fermentum cras donec eget nibh est vel, sed justo risus et pharetra diam, eu vivamus egestas ligula risus diam, sed justo eget hac ut mauris. Vestibulum diam nec vitae mi eget suspendisse, aenean arcu purus facilisis purus class in, id aliquam sit id scelerisque sapien etiam. Ut nullam sit sed at mauris lobortis, consequat dolor autem ipsum euismod nulla, elit quis proin eget conubia varius, erat arcu massa mus in mauris, scelerisque ut eu sollicitudin libero leo urna.

Consectetuer luctus tempor elit ut dolor ligula, quis dui per dui hendrerit ante sagittis, in quisque pretium in eleifend enim. Condimentum iaculis vitae feugiat dis tellus vel, lectus dolor nec dui nulla nascetur, et pellentesque curabitur lorem leo velit eget. Id nascetur arcu lobortis suspendisse imperdiet urna, natoque nascetur ante in porta a, interdum hendrerit mi bibendum platea tellus, urna in enim ornare vestibulum faucibus enim. Leo fusce egestas ante nec volutpat, in tempor vel facilisis potenti ut, pede at non lorem a commodo, nulla dolor orci interdum vestibulum nulla. Dui nulla vestibulum quisque a pharetra porta, integer nec ipsum nec sed dui pharetra, magna et dignissim ipsum sed dictum, litora eros vivamus scelerisque libero ipsum. Sed ac ac lorem molestie adipiscing morbi, pellentesque imperdiet nunc quis morbi amet ante, libero dui ligula nec risus neque et, velit nonummy phasellus et facilisi amet, ligula in elementum non sapien pulvinar faucibus. Eu leo ut posuere sed aliquet, tincidunt vel urna volutpat tempus sem, sit felis aliquet vestibulum condimentum sit, amet nibh vel tellus purus ullamcorper libero, nulla vestibulum pede ut vestibulum pretium. Eu nulla vestibulum a neque in metus, quisquam nam sed cursus eget luctus, pede ultrices nec sed dignissim pellentesque, sit class cursus metus nulla placerat mauris, consequat mollis neque vivamus amet pede. Mauris dolor nulla diam eros bibendum, quam ante vestibulum morbi non ligula vel, molestie curabitur rhoncus nulla euismod interdum non. Nulla fringilla lorem mollis ad massa, sit molestie nibh lorem arcu volutpat, accumsan commodo lectus eu et donec, sit tempor tempus rutrum in curabitur amet. Nec urna euismod a tincidunt commodo, eu pede turpis libero vitae viverra, ante vestibulum nam non habitasse potenti, mauris imperdiet in in nunc convallis. Et nostra wisi in est accumsan vehicula, quisque vitae felis mauris sed vulputate nec, ante imperdiet sollicitudin massa iaculis massa sit.

Quam libero nulla netus eu porta curae, ut nulla bibendum facilisis et urna sed, quis congue vestibulum aliquam interdum etiam. Nulla vel lobortis ullamcorper vitae excepturi, neque urna feugiat lectus vel lacinia, massa pretium orci eu metus neque vulputate. Imperdiet ac velit rhoncus nulla malesuada nullam, nec pulvinar justo gravida lorem rutrum magna, habitasse repudiandae mi eros vestibulum ante, nec euismod dui iaculis in turpis pretium, ac id metus egestas proin lacus lectus. Laoreet lorem nec vitae risus erat arcu, vitae quam ut in ante tristique, porta dolor pede quam et odio nam, arcu lacus sem congue ante cursus massa. Et mattis sagittis erat accumsan fusce quam, vehicula ligula beatae natoque fusce sodales conubia, habitasse metus cum magnis viverra nam cursus, egestas urna wisi primis blandit eu magna, eget libero elit lacus lorem dis aliquam. Ut mauris ante natoque lacus massa, justo a lectus sodales enim adipiscing id, accumsan ut ipsum vestibulum sed enim auctor, vitae congue tincidunt id phasellus lacinia scelerisque, tincidunt sapien nulla euismod volutpat iaculis. Platea sociis nec aliquet nec molestie, in mi et augue sapien in vivamus, integer fames proin vitae in ullamcorper et. Fringilla etiam sapiente rhoncus suspendisse nec id, lobortis cras eget egestas dui ac nec, justo lacus ut lorem bibendum quia eros, eget a gravida id donec nunc suscipit, porta sed in sodales non rutrum. Lectus vel dui elementum pellentesque magna aliquam, vitae non sit pede et fusce nibh, id id deserunt ornare dui sit condimentum, in adipiscing imperdiet turpis nam aliquet, facilisis metus magna lacus wisi facilisis tortor. Vulputate elit accumsan quam amet ligula, suspendisse lacus mi nonummy integer urna, libero nulla nunc varius in odio, laoreet nulla amet placerat amet nec. Consectetuer vel massa hendrerit vitae iaculis id, sed ut ut laudantium odio in, elit vestibulum duis ante maecenas interdum in, neque vehicula ultrices varius in quam, pede tellus pellentesque sed nullam quis.

================================================ FILE: samples/build.gradle ================================================ apply plugin: 'com.android.application' // for using SNAPSHOT //repositories { // maven { // url uri('https://oss.sonatype.org/content/repositories/snapshots/') // } //} dependencies { compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.nineoldandroids:library:2.4.0' compile 'com.melnykov:floatingactionbutton:1.0.7' debugCompile project(':library') // Release build uses the synced latest version releaseCompile "com.github.ksoichiro:android-observablescrollview:${SYNCED_VERSION_NAME}" // for using SNAPSHOT //compile "com.github.ksoichiro:android-observablescrollview:$VERSION_NAME" } apply from: "${rootDir}/gradle/version.gradle" project.ext.versionInfo.releaseVersionName = SYNCED_VERSION_NAME android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "com.github.ksoichiro.android.observablescrollview.samples" versionCode 5 versionName "1.3.0" } productFlavors { // for development dev { } // for Google Play Store release play { // The first version is removed from the Google Play store // due to a violation of the branding guidelines. // Therefore the suffix "sample" is replaced to "sample2". applicationId "com.github.ksoichiro.android.observablescrollview.samples2" } } signingConfigs { release { def filePrivateProperties = file("private.properties") if (filePrivateProperties.exists()) { Properties propsPrivate = new Properties() propsPrivate.load(new FileInputStream(filePrivateProperties)) storeFile file(propsPrivate['key.store']) keyAlias propsPrivate['key.alias'] storePassword propsPrivate['key.store.password'] keyPassword propsPrivate['key.alias.password'] } } } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-debug" } release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' def filePrivateProperties = file("private.properties") if (filePrivateProperties.exists()) { signingConfig signingConfigs.release } } } sourceSets { main { manifest.srcFile 'AndroidManifest.xml' res.srcDirs = ['res'] assets.srcDirs = ['assets'] } } lintOptions { abortOnError false } // Rename APK files applicationVariants.all { variant -> def output = variant.outputs.get(0) File apk = output.outputFile String newName = output.outputFile.name.replace(".apk", "-${variant.mergedFlavor.versionCode}-${variant.mergedFlavor.versionName}-${project.ext.versionInfo.build}.apk") .replace("app-", "${variant.mergedFlavor.applicationId}-") output.outputFile = new File(apk.parentFile, newName) } } ================================================ FILE: samples/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Applications/adt-bundle-mac-x86_64-20131030/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: samples/res/color/tab_text_color.xml ================================================ ================================================ FILE: samples/res/drawable/gradient_header_background.xml ================================================ ================================================ FILE: samples/res/drawable/sliding_header_overlay.xml ================================================ ================================================ FILE: samples/res/layout/activity_about.xml ================================================ ================================================ FILE: samples/res/layout/activity_actionbarcontrolgridview.xml ================================================ ================================================ FILE: samples/res/layout/activity_actionbarcontrollistview.xml ================================================ ================================================ FILE: samples/res/layout/activity_actionbarcontrolrecyclerview.xml ================================================ ================================================ FILE: samples/res/layout/activity_actionbarcontrolscrollview.xml ================================================ ================================================ FILE: samples/res/layout/activity_actionbarcontrolwebview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgap3listview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgap3recyclerview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgap3scrollview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgaplistview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgaprecyclerview.xml ================================================ ================================================ FILE: samples/res/layout/activity_fillgapscrollview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacetoolbarscrollview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacetoolbarwebview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagegridview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagelistview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagerecyclerview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagescrollview.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagewithviewpagertab.xml ================================================ ================================================ FILE: samples/res/layout/activity_flexiblespacewithimagewithviewpagertab2.xml ================================================ ================================================ FILE: samples/res/layout/activity_fragmentactionbarcontrol.xml ================================================ ================================================ FILE: samples/res/layout/activity_fragmenttransition.xml ================================================ ================================================ FILE: samples/res/layout/activity_handletouchgridview.xml ================================================ ================================================ FILE: samples/res/layout/activity_handletouchlistview.xml ================================================ ================================================ FILE: samples/res/layout/activity_handletouchrecyclerview.xml ================================================ ================================================ FILE: samples/res/layout/activity_handletouchscrollview.xml ================================================ <%- name %> ================================================ FILE: website/public/browserconfig.xml ================================================ #ffffff ================================================ FILE: website/public/css/_code.less ================================================ // Based on androidstudio.css // made by Author: Pedro Oliveira @code-bg-color: #263238; // Blue Grey 900 pre { border-width: 0px; border-radius: 0px; background: @code-bg-color; @code-bg-shadow-alpha: 0.6; -webkit-box-shadow: 0 4px 6px 2px rgba(24, 24, 24, @code-bg-shadow-alpha) inset; -moz-box-shadow: 0 4px 6px 2px rgba(24, 24, 24, @code-bg-shadow-alpha) inset; box-shadow: 0 4px 6px 2px rgba(24, 24, 24, @code-bg-shadow-alpha) inset; } .nohighlight, .hljs { color: #A9B7C6; background: @code-bg-color; display: block; overflow-x: auto; padding: 12px; webkit-text-size-adjust: none; } .hljs-number { color: #6897BB; } .hljs-keyword, .hljs-deletion { color: #CC7832; } .hljs-javadoc { color: #629755; } .hljs-comment { color: #808080; } .hljs-annotation { color: #BBB529; } .hljs-string, .hljs-addition { color: #6A8759; } .hljs-function .hljs-title, .hljs-change { color: #FFC66D; } .hljs-tag .hljs-title, .hljs-doctype { color: #E8BF6A; } .hljs-tag .hljs-attribute { color: #BABABA; } .hljs-tag .hljs-value { color: #A5C261; } ================================================ FILE: website/public/css/_colors.less ================================================ @theme-main-color: #009688; // 500 @theme-focus-color: #00897B; // 600 @theme-brand-color: #E0F2F1; // 50 @theme-focus-border-color: #00695C; // 800 @theme-active-color: #00695C; // 800 .navbar-inverse { background-color: @theme-main-color; border-color: @theme-main-color; .navbar-brand, .navbar-nav>li>a { color: @theme-brand-color; } .navbar-nav>.active>a, .navbar-nav>.active>a:focus, .navbar-nav>.active>a:hover { background-color: @theme-active-color; } .navbar-nav>.open>a, .navbar-nav>.open>a:focus, .navbar-nav>.open>a:hover { background-color: @theme-focus-color; } .navbar-toggle { background-color: @theme-main-color; } .navbar-toggle { border-color: @theme-focus-border-color; &:hover, &:focus { background-color: @theme-focus-color; } } .navbar-collapse { border-color: @theme-focus-border-color; } } code { color: @theme-main-color; background-color: lighten(@theme-brand-color, 7%); } a, a:hover { color: @theme-main-color; } a:hover { text-decoration: none; } h1, h2 { color: @theme-main-color; } ================================================ FILE: website/public/css/_fonts.less ================================================ @import '_roboto-fonts.less'; body, h1, h2, h3, h4, h5, h6, p { font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif; font-weight: 300; color: #212121; } body { font-size: 14px; } h1 { font-size: 28px; font-weight: 400; } h2 { font-size: 22px; font-weight: 400; } h3 { font-size: 20px; } #sidebar-main-content { font-size: 16px; h1 { font-size: 32px; } h2 { font-size: 28px; } h3 { font-size: 22px; font-weight: 400; } h4 { font-size: 18px; font-weight: 400; } h5 { font-size: 16px; font-weight: 400; } } ================================================ FILE: website/public/css/_footer.less ================================================ @import (reference) '_mixins.less'; // Footer .footer { .container; .override-container; padding: 15px; border-top: 1px solid lighten(#E0F2F1, 7%); .btns { text-align: right; padding-bottom: 15px; .ghstar { width: 100px; height: 20px; } .ghfork { width: 100px; height: 20px; } } .copyright { .text-muted; .clearfix; text-align: right; font-size: 12px; line-height: 150%; } } ================================================ FILE: website/public/css/_layout.less ================================================ @import (reference) '_mixins.less'; html, body { overflow-x: hidden; } body { padding-top: 70px; } h1 { margin-top: 8px; } // Markdown fix // Apply .table to all s table { .table; } .container { .override-container; } #sidebar-main-content { h1 { margin-bottom: 16px; } h2 { margin-top: 34px; margin-bottom: 18px; } h3 { margin-top: 26px; margin-bottom: 16px; } h4 { margin-top: 22px; margin-bottom: 16px; } h5 { margin-top: 20px; margin-bottom: 16px; } p, ol, ul, pre { margin-bottom: 16px; } } ================================================ FILE: website/public/css/_misc.less ================================================ // Showing fragment links #sidebar-main-content { h2, h3, h4, h5 { position: relative; .anchor { visibility: hidden; padding-left: 4px; } &:hover { .anchor { visibility: visible; } } } .marker { position: absolute; top: -70px; left: 0; padding: 0; margin: 0; } h2 .anchor { font-size: 20px; } h3 .anchor { font-size: 16px; color: #212121; } h4 .anchor { font-size: 14px; color: #212121; } h5 .anchor { font-size: 12px; color: #212121; } img { -webkit-box-shadow: 0 2px 4px 2px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 2px 4px 2px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px 2px rgba(0, 0, 0, 0.3); } a>img { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } } ================================================ FILE: website/public/css/_mixins.less ================================================ // Overwrite bootstrap's default .override-container(@width: 970px) { @media (min-width: 1200px) { & { width: @width; } } } ================================================ FILE: website/public/css/_navbar.less ================================================ .navbar-shadow() { @shadow-h-offset: 0px; @shadow-v-offset: -3px; @shadow-blur-radius: 6px; @shadow-spread-radius: 6px; @shadow-color: rgba(32, 32, 32, 0.4); -moz-box-shadow: @shadow-h-offset @shadow-v-offset @shadow-blur-radius @shadow-spread-radius @shadow-color; -webkit-box-shadow: @shadow-h-offset @shadow-v-offset @shadow-blur-radius @shadow-spread-radius @shadow-color; box-shadow: @shadow-h-offset @shadow-v-offset @shadow-blur-radius @shadow-spread-radius @shadow-color; } .navbar-shadow-clear() { -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } nav.navbar { .navbar-shadow(); .navbar-brand { background-image: url('../images/logo.svg'); background-repeat: no-repeat; background-position: 12px center; padding-left: 64px; } } @media screen and (max-width: 767px) { nav.navbar { .navbar-brand { background-position: 4px center; padding-left: 56px; padding-right: 8px; } } } ================================================ FILE: website/public/css/_roboto-fonts.less ================================================ @roboto-font-path: '../lib/roboto-fontface/fonts'; .roboto-font(@type, @weight, @style) { @font-face { font-family: 'Roboto'; src: url('@{roboto-font-path}/Roboto-@{type}.eot'); src: url('@{roboto-font-path}/Roboto-@{type}.eot?#iefix') format('embedded-opentype'), url('@{roboto-font-path}/Roboto-@{type}.woff2') format('woff2'), url('@{roboto-font-path}/Roboto-@{type}.woff') format('woff'), url('@{roboto-font-path}/Roboto-@{type}.ttf') format('truetype'), url('@{roboto-font-path}/Roboto-@{type}.svg#Roboto') format('svg'); font-weight: @weight; font-style: @style; } @font-face { font-family: 'Roboto-@{type}'; src: url('@{roboto-font-path}/Roboto-@{type}.eot'); src: url('@{roboto-font-path}/Roboto-@{type}.eot?#iefix') format('embedded-opentype'), url('@{roboto-font-path}/Roboto-@{type}.woff2') format('woff2'), url('@{roboto-font-path}/Roboto-@{type}.woff') format('woff'), url('@{roboto-font-path}/Roboto-@{type}.ttf') format('truetype'), url('@{roboto-font-path}/Roboto-@{type}.svg#Roboto') format('svg'); } } .roboto-font-pair(@type, @weight) { .roboto-font('@{type}', @weight, normal); .roboto-font('@{type}Italic', @weight, italic); } .roboto-font-pair('Thin', 100); .roboto-font-pair('Light', 300); .roboto-font-pair('Regular', 400); ================================================ FILE: website/public/css/_sidebar.less ================================================ @import (reference) '../../bower_components/bootstrap/less/bootstrap.less'; // Page with sidebar .btn-sidebar-toggle { .btn; .btn-default; .btn-xs; } #grid-content { .make-row(); #sidebar { .make-xs-column(6); .make-sm-column(3); ul { padding-left: 0px; } &>ul { border-right: 1px solid #E0E0E0; padding-right: 15px; li { position: relative; display: block; } &>li { padding: 8px; } .section { font-size: 14px; font-weight: 400; margin: 0px; margin-bottom: 8px; &, &>a { color: #009688; text-decoration: none; } } .topic { &>a { color: #212121; } } } } #sidebar-main-content { .make-xs-column(12); .make-sm-column(9); } } @media screen and (max-width: 767px) { #grid-content { position: relative; -webkit-transition: all .25s ease-out; -o-transition: all .25s ease-out; transition: all .25s ease-out; left: 0; &.active { left: 50%; } #sidebar { left: -50%; position: absolute; top: 0; width: 50%; } } } ================================================ FILE: website/public/css/_site-top.less ================================================ @import (reference) '_mixins.less'; @import (reference) '_colors.less'; @content-header-height: 300px; body#site-top { padding-top: 0px; #content-header { padding-top: 100px; height: @content-header-height; background-color: #ffffff; #site-title { font-weight: 100; padding-top: 50px; color: @theme-main-color; } } #main-content { padding-top: 30px; h1 { display: none; } } } @media screen and (min-width: 768px) { body#site-top { padding-top: 0px; nav.navbar { background: none; border-color: transparent; .navbar-shadow-clear(); &.sticky { background-color: @theme-main-color; .navbar-shadow(); } .navbar-brand { background-image: none; background-position: left center; padding-left: 0px; visibility: hidden; &.visible { visibility: visible; padding-left: 15px; } } } nav.navbar { #navbar { &.right { @translate-amount: -250px; -moz-transform: translateX(@translate-amount); -webkit-transform: translateX(@translate-amount); -o-transform: translateX(@translate-amount); -ms-transform: translateX(@translate-amount); } } } padding-top: 0px; #content-header { display: block; padding-top: 100px; height: @content-header-height; background-color: @theme-main-color; #site-title { font-size: 48px; color: #ffffff; visibility: hidden; &.visible { visibility: visible; } } } } } ================================================ FILE: website/public/css/main.less ================================================ @import (reference) '../../bower_components/bootstrap/less/bootstrap.less'; @import (reference) '_mixins.less'; @import '_fonts.less'; @import '_colors.less'; @import '_layout.less'; @import '_navbar.less'; @import '_footer.less'; @import '_sidebar.less'; @import '_site-top.less'; @import '_code.less'; @import '_misc.less'; ================================================ FILE: website/public/index.ejs ================================================ <%- partial("_head") %>

<%- title %>

<%- partial('../../README') %>
<%- partial("_footer") %> ================================================ FILE: website/public/js/main.coffee ================================================ # Disable highlight for license quotation $('.language-license').addClass 'nohighlight' # Remove .md, except external links $("a[href$='.md']").not("[href^='http']").each -> @.href = @.href.replace /\.md$/, "" # Insert subdirectory for links base = $("meta[name='base']").attr('content') if base != "" $("a[href$='.md']").not("[href^='http']").each -> @.href = @.href.replace 'docs/', "#{base}/docs/" # Toggling sidebar $(document).ready -> $('[data-toggle="offcanvas"]').click -> $('#grid-content').toggleClass('active') if $('#grid-content').hasClass('active') $('[data-toggle="offcanvas"]').text 'Hide menu' else $('[data-toggle="offcanvas"]').text 'Show menu' if $('#site-top') $(window).scroll -> if 70 < $(document).scrollTop() $('.navbar-brand').addClass('visible') $('#site-title').removeClass('visible') $('#navbar').removeClass('right') else $('.navbar-brand').removeClass('visible') $('#site-title').addClass('visible') $('#navbar').addClass('right') if 250 < $(document).scrollTop() $('nav').addClass('sticky') else $('nav').removeClass('sticky') # Create fragment links $(document).ready -> $('#sidebar-main-content h2, #sidebar-main-content h3, #sidebar-main-content h4, #sidebar-main-content h5').each -> fragment = $(@).text().toLowerCase().replace(/[ _\.\/]/g, '-').replace(/--+/g, '-').replace(/([,':()\?!]|-+ |-+$)/g, '') $(@).html($(@).html() + '#') ================================================ FILE: website/public/manifest.json ================================================ { "name": "App", "icons": [ { "src": "\/android-icon-36x36.png", "sizes": "36x36", "type": "image\/png", "density": "0.75" }, { "src": "\/android-icon-48x48.png", "sizes": "48x48", "type": "image\/png", "density": "1.0" }, { "src": "\/android-icon-72x72.png", "sizes": "72x72", "type": "image\/png", "density": "1.5" }, { "src": "\/android-icon-96x96.png", "sizes": "96x96", "type": "image\/png", "density": "2.0" }, { "src": "\/android-icon-144x144.png", "sizes": "144x144", "type": "image\/png", "density": "3.0" }, { "src": "\/android-icon-192x192.png", "sizes": "192x192", "type": "image\/png", "density": "4.0" } ] } ================================================ FILE: wercker.yml ================================================ box: wercker/android # Build definition build: # The steps that will be executed on build steps: - script: name: show base information code: | ./gradlew -v echo $ANDROID_HOME echo $ANDROID_SDK_VERSION echo $ANDROID_BUILD_TOOLS echo $ANDROID_UPDATE_FILTER echo $ANDROID_NDK_HOME - android-sdk-update: filter: tools,platform-tools - android-sdk-update: filter: android-21,android-22,android-23,build-tools-23.0.2,extra-android-support,extra-android-m2repository # A step that executes `gradle build` command - script: name: run gradle code: | ./gradlew --full-stacktrace -q --project-cache-dir=$WERCKER_CACHE_DIR assembleDebug after-steps: - script: name: inspect build result code: | ls -la ./samples/build/outputs/apk cp ./samples/build/outputs/apk/*.apk $WERCKER_REPORT_ARTIFACTS_DIR rm -f $WERCKER_REPORT_ARTIFACTS_DIR/*-unaligned.apk