diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3269cbdf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +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 diff --git a/.gitignore b/.gitignore index 2cd75b76..cb4636b6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,11 @@ private.properties /.idea/ .DS_Store /build +bin/ +gen/ +libs/ +aarDependencies/ +.project +.classpath +project.properties +*.swp diff --git a/.travis-script.sh b/.travis-script.sh new file mode 100644 index 00000000..75f99da7 --- /dev/null +++ b/.travis-script.sh @@ -0,0 +1,19 @@ +#!/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 diff --git a/.travis.yml b/.travis.yml index 648bb908..6b4064ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,35 @@ language: android -android: - components: - - build-tools-21.1.1 - - tools - #- android-19 - - android-21 - #- system-image - - extra-android-support - - extra-android-m2repository - licenses: - - 'android-sdk-license-.+' -#before_script: - #- echo no | android create avd --force -n test -t android-19 --abi default/armeabi-v7a - #- emulator -avd test -no-skin -no-audio -no-window & - #- android-wait-for-emulator +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: - # 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. - - travis_retry ./gradlew --full-stacktrace -q assembleDebug +- "/bin/bash .travis-script.sh" +after_success: +- true && ([ "$TEST_TARGET" != "android" ] || ./gradlew :library:coveralls) diff --git a/README.md b/README.md index 02329299..a7887ccb 100644 --- a/README.md +++ b/README.md @@ -1,196 +1,108 @@ -Android-ObservableScrollView -=== +# 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. -![](observablescrollview-samples/demo12.gif) -![](observablescrollview-samples/demo10.gif) -![](observablescrollview-samples/demo11.gif) -![](observablescrollview-samples/demo13.gif) +![](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) -![](observablescrollview-samples/demo1.gif) -![](observablescrollview-samples/demo2.gif) -![](observablescrollview-samples/demo3.gif) -![](observablescrollview-samples/demo4.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) -![](observablescrollview-samples/demo5.gif) -![](observablescrollview-samples/demo6.gif) -![](observablescrollview-samples/demo7.gif) -![](observablescrollview-samples/demo8.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) -![](observablescrollview-samples/demo9.gif) +## Examples -## Samples +### Download from Google Play -### Google Play store +Get it on 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. -### wercker +### Download from wercker -If you are a wercker user, you can download the latest build artifact. -[See here for details](https://github.com/ksoichiro/Android-ObservableScrollView/tree/master/docs/wercker.md). +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 with Gradle +### Install manually -Clone this repository and build the app using Gradle wrapper. - -```sh -$ git clone https://github.com/ksoichiro/Android-ObservableScrollView.git -$ cd Android-ObservableScrollView -$ ./gradlew installDevDebug -``` +Just clone and execute `installDevDebug` task with Gradle. +[See here for details](docs/example/android-studio.md). ## Usage -### Add to your dependencies - -AAR is distributed on the Maven Central repository. - -```groovy -repositories { - mavenCentral() -} - -dependencies { - compile 'com.github.ksoichiro:android-observablescrollview:1.4.0' -} -``` - -Eclipse is not supported but if you really want to build on Eclipse, [see here](https://github.com/ksoichiro/Android-ObservableScrollView/tree/master/docs/eclipse.md). +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. -### Add widgets to your layout - -Use one of the `ObservableListView`, `ObservableScrollView`, `ObservableWebView`, `ObservableRecyclerView`, `ObservableGridView` in your XML layout file. - -### Control scroll events with callbacks - -Widgets above provides callbacks with `ObservableScrollViewCallbacks` interface. -You can listen scroll events of the widgets by using `setScrollViewCallbacks()`. - -```java - ObservableListView listView = (ObservableListView) findViewById(R.id.list); - listView.setScrollViewCallbacks(this); -``` - -Then implement your interaction codes to the callbacks. -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(); - } - } - } -``` +## Reference -See [sample app's Activity codes](https://github.com/ksoichiro/Android-ObservableScrollView/tree/master/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples) for more details. +* [Supported widgets](docs/reference/supported-widgets.md) +* [Environment](docs/reference/environment.md) +* [Release notes](docs/reference/release-notes.md) +* [FAQ](docs/faq.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 | | - - -## Environment - -This project is built and tested under the following environment. - -| OS | IDE | JDK | -| -------------- | ------------------ | --- | -| Mac OS X 10.10 | Android Studio 1.0 | 1.7 | - -## Release notes - -* 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. - - -## Apps that uses this library +## 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](soichiro.kashima@gmail.com) or pull requests or issues. +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 [contributing guideline](https://github.com/ksoichiro/Android-ObservableScrollView/tree/master/CONTRIBUTING.md) before submitting a new issue. +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 - [soichiro.kashima@gmail.com](mailto:soichiro.kashima@gmail.com) -## Credits +## Thanks * Inspired by `ObservableScrollView` in [romannurik-code](https://code.google.com/p/romannurik-code/). ## 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 +```license +Copyright 2014 Soichiro Kashima - http://www.apache.org/licenses/LICENSE-2.0 +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 - 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. + 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. +``` diff --git a/build.gradle b/build.gradle index f694d3ab..cdfb0477 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,11 @@ 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 { @@ -11,3 +16,22 @@ allprojects { 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 +} diff --git a/docs/_data.json b/docs/_data.json new file mode 100644 index 00000000..355808da --- /dev/null +++ b/docs/_data.json @@ -0,0 +1,8 @@ +{ + "overview": { + "title": "Overview" + }, + "faq": { + "title": "FAQ" + } +} diff --git a/docs/_layout.ejs b/docs/_layout.ejs new file mode 100644 index 00000000..601a77d8 --- /dev/null +++ b/docs/_layout.ejs @@ -0,0 +1,41 @@ + + + +<%- partial("../_head", { title: title, ogType: "article" }) %> + + + +<%- partial("../_nav") %> + +
+

+ +

+
+ +
+<%- yield %> +
+
+
+ +<%- partial("../_footer") %> + + diff --git a/docs/advanced/_data.json b/docs/advanced/_data.json new file mode 100644 index 00000000..88118141 --- /dev/null +++ b/docs/advanced/_data.json @@ -0,0 +1,11 @@ +{ + "index": { + "title": "Advanced techniques" + }, + "sliding-up": { + "title": "Sliding up pattern" + }, + "viewpager": { + "title": "ViewPager pattern" + } +} diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 00000000..3adac725 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,10 @@ +# 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) diff --git a/docs/advanced/sliding-up.md b/docs/advanced/sliding-up.md new file mode 100644 index 00000000..6e2a7084 --- /dev/null +++ b/docs/advanced/sliding-up.md @@ -0,0 +1,17 @@ +# 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) diff --git a/docs/advanced/viewpager.md b/docs/advanced/viewpager.md new file mode 100644 index 00000000..a991173b --- /dev/null +++ b/docs/advanced/viewpager.md @@ -0,0 +1,14 @@ +# 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... diff --git a/docs/basic/_data.json b/docs/basic/_data.json new file mode 100644 index 00000000..d05c8adf --- /dev/null +++ b/docs/basic/_data.json @@ -0,0 +1,26 @@ +{ + "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" + } +} diff --git a/docs/basic/filling-gap.md b/docs/basic/filling-gap.md new file mode 100644 index 00000000..e8d27881 --- /dev/null +++ b/docs/basic/filling-gap.md @@ -0,0 +1,84 @@ +# 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) diff --git a/docs/basic/flexible-space-toolbar.md b/docs/basic/flexible-space-toolbar.md new file mode 100644 index 00000000..100c7a62 --- /dev/null +++ b/docs/basic/flexible-space-toolbar.md @@ -0,0 +1,288 @@ +# 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) diff --git a/docs/basic/flexible-space-with-image.md b/docs/basic/flexible-space-with-image.md new file mode 100644 index 00000000..baf82eab --- /dev/null +++ b/docs/basic/flexible-space-with-image.md @@ -0,0 +1,250 @@ +# 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) diff --git a/docs/basic/index.md b/docs/basic/index.md new file mode 100644 index 00000000..84896817 --- /dev/null +++ b/docs/basic/index.md @@ -0,0 +1,13 @@ +# 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) diff --git a/docs/basic/parallax-image.md b/docs/basic/parallax-image.md new file mode 100644 index 00000000..b942d779 --- /dev/null +++ b/docs/basic/parallax-image.md @@ -0,0 +1,441 @@ +# 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) diff --git a/docs/basic/show-hide-action-bar.md b/docs/basic/show-hide-action-bar.md new file mode 100644 index 00000000..12c281e8 --- /dev/null +++ b/docs/basic/show-hide-action-bar.md @@ -0,0 +1,111 @@ +# 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) diff --git a/docs/basic/sticky-header.md b/docs/basic/sticky-header.md new file mode 100644 index 00000000..2109d253 --- /dev/null +++ b/docs/basic/sticky-header.md @@ -0,0 +1,313 @@ +# 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) diff --git a/docs/basic/translating-toolbar.md b/docs/basic/translating-toolbar.md new file mode 100644 index 00000000..8d681874 --- /dev/null +++ b/docs/basic/translating-toolbar.md @@ -0,0 +1,208 @@ +# 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) diff --git a/docs/contributor/_data.json b/docs/contributor/_data.json new file mode 100644 index 00000000..28ab9142 --- /dev/null +++ b/docs/contributor/_data.json @@ -0,0 +1,14 @@ +{ + "index": { + "title": "For contributors" + }, + "ci": { + "title": "CI" + }, + "update-website": { + "title": "Update website" + }, + "release": { + "title": "Release" + } +} diff --git a/docs/contributor/ci.md b/docs/contributor/ci.md new file mode 100644 index 00000000..f861d522 --- /dev/null +++ b/docs/contributor/ci.md @@ -0,0 +1,3 @@ +# CI + +Coming soon... diff --git a/docs/contributor/index.md b/docs/contributor/index.md new file mode 100644 index 00000000..a839f990 --- /dev/null +++ b/docs/contributor/index.md @@ -0,0 +1,8 @@ +# 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) + diff --git a/docs/contributor/release.md b/docs/contributor/release.md new file mode 100644 index 00000000..fca348d3 --- /dev/null +++ b/docs/contributor/release.md @@ -0,0 +1,88 @@ +# 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 diff --git a/docs/contributor/update-website.md b/docs/contributor/update-website.md new file mode 100644 index 00000000..575838db --- /dev/null +++ b/docs/contributor/update-website.md @@ -0,0 +1,3 @@ +# Update website + +Coming soon... diff --git a/docs/eclipse.md b/docs/eclipse.md deleted file mode 100644 index aa09add0..00000000 --- a/docs/eclipse.md +++ /dev/null @@ -1,62 +0,0 @@ -# Building on Eclipse - -This library only supports 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, if you really want to build it on Eclipse, here are some hints. -These are not the complete and precise instructions but it might help you. - -If you feel these complicated instructions annoying, then maybe it's time to use the Android Studio. - -## Import library to your project - -1. Install the following components on SDK manager. - * Android 5.0 SDK Platform (Rev.1+) - * Android Support Repository (Rev.9+) - * Android Support Library (Rev.21.0.2+) -1. Import library project of this lib (`observablescrollview` directory) to your workspace. - * Import with "Import Android Code Into Workspace". - * Add `android-support-v7-recyclerview.jar` to build path. - Jar file is located here: - `/path/to/sdk/extra/android/support/v7/recyclerview/libs/android-support-v7-recyclerview.jar` - -## Build the sample app - -After importing library to your workspace, import other dependencies and the sample app. - -1. Import "android-support-v7-appcompat" project to your workspace. - * This project is located in the following path: - `/path/to/sdk/extra/android/support/v7/appcompat` - * See [here](https://developer.android.com/tools/support-library/setup.html#libs-with-res) for details: - * Note that you must modify `target=android-19` to `target=android-21` on `project.properties`. - * Also add `android-support-v4.jar` and `android-support-v7-appcompat.jar` to build path. - They also should be exported. (On "Java Build Path > Order and Export" setting) -1. Import [NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids/) library to your workspace. - * Import "library" directory with "Import Android Code Into Workspace". -1. Import [FloatingActionButton](https://github.com/makovkastar/FloatingActionButton) library to your workspace. - * This lib also uses Android Studio(Gradle), - so import `library` directory with "Import Android Code Into Workspace". - * This depends on "NineOldAndroids" library, so add it as a library. - * Also add `android-support-v4.jar` and `android-support-v7-recyclerview.jar` to build path. - They also should be exported. (On "Java Build Path > Order and Export" setting) -1. Import sample project of this lib (`observablescrollview-samples` directory) to your workspace. - * Import with `Import Android Code Into Workspace`. - * Add `android-support-v4.jar` and `android-support-v7-recyclerview.jar` to build path. - * This depends on "android-support-v7-appcompat", - "FloatingActionButton" and "observablescrollview" libraries, - so add them as a library. - * Sample codes depends on Gradle build mechaninsm, and some codes should be modified: - * On `AndroidManifest.xml`, - * Add `android:versionName` and `android:versionCode` attributes to `` element. - * Replace all `` to ``. - * Add `android:targetSdkVersion="19"` to `` element. - * On `MainActivity`, replace value of `CATEGORY_SAMPLES` to `"com.github.ksoichiro.android.observablescrollview.samples"`. - * On `AboutActivity`, remove the following lines. - * `((TextView) -findViewById(R.id.app_version)).setText(getString(R.string.msg_app_version, -BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH));` - * `((TextView) -findViewById(R.id.lib_version)).setText(getString(R.string.msg_lib_version, -BuildConfig.LIB_VERSION));` diff --git a/docs/example/_data.json b/docs/example/_data.json new file mode 100644 index 00000000..0016d988 --- /dev/null +++ b/docs/example/_data.json @@ -0,0 +1,17 @@ +{ + "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" + } +} diff --git a/docs/example/android-studio.md b/docs/example/android-studio.md new file mode 100644 index 00000000..081c0ae5 --- /dev/null +++ b/docs/example/android-studio.md @@ -0,0 +1,54 @@ +# 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) diff --git a/docs/example/eclipse.md b/docs/example/eclipse.md new file mode 100644 index 00000000..43a45f5d --- /dev/null +++ b/docs/example/eclipse.md @@ -0,0 +1,63 @@ +# 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) diff --git a/docs/example/google-play.md b/docs/example/google-play.md new file mode 100644 index 00000000..fa4865ec --- /dev/null +++ b/docs/example/google-play.md @@ -0,0 +1,13 @@ +# 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) diff --git a/docs/example/index.md b/docs/example/index.md new file mode 100644 index 00000000..e73dc4d9 --- /dev/null +++ b/docs/example/index.md @@ -0,0 +1,11 @@ +# 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) diff --git a/docs/wercker.md b/docs/example/wercker.md similarity index 79% rename from docs/wercker.md rename to docs/example/wercker.md index 05698fbd..fcd022f7 100644 --- a/docs/wercker.md +++ b/docs/example/wercker.md @@ -1,6 +1,7 @@ -# Download the sample app from wercker +# 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 @@ -18,16 +19,18 @@ 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) +![](../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) +![](../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) +![](../images/wercker_3.png) + +[Next: Build on Android Studio »](../../docs/example/android-studio.md) diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..58f69117 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,91 @@ +# 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 + + + +``` + diff --git a/docs/images/basic_1.png b/docs/images/basic_1.png new file mode 100644 index 00000000..c8959097 Binary files /dev/null and b/docs/images/basic_1.png differ diff --git a/docs/images/basic_2.png b/docs/images/basic_2.png new file mode 100644 index 00000000..0e50291d Binary files /dev/null and b/docs/images/basic_2.png differ diff --git a/docs/images/basic_3.png b/docs/images/basic_3.png new file mode 100644 index 00000000..cf12590b Binary files /dev/null and b/docs/images/basic_3.png differ diff --git a/docs/images/basic_4.png b/docs/images/basic_4.png new file mode 100644 index 00000000..9f5f9250 Binary files /dev/null and b/docs/images/basic_4.png differ diff --git a/docs/images/basic_5.png b/docs/images/basic_5.png new file mode 100644 index 00000000..8fcf6895 Binary files /dev/null and b/docs/images/basic_5.png differ diff --git a/docs/images/basic_6.png b/docs/images/basic_6.png new file mode 100644 index 00000000..c033bbc2 Binary files /dev/null and b/docs/images/basic_6.png differ diff --git a/docs/images/basic_7.png b/docs/images/basic_7.png new file mode 100644 index 00000000..062fcc04 Binary files /dev/null and b/docs/images/basic_7.png differ diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..261d8fba --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,31 @@ +# 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. diff --git a/docs/quick-start/_data.json b/docs/quick-start/_data.json new file mode 100644 index 00000000..2ef0780c --- /dev/null +++ b/docs/quick-start/_data.json @@ -0,0 +1,14 @@ +{ + "index": { + "title": "Quick start" + }, + "dependencies": { + "title": "Dependencies" + }, + "layout": { + "title": "Layout" + }, + "animation": { + "title": "Animation codes" + } +} diff --git a/docs/quick-start/animation.md b/docs/quick-start/animation.md new file mode 100644 index 00000000..670c6f19 --- /dev/null +++ b/docs/quick-start/animation.md @@ -0,0 +1,160 @@ +# 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) diff --git a/docs/quick-start/dependencies.md b/docs/quick-start/dependencies.md new file mode 100644 index 00000000..9d6ba48f --- /dev/null +++ b/docs/quick-start/dependencies.md @@ -0,0 +1,28 @@ +# 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) diff --git a/docs/quick-start/index.md b/docs/quick-start/index.md new file mode 100644 index 00000000..31b63d0f --- /dev/null +++ b/docs/quick-start/index.md @@ -0,0 +1,10 @@ +# 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) diff --git a/docs/quick-start/layout.md b/docs/quick-start/layout.md new file mode 100644 index 00000000..df81efb0 --- /dev/null +++ b/docs/quick-start/layout.md @@ -0,0 +1,20 @@ +# 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) diff --git a/docs/reference/_data.json b/docs/reference/_data.json new file mode 100644 index 00000000..fc3b6749 --- /dev/null +++ b/docs/reference/_data.json @@ -0,0 +1,14 @@ +{ + "index": { + "title": "Rerefence" + }, + "supported-widgets": { + "title": "Supported widgets" + }, + "environment": { + "title": "Environment" + }, + "release-notes": { + "title": "Release notes" + } +} diff --git a/docs/reference/environment.md b/docs/reference/environment.md new file mode 100644 index 00000000..d955c592 --- /dev/null +++ b/docs/reference/environment.md @@ -0,0 +1,28 @@ +# 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+) diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..5e29797a --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,5 @@ +# Reference + +1. [Supported widgets](../../docs/basic/supported-widgets.md) +1. [Environment](../../docs/reference/environment.md) +1. [Release notes](../../docs/reference/release-notes.md) diff --git a/docs/reference/release-notes.md b/docs/reference/release-notes.md new file mode 100644 index 00000000..7012d8b9 --- /dev/null +++ b/docs/reference/release-notes.md @@ -0,0 +1,39 @@ +# 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. diff --git a/docs/reference/supported-widgets.md b/docs/reference/supported-widgets.md new file mode 100644 index 00000000..12f3e325 --- /dev/null +++ b/docs/reference/supported-widgets.md @@ -0,0 +1,13 @@ +# 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 | - | diff --git a/gradle.properties b/gradle.properties index 126c9201..5fd0a55d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=1.5.0-SNAPSHOT -SYNCED_VERSION_NAME=1.4.0 +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. diff --git a/observablescrollview/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle similarity index 100% rename from observablescrollview/gradle-mvn-push.gradle rename to gradle/gradle-mvn-push.gradle diff --git a/gradle/version.gradle b/gradle/version.gradle new file mode 100644 index 00000000..9360393e --- /dev/null +++ b/gradle/version.gradle @@ -0,0 +1,59 @@ +// 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()}") + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e760..e7faee01 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/observablescrollview-samples/.gitignore b/library/.gitignore similarity index 100% rename from observablescrollview-samples/.gitignore rename to library/.gitignore diff --git a/observablescrollview/src/main/AndroidManifest.xml b/library/AndroidManifest.xml similarity index 100% rename from observablescrollview/src/main/AndroidManifest.xml rename to library/AndroidManifest.xml diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 00000000..a1bede3c --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,56 @@ +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" diff --git a/observablescrollview/gradle.properties b/library/gradle.properties similarity index 100% rename from observablescrollview/gradle.properties rename to library/gradle.properties diff --git a/library/res/.gitkeep b/library/res/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..d8476e95 --- /dev/null +++ b/library/src/androidTest/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/assets/lipsum.html b/library/src/androidTest/assets/lipsum.html similarity index 100% rename from observablescrollview-samples/src/main/assets/lipsum.html rename to library/src/androidTest/assets/lipsum.html diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/SavedStateTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/SavedStateTest.java new file mode 100644 index 00000000..c98d0fa5 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/SavedStateTest.java @@ -0,0 +1,129 @@ +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); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivity.java new file mode 100644 index 00000000..111b616f --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivity.java @@ -0,0 +1,42 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivityTest.java new file mode 100644 index 00000000..d8245dce --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/GridViewActivityTest.java @@ -0,0 +1,185 @@ +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) { + } + } + }); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivity.java new file mode 100644 index 00000000..70d536e5 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivity.java @@ -0,0 +1,66 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivityTest.java new file mode 100644 index 00000000..3ba7db6a --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/HeaderGridViewActivityTest.java @@ -0,0 +1,242 @@ +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) { + } + } + }); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivity.java new file mode 100644 index 00000000..5d6dc796 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivity.java @@ -0,0 +1,42 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivityTest.java new file mode 100644 index 00000000..cbf1e0b5 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewActivityTest.java @@ -0,0 +1,155 @@ +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]); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivity.java new file mode 100644 index 00000000..6a3509b2 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivity.java @@ -0,0 +1,24 @@ +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); + } + }); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivityTest.java new file mode 100644 index 00000000..db8b465b --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ListViewScrollFromBottomActivityTest.java @@ -0,0 +1,32 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivity.java new file mode 100644 index 00000000..01753974 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivity.java @@ -0,0 +1,36 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivityTest.java new file mode 100644 index 00000000..73d8fa1f --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewActivityTest.java @@ -0,0 +1,156 @@ +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]); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivity.java new file mode 100644 index 00000000..5bf39218 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivity.java @@ -0,0 +1,23 @@ +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); + } + }); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivityTest.java new file mode 100644 index 00000000..37eaf4d5 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/RecyclerViewScrollFromBottomActivityTest.java @@ -0,0 +1,33 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollUtilsTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollUtilsTest.java new file mode 100644 index 00000000..6b2aecdf --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollUtilsTest.java @@ -0,0 +1,26 @@ +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)); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivity.java new file mode 100644 index 00000000..d98a9727 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivity.java @@ -0,0 +1,31 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivityTest.java new file mode 100644 index 00000000..902154ed --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ScrollViewActivityTest.java @@ -0,0 +1,134 @@ +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]); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleHeaderRecyclerAdapter.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleHeaderRecyclerAdapter.java new file mode 100644 index 00000000..18a3887f --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleHeaderRecyclerAdapter.java @@ -0,0 +1,86 @@ +/* + * 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); + } + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleRecyclerAdapter.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleRecyclerAdapter.java new file mode 100644 index 00000000..daf2b5df --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/SimpleRecyclerAdapter.java @@ -0,0 +1,60 @@ +/* + * 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); + } + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivity.java new file mode 100644 index 00000000..5a0891c8 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivity.java @@ -0,0 +1,97 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivityTest.java new file mode 100644 index 00000000..381f2479 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionGridViewActivityTest.java @@ -0,0 +1,44 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivity.java new file mode 100644 index 00000000..3b800748 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivity.java @@ -0,0 +1,97 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivityTest.java new file mode 100644 index 00000000..430b7759 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionListViewActivityTest.java @@ -0,0 +1,44 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivity.java new file mode 100644 index 00000000..a7b80620 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivity.java @@ -0,0 +1,102 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivityTest.java new file mode 100644 index 00000000..caf54346 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionRecyclerViewActivityTest.java @@ -0,0 +1,45 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivity.java new file mode 100644 index 00000000..f2b9c5cb --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivity.java @@ -0,0 +1,95 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivityTest.java new file mode 100644 index 00000000..3773b2b0 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionScrollViewActivityTest.java @@ -0,0 +1,59 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivity.java new file mode 100644 index 00000000..023905b1 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivity.java @@ -0,0 +1,97 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivityTest.java new file mode 100644 index 00000000..d1f24e4f --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/TouchInterceptionWebViewActivityTest.java @@ -0,0 +1,44 @@ +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(); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/UiTestUtils.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/UiTestUtils.java new file mode 100644 index 00000000..43ebbf84 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/UiTestUtils.java @@ -0,0 +1,124 @@ +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)); + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2Activity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2Activity.java new file mode 100644 index 00000000..ecde2777 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2Activity.java @@ -0,0 +1,291 @@ +/* + * 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]; + } + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ActivityTest.java new file mode 100644 index 00000000..e1dc35a6 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ActivityTest.java @@ -0,0 +1,53 @@ +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(); + } + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2GridViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2GridViewFragment.java new file mode 100644 index 00000000..401752a9 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2GridViewFragment.java @@ -0,0 +1,44 @@ +/* + * 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; + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ListViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ListViewFragment.java new file mode 100644 index 00000000..33daf0c8 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ListViewFragment.java @@ -0,0 +1,44 @@ +/* + * 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; + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2RecyclerViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2RecyclerViewFragment.java new file mode 100644 index 00000000..1e20ed93 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2RecyclerViewFragment.java @@ -0,0 +1,47 @@ +/* + * 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; + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ScrollViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ScrollViewFragment.java new file mode 100644 index 00000000..286e96e5 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2ScrollViewFragment.java @@ -0,0 +1,42 @@ +/* + * 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; + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2WebViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2WebViewFragment.java new file mode 100644 index 00000000..582873f3 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTab2WebViewFragment.java @@ -0,0 +1,43 @@ +/* + * 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; + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivity.java new file mode 100644 index 00000000..39153790 --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivity.java @@ -0,0 +1,288 @@ +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]; + } + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivityTest.java new file mode 100644 index 00000000..6125d6ff --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabActivityTest.java @@ -0,0 +1,53 @@ +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(); + } + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabListViewFragment.java similarity index 88% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java rename to library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabListViewFragment.java index e1363af7..447e54b2 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabListViewFragment.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.github.ksoichiro.android.observablescrollview.samples; +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; @@ -26,7 +27,7 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; -public class ViewPagerTabListViewFragment extends BaseFragment { +public class ViewPagerTabListViewFragment extends Fragment { public static final String ARG_INITIAL_POSITION = "ARG_INITIAL_POSITION"; @@ -36,7 +37,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa Activity parentActivity = getActivity(); final ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); - setDummyDataWithHeader(listView, inflater.inflate(R.layout.padding, null)); + UiTestUtils.setDummyDataWithHeader(getActivity(), listView, inflater.inflate(R.layout.padding, null)); if (parentActivity instanceof ObservableScrollViewCallbacks) { // Scroll to the specified position after layout diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabRecyclerViewFragment.java similarity index 90% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java rename to library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabRecyclerViewFragment.java index ea9a8ce2..f91988e8 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabRecyclerViewFragment.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.github.ksoichiro.android.observablescrollview.samples; +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; @@ -27,7 +28,7 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; -public class ViewPagerTabRecyclerViewFragment extends BaseFragment { +public class ViewPagerTabRecyclerViewFragment extends Fragment { public static final String ARG_INITIAL_POSITION = "ARG_INITIAL_POSITION"; @@ -40,7 +41,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa recyclerView.setLayoutManager(new LinearLayoutManager(parentActivity)); recyclerView.setHasFixedSize(false); View headerView = LayoutInflater.from(parentActivity).inflate(R.layout.padding, null); - setDummyDataWithHeader(recyclerView, headerView); + UiTestUtils.setDummyDataWithHeader(getActivity(), recyclerView, headerView); if (parentActivity instanceof ObservableScrollViewCallbacks) { // Scroll to the specified offset after layout diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabScrollViewFragment.java similarity index 92% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java rename to library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabScrollViewFragment.java index 28347a33..16c62658 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/ViewPagerTabScrollViewFragment.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.github.ksoichiro.android.observablescrollview.samples; +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; @@ -26,7 +27,7 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollUtils; -public class ViewPagerTabScrollViewFragment extends BaseFragment { +public class ViewPagerTabScrollViewFragment extends Fragment { public static final String ARG_SCROLL_Y = "ARG_SCROLL_Y"; diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivity.java new file mode 100644 index 00000000..dd13963a --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivity.java @@ -0,0 +1,33 @@ +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) { + } +} diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivityTest.java new file mode 100644 index 00000000..96c9e8bb --- /dev/null +++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/WebViewActivityTest.java @@ -0,0 +1,155 @@ +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]); + } +} diff --git a/library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java b/library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java new file mode 100644 index 00000000..06f60f7a --- /dev/null +++ b/library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java @@ -0,0 +1,321 @@ +/* + * 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; + } + } + } + } + +} diff --git a/observablescrollview-samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java b/library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java rename to library/src/androidTest/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java diff --git a/observablescrollview-samples/src/main/res/color/tab_text_color.xml b/library/src/androidTest/res/color/tab_text_color.xml similarity index 100% rename from observablescrollview-samples/src/main/res/color/tab_text_color.xml rename to library/src/androidTest/res/color/tab_text_color.xml diff --git a/library/src/androidTest/res/layout/activity_gridview.xml b/library/src/androidTest/res/layout/activity_gridview.xml new file mode 100644 index 00000000..d5ddb31d --- /dev/null +++ b/library/src/androidTest/res/layout/activity_gridview.xml @@ -0,0 +1,20 @@ + + diff --git a/library/src/androidTest/res/layout/activity_listview.xml b/library/src/androidTest/res/layout/activity_listview.xml new file mode 100644 index 00000000..18dd210b --- /dev/null +++ b/library/src/androidTest/res/layout/activity_listview.xml @@ -0,0 +1,19 @@ + + diff --git a/library/src/androidTest/res/layout/activity_recyclerview.xml b/library/src/androidTest/res/layout/activity_recyclerview.xml new file mode 100644 index 00000000..dce42c47 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_recyclerview.xml @@ -0,0 +1,19 @@ + + diff --git a/library/src/androidTest/res/layout/activity_scrollview.xml b/library/src/androidTest/res/layout/activity_scrollview.xml new file mode 100644 index 00000000..972537fe --- /dev/null +++ b/library/src/androidTest/res/layout/activity_scrollview.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/library/src/androidTest/res/layout/activity_touchinterception_gridview.xml b/library/src/androidTest/res/layout/activity_touchinterception_gridview.xml new file mode 100644 index 00000000..043e6175 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_touchinterception_gridview.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + diff --git a/library/src/androidTest/res/layout/activity_touchinterception_listview.xml b/library/src/androidTest/res/layout/activity_touchinterception_listview.xml new file mode 100644 index 00000000..05c8e299 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_touchinterception_listview.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + diff --git a/library/src/androidTest/res/layout/activity_touchinterception_recyclerview.xml b/library/src/androidTest/res/layout/activity_touchinterception_recyclerview.xml new file mode 100644 index 00000000..c236b3d1 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_touchinterception_recyclerview.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + diff --git a/library/src/androidTest/res/layout/activity_touchinterception_scrollview.xml b/library/src/androidTest/res/layout/activity_touchinterception_scrollview.xml new file mode 100644 index 00000000..de7cf631 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_touchinterception_scrollview.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/library/src/androidTest/res/layout/activity_touchinterception_webview.xml b/library/src/androidTest/res/layout/activity_touchinterception_webview.xml new file mode 100644 index 00000000..8e834969 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_touchinterception_webview.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/activity_viewpagertab.xml b/library/src/androidTest/res/layout/activity_viewpagertab.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_viewpagertab.xml rename to library/src/androidTest/res/layout/activity_viewpagertab.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_viewpagertab2.xml b/library/src/androidTest/res/layout/activity_viewpagertab2.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_viewpagertab2.xml rename to library/src/androidTest/res/layout/activity_viewpagertab2.xml diff --git a/library/src/androidTest/res/layout/activity_webview.xml b/library/src/androidTest/res/layout/activity_webview.xml new file mode 100644 index 00000000..75d62617 --- /dev/null +++ b/library/src/androidTest/res/layout/activity_webview.xml @@ -0,0 +1,19 @@ + + diff --git a/observablescrollview-samples/src/main/res/layout/fragment_gridview.xml b/library/src/androidTest/res/layout/fragment_gridview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_gridview.xml rename to library/src/androidTest/res/layout/fragment_gridview.xml diff --git a/observablescrollview-samples/src/main/res/layout/fragment_listview.xml b/library/src/androidTest/res/layout/fragment_listview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_listview.xml rename to library/src/androidTest/res/layout/fragment_listview.xml diff --git a/observablescrollview-samples/src/main/res/layout/fragment_recyclerview.xml b/library/src/androidTest/res/layout/fragment_recyclerview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_recyclerview.xml rename to library/src/androidTest/res/layout/fragment_recyclerview.xml diff --git a/library/src/androidTest/res/layout/fragment_scrollview.xml b/library/src/androidTest/res/layout/fragment_scrollview.xml new file mode 100644 index 00000000..0be95c85 --- /dev/null +++ b/library/src/androidTest/res/layout/fragment_scrollview.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/library/src/androidTest/res/layout/fragment_scrollview_noheader.xml b/library/src/androidTest/res/layout/fragment_scrollview_noheader.xml new file mode 100644 index 00000000..2ac408dd --- /dev/null +++ b/library/src/androidTest/res/layout/fragment_scrollview_noheader.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/fragment_webview.xml b/library/src/androidTest/res/layout/fragment_webview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_webview.xml rename to library/src/androidTest/res/layout/fragment_webview.xml diff --git a/observablescrollview-samples/src/main/res/layout/padding.xml b/library/src/androidTest/res/layout/padding.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/padding.xml rename to library/src/androidTest/res/layout/padding.xml diff --git a/observablescrollview-samples/src/main/res/layout/tab_indicator.xml b/library/src/androidTest/res/layout/tab_indicator.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/tab_indicator.xml rename to library/src/androidTest/res/layout/tab_indicator.xml diff --git a/library/src/androidTest/res/values/colors.xml b/library/src/androidTest/res/values/colors.xml new file mode 100644 index 00000000..221c75c7 --- /dev/null +++ b/library/src/androidTest/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + #009688 + #00796b + #eeff41 + + diff --git a/library/src/androidTest/res/values/dimens.xml b/library/src/androidTest/res/values/dimens.xml new file mode 100644 index 00000000..592125de --- /dev/null +++ b/library/src/androidTest/res/values/dimens.xml @@ -0,0 +1,22 @@ + + + 48dp + 72dp + 56dp + 16dp + 240dp + diff --git a/library/src/androidTest/res/values/strings.xml b/library/src/androidTest/res/values/strings.xml new file mode 100644 index 00000000..6f8f6062 --- /dev/null +++ b/library/src/androidTest/res/values/strings.xml @@ -0,0 +1,23 @@ + + + + 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. + + diff --git a/observablescrollview-samples/src/main/res/values/styles.xml b/library/src/androidTest/res/values/styles.xml similarity index 100% rename from observablescrollview-samples/src/main/res/values/styles.xml rename to library/src/androidTest/res/values/styles.xml diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java similarity index 82% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java index f0057410..f4699437 100644 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/CacheFragmentStatePagerAdapter.java @@ -26,10 +26,10 @@ /** * FragmentStatePagerAdapter that caches each pages. - * FragmentStatePagerAdapter is also originally caches 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. + * 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 { @@ -43,7 +43,7 @@ public abstract class CacheFragmentStatePagerAdapter extends FragmentStatePagerA public CacheFragmentStatePagerAdapter(FragmentManager fm) { super(fm); - mPages = new SparseArray(); + mPages = new SparseArray<>(); mFm = fm; } @@ -83,15 +83,15 @@ public void restoreState(Parcelable state, ClassLoader loader) { /** * Get a new Fragment instance. - * Each fragments are automatically cached in this method, + *

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. - * + * you should override {@link #createItem(int)} instead.

+ *

* {@inheritDoc} * - * @param position position of the item in the adapter - * @return fragment instance + * @param position Position of the item in the adapter. + * @return Fragment instance. */ @Override public Fragment getItem(int position) { @@ -112,8 +112,8 @@ public void destroyItem(ViewGroup container, int position, Object object) { /** * Get the item at the specified position in the adapter. * - * @param position position of the item in the adapter - * @return fragment instance + * @param position Position of the item in the adapter. + * @return Fragment instance. */ public Fragment getItemAt(int position) { return mPages.get(position); @@ -123,16 +123,16 @@ public Fragment getItemAt(int 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 + * @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 + * @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; @@ -141,8 +141,8 @@ protected String createCacheIndex(int 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 + * @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; diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java new file mode 100644 index 00000000..8a90016d --- /dev/null +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java @@ -0,0 +1,983 @@ +/* + * 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(); + } + } +} diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java new file mode 100644 index 00000000..3820dadf --- /dev/null +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java @@ -0,0 +1,457 @@ +/* + * 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]; + } + }; + } +} diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java new file mode 100644 index 00000000..2f84828c --- /dev/null +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java @@ -0,0 +1,516 @@ +/* + * 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]; + } + }; + } +} diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java new file mode 100644 index 00000000..d841f0b0 --- /dev/null +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java @@ -0,0 +1,321 @@ +/* + * 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]; + } + }; + } +} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java similarity index 62% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java index 4f7e9c62..003c806c 100644 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollViewCallbacks.java @@ -22,25 +22,25 @@ 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 + *

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. + * 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 + * @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. */ - public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging); + void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging); /** * Called when the down motion event occurred. */ - public void onDownMotionEvent(); + void onDownMotionEvent(); /** * Called when the dragging ended or canceled. * - * @param scrollState state to indicate the scroll direction + * @param scrollState State to indicate the scroll direction. */ - public void onUpOrCancelMotionEvent(ScrollState scrollState); + void onUpOrCancelMotionEvent(ScrollState scrollState); } diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java new file mode 100644 index 00000000..f84dceed --- /dev/null +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java @@ -0,0 +1,319 @@ +/* + * 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]; + } + }; + } +} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollState.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollState.java similarity index 100% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollState.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollState.java diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java similarity index 75% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java index daec7556..cbe5e9c5 100644 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ScrollUtils.java @@ -30,13 +30,13 @@ 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?"). + *

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 + * @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)); @@ -44,11 +44,11 @@ public static float getFloat(final float value, final float minValue, final floa /** * Create a color integer value with specified alpha. - * This may be useful to change alpha value of background color. + *

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 + * @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; @@ -58,15 +58,16 @@ public static int getColorWithAlpha(float alpha, int baseColor) { /** * 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. + *

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 + * @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) { @@ -81,13 +82,13 @@ public void onGlobalLayout() { /** * Mix two colors. - * {@code toColor} will be {@code toAlpha/1} percent, - * and {@code fromColor} will be {@code (1-toAlpha)/1} percent. + *

{@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). + * @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); @@ -102,8 +103,8 @@ public static int mixColors(int fromColor, int toColor, float toAlpha) { /** * Convert RGB color to CMYK color. * - * @param rgbColor target color - * @return CMYK array + * @param rgbColor Target color. + * @return CMYK array. */ public static float[] cmykFromRgb(int rgbColor) { int red = (0xff0000 & rgbColor) >> 16; @@ -124,9 +125,9 @@ public static float[] cmykFromRgb(int rgbColor) { /** * Convert CYMK color to RGB color. - * This method doesn't check f cmyk is not null or have 4 elements in array. + * 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, + * @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). */ diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java similarity index 55% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java index 1043b4bc..47f10283 100644 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/Scrollable.java @@ -19,38 +19,64 @@ import android.view.ViewGroup; /** - * Provides common API for observable and scrollable widgets. + * Interface for providing common API for observable and scrollable widgets. */ public interface Scrollable { /** - * Sets a callback listener. + * Set a callback listener.
+ * Developers should use {@link #addScrollViewCallbacks(ObservableScrollViewCallbacks)} + * and {@link #removeScrollViewCallbacks(ObservableScrollViewCallbacks)}. * - * @param listener listener to set + * @param listener Listener to set. */ + @Deprecated void setScrollViewCallbacks(ObservableScrollViewCallbacks listener); /** - * Scrolls vertically to the absolute Y. + * 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 + * @param y Vertical position to scroll to. */ void scrollVerticallyTo(int y); /** - * Returns the current Y of the scrollable view. + * Return the current Y of the scrollable view. * - * @return current Y pixel + * @return Current Y pixel. */ int getCurrentScrollY(); /** - * Sets a touch motion event delegation ViewGroup. + * 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 + * @param viewGroup ViewGroup object to dispatch motion events. */ void setTouchInterceptionViewGroup(ViewGroup viewGroup); } diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java similarity index 93% rename from observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java rename to library/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java index 6a5ed557..a4ecf1d7 100644 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java +++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/TouchInterceptionFrameLayout.java @@ -40,36 +40,36 @@ public class TouchInterceptionFrameLayout extends FrameLayout { */ public interface TouchInterceptionListener { /** - * Determines whether the layout should intercept this event. + * 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 + * @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 + * @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 + * @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 + * @param ev Motion event. */ void onUpOrCancelMotionEvent(MotionEvent ev); } @@ -247,8 +247,8 @@ private MotionEvent obtainMotionEvent(MotionEvent base, int action) { * 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 + * @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) { diff --git a/observablescrollview/.gitignore b/observablescrollview/.gitignore deleted file mode 100644 index 796b96d1..00000000 --- a/observablescrollview/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/observablescrollview/build.gradle b/observablescrollview/build.gradle deleted file mode 100644 index 92ac1196..00000000 --- a/observablescrollview/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' - } -} - -apply plugin: 'com.android.library' - -repositories { - mavenCentral() -} - -dependencies { - compile 'com.android.support:recyclerview-v7:21.0.0' -} - -android { - compileSdkVersion 21 - buildToolsVersion "21.1.1" - - defaultConfig { - minSdkVersion 9 - } -} - -// This is from 'https://github.com/chrisbanes/gradle-mvn-push' -apply from: 'gradle-mvn-push.gradle' diff --git a/observablescrollview/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/ApplicationTest.java b/observablescrollview/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/ApplicationTest.java deleted file mode 100644 index ff68ca2d..00000000 --- a/observablescrollview/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.ksoichiro.android.observablescrollview; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java b/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java deleted file mode 100644 index 0f36183d..00000000 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableGridView.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * 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.GridView; - -/** - * 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 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 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 (mCallbacks != null) { - 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; - mCallbacks.onDownMotionEvent(); - break; - } - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mCallbacks != null) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIntercepted = false; - mDragging = false; - mCallbacks.onUpOrCancelMotionEvent(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 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 setTouchInterceptionViewGroup(ViewGroup viewGroup) { - mTouchInterceptionViewGroup = viewGroup; - } - - @Override - public void scrollVerticallyTo(int y) { - scrollTo(0, y); - } - - @Override - public int getCurrentScrollY() { - return mScrollY; - } - - private void init() { - mChildrenHeights = new SparseIntArray(); - super.setOnScrollListener(mScrollListener); - } - - private void onScrollChanged() { - if (mCallbacks != null) { - 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); - } - } - } - 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(); - } - if (mPrevFirstVisibleChildHeight < 0) { - mPrevFirstVisibleChildHeight = 0; - } - mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop(); - mPrevFirstVisiblePosition = firstVisiblePosition; - - mCallbacks.onScrollChanged(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; - } - } - } - } - - static class SavedState extends BaseSavedState { - int prevFirstVisiblePosition; - int prevFirstVisibleChildHeight = -1; - int prevScrolledChildrenHeight; - int prevScrollY; - int scrollY; - SparseIntArray childrenHeights; - - /** - * Called by onSaveInstanceState. - */ - private 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]; - } - }; - } -} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java b/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java deleted file mode 100644 index d9698280..00000000 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableListView.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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; - -/** - * 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 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 (mCallbacks != null) { - 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; - mCallbacks.onDownMotionEvent(); - break; - } - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mCallbacks != null) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIntercepted = false; - mDragging = false; - mCallbacks.onUpOrCancelMotionEvent(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 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 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 (mCallbacks != null) { - 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(); - } - if (mPrevFirstVisibleChildHeight < 0) { - mPrevFirstVisibleChildHeight = 0; - } - mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop(); - mPrevFirstVisiblePosition = firstVisiblePosition; - - mCallbacks.onScrollChanged(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; - } - } - } - } - - static class SavedState extends BaseSavedState { - int prevFirstVisiblePosition; - int prevFirstVisibleChildHeight = -1; - int prevScrolledChildrenHeight; - int prevScrollY; - int scrollY; - SparseIntArray childrenHeights; - - /** - * Called by onSaveInstanceState. - */ - private 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]; - } - }; - } -} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java b/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java deleted file mode 100644 index 76ac3618..00000000 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableRecyclerView.java +++ /dev/null @@ -1,422 +0,0 @@ -/* - * 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; - -/** - * 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 { - - // 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 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 (mCallbacks != null) { - if (getChildCount() > 0) { - int firstVisiblePosition = getChildPosition(getChildAt(0)); - int lastVisiblePosition = getChildPosition(getChildAt(getChildCount() - 1)); - for (int i = firstVisiblePosition, j = 0; i <= lastVisiblePosition; 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(); - } - if (mPrevFirstVisibleChildHeight < 0) { - mPrevFirstVisibleChildHeight = 0; - } - mScrollY = mPrevScrolledChildrenHeight - firstVisibleChild.getTop(); - mPrevFirstVisiblePosition = firstVisiblePosition; - - mCallbacks.onScrollChanged(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 (mCallbacks != null) { - 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; - mCallbacks.onDownMotionEvent(); - break; - } - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mCallbacks != null) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIntercepted = false; - mDragging = false; - mCallbacks.onUpOrCancelMotionEvent(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 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; - } - - private void init() { - mChildrenHeights = new SparseIntArray(); - } - - /** - * 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. - */ - private 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]; - } - }; - } -} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java b/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java deleted file mode 100644 index a76cd7f8..00000000 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableScrollView.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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; - -/** - * 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 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 (mCallbacks != null) { - mScrollY = t; - - mCallbacks.onScrollChanged(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 (mCallbacks != null) { - 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; - mCallbacks.onDownMotionEvent(); - break; - } - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mCallbacks != null) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIntercepted = false; - mDragging = false; - mCallbacks.onUpOrCancelMotionEvent(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 setTouchInterceptionViewGroup(ViewGroup viewGroup) { - mTouchInterceptionViewGroup = viewGroup; - } - - @Override - public void scrollVerticallyTo(int y) { - scrollTo(0, y); - } - - @Override - public int getCurrentScrollY() { - return mScrollY; - } - - static class SavedState extends BaseSavedState { - int prevScrollY; - int scrollY; - - /** - * Called by onSaveInstanceState. - */ - private 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]; - } - }; - } -} diff --git a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java b/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java deleted file mode 100644 index 0167e704..00000000 --- a/observablescrollview/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableWebView.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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; - -/** - * 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 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 (mCallbacks != null) { - mScrollY = t; - - mCallbacks.onScrollChanged(t, 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 (mCallbacks != null) { - 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; - mCallbacks.onDownMotionEvent(); - break; - } - } - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mCallbacks != null) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIntercepted = false; - mDragging = false; - mCallbacks.onUpOrCancelMotionEvent(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 setTouchInterceptionViewGroup(ViewGroup viewGroup) { - mTouchInterceptionViewGroup = viewGroup; - } - - @Override - public void scrollVerticallyTo(int y) { - scrollTo(0, y); - } - - @Override - public int getCurrentScrollY() { - return mScrollY; - } - - static class SavedState extends BaseSavedState { - int prevScrollY; - int scrollY; - - /** - * Called by onSaveInstanceState. - */ - private 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]; - } - }; - } -} diff --git a/samples/.gitignore b/samples/.gitignore new file mode 100644 index 00000000..915c5f4b --- /dev/null +++ b/samples/.gitignore @@ -0,0 +1,2 @@ +/build +/src/version/ diff --git a/observablescrollview-samples/src/main/AndroidManifest.xml b/samples/AndroidManifest.xml similarity index 66% rename from observablescrollview-samples/src/main/AndroidManifest.xml rename to samples/AndroidManifest.xml index b5818066..fe642f43 100644 --- a/observablescrollview-samples/src/main/AndroidManifest.xml +++ b/samples/AndroidManifest.xml @@ -15,9 +15,14 @@ --> + package="com.github.ksoichiro.android.observablescrollview.samples" + android:versionCode="5" + android:versionName="1.3.0"> - + + android:label="@string/title_activity_main" + android:theme="@style/AppTheme.Toolbar"> @@ -42,7 +48,7 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + - + - + - + - + - + - + - + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000..885c3f91 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,33 @@ +# 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 +``` \ No newline at end of file diff --git a/observablescrollview-samples/src/main/assets/handletouch.html b/samples/assets/handletouch.html similarity index 100% rename from observablescrollview-samples/src/main/assets/handletouch.html rename to samples/assets/handletouch.html diff --git a/samples/assets/lipsum.html b/samples/assets/lipsum.html new file mode 100644 index 00000000..7d71889e --- /dev/null +++ b/samples/assets/lipsum.html @@ -0,0 +1,17 @@ + + + + + +

+

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.

+
+ + \ No newline at end of file diff --git a/observablescrollview-samples/build.gradle b/samples/build.gradle similarity index 71% rename from observablescrollview-samples/build.gradle rename to samples/build.gradle index cafea876..571807b4 100644 --- a/observablescrollview-samples/build.gradle +++ b/samples/build.gradle @@ -1,35 +1,17 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' - } -} - -// Get git commit hash for naming the APK file if 'git' is available -try { - project.ext.gitHash = "git rev-parse --short HEAD".execute().text.trim() -} catch (all) { - project.ext.gitHash = "unknown" -} - apply plugin: 'com.android.application' -repositories { - mavenCentral() - - // for using SNAPSHOT - //maven { - // url uri('https://oss.sonatype.org/content/repositories/snapshots/') - //} -} +// for using SNAPSHOT +//repositories { +// maven { +// url uri('https://oss.sonatype.org/content/repositories/snapshots/') +// } +//} dependencies { - compile 'com.android.support:appcompat-v7:21.0.2' + 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(':observablescrollview') + debugCompile project(':library') // Release build uses the synced latest version releaseCompile "com.github.ksoichiro:android-observablescrollview:${SYNCED_VERSION_NAME}" @@ -37,17 +19,17 @@ dependencies { //compile "com.github.ksoichiro:android-observablescrollview:$VERSION_NAME" } +apply from: "${rootDir}/gradle/version.gradle" +project.ext.versionInfo.releaseVersionName = SYNCED_VERSION_NAME + android { - compileSdkVersion 21 - buildToolsVersion "21.1.1" + compileSdkVersion 23 + buildToolsVersion "23.0.2" defaultConfig { applicationId "com.github.ksoichiro.android.observablescrollview.samples" - minSdkVersion 9 - targetSdkVersion 21 - versionCode 4 - versionName "1.2.1" - buildConfigField "String", "GIT_HASH", "\"${project.ext.gitHash}\"" + versionCode 5 + versionName "1.3.0" } productFlavors { @@ -83,12 +65,9 @@ android { debug { applicationIdSuffix ".debug" versionNameSuffix "-debug" - buildConfigField "String", "LIB_VERSION", "\"${project.ext.gitHash}\"" } release { - buildConfigField "String", "LIB_VERSION", "\"${VERSION_NAME}\"" - minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -99,6 +78,14 @@ android { } } + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + } + lintOptions { abortOnError false } @@ -107,7 +94,7 @@ android { 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.gitHash}.apk") + 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) } diff --git a/observablescrollview-samples/ic_launcher-web.png b/samples/ic_launcher-web.png similarity index 100% rename from observablescrollview-samples/ic_launcher-web.png rename to samples/ic_launcher-web.png diff --git a/observablescrollview-samples/demo1.gif b/samples/images/demo1.gif similarity index 100% rename from observablescrollview-samples/demo1.gif rename to samples/images/demo1.gif diff --git a/observablescrollview-samples/demo10.gif b/samples/images/demo10.gif similarity index 100% rename from observablescrollview-samples/demo10.gif rename to samples/images/demo10.gif diff --git a/observablescrollview-samples/demo11.gif b/samples/images/demo11.gif similarity index 100% rename from observablescrollview-samples/demo11.gif rename to samples/images/demo11.gif diff --git a/observablescrollview-samples/demo12.gif b/samples/images/demo12.gif similarity index 100% rename from observablescrollview-samples/demo12.gif rename to samples/images/demo12.gif diff --git a/observablescrollview-samples/demo13.gif b/samples/images/demo13.gif similarity index 100% rename from observablescrollview-samples/demo13.gif rename to samples/images/demo13.gif diff --git a/observablescrollview-samples/demo2.gif b/samples/images/demo2.gif similarity index 100% rename from observablescrollview-samples/demo2.gif rename to samples/images/demo2.gif diff --git a/observablescrollview-samples/demo3.gif b/samples/images/demo3.gif similarity index 100% rename from observablescrollview-samples/demo3.gif rename to samples/images/demo3.gif diff --git a/observablescrollview-samples/demo4.gif b/samples/images/demo4.gif similarity index 100% rename from observablescrollview-samples/demo4.gif rename to samples/images/demo4.gif diff --git a/observablescrollview-samples/demo5.gif b/samples/images/demo5.gif similarity index 100% rename from observablescrollview-samples/demo5.gif rename to samples/images/demo5.gif diff --git a/observablescrollview-samples/demo6.gif b/samples/images/demo6.gif similarity index 100% rename from observablescrollview-samples/demo6.gif rename to samples/images/demo6.gif diff --git a/observablescrollview-samples/demo7.gif b/samples/images/demo7.gif similarity index 100% rename from observablescrollview-samples/demo7.gif rename to samples/images/demo7.gif diff --git a/observablescrollview-samples/demo8.gif b/samples/images/demo8.gif similarity index 100% rename from observablescrollview-samples/demo8.gif rename to samples/images/demo8.gif diff --git a/observablescrollview-samples/demo9.gif b/samples/images/demo9.gif similarity index 100% rename from observablescrollview-samples/demo9.gif rename to samples/images/demo9.gif diff --git a/observablescrollview-samples/proguard-rules.pro b/samples/proguard-rules.pro similarity index 100% rename from observablescrollview-samples/proguard-rules.pro rename to samples/proguard-rules.pro diff --git a/samples/res/color/tab_text_color.xml b/samples/res/color/tab_text_color.xml new file mode 100644 index 00000000..48f21f86 --- /dev/null +++ b/samples/res/color/tab_text_color.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/observablescrollview-samples/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/res/drawable-xhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/main/res/drawable-xhdpi/ic_launcher.png rename to samples/res/drawable-xhdpi/ic_launcher.png diff --git a/observablescrollview-samples/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/res/drawable-xxhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/main/res/drawable-xxhdpi/ic_launcher.png rename to samples/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/res/drawable-xxxhdpi/ic_action_info.png b/samples/res/drawable-xxxhdpi/ic_action_info.png new file mode 100644 index 00000000..fed22b87 Binary files /dev/null and b/samples/res/drawable-xxxhdpi/ic_action_info.png differ diff --git a/observablescrollview-samples/src/main/res/drawable-xxxhdpi/ic_launcher.png b/samples/res/drawable-xxxhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/main/res/drawable-xxxhdpi/ic_launcher.png rename to samples/res/drawable-xxxhdpi/ic_launcher.png diff --git a/observablescrollview-samples/src/main/res/drawable/example.jpeg b/samples/res/drawable/example.jpeg similarity index 100% rename from observablescrollview-samples/src/main/res/drawable/example.jpeg rename to samples/res/drawable/example.jpeg diff --git a/observablescrollview-samples/src/main/res/drawable/gradient_header_background.xml b/samples/res/drawable/gradient_header_background.xml similarity index 100% rename from observablescrollview-samples/src/main/res/drawable/gradient_header_background.xml rename to samples/res/drawable/gradient_header_background.xml diff --git a/observablescrollview-samples/src/main/res/drawable/sliding_header_overlay.xml b/samples/res/drawable/sliding_header_overlay.xml similarity index 100% rename from observablescrollview-samples/src/main/res/drawable/sliding_header_overlay.xml rename to samples/res/drawable/sliding_header_overlay.xml diff --git a/observablescrollview-samples/src/main/res/layout-v11/tab_indicator.xml b/samples/res/layout-v11/tab_indicator.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout-v11/tab_indicator.xml rename to samples/res/layout-v11/tab_indicator.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_about.xml b/samples/res/layout/activity_about.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_about.xml rename to samples/res/layout/activity_about.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolgridview.xml b/samples/res/layout/activity_actionbarcontrolgridview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolgridview.xml rename to samples/res/layout/activity_actionbarcontrolgridview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_actionbarcontrollistview.xml b/samples/res/layout/activity_actionbarcontrollistview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_actionbarcontrollistview.xml rename to samples/res/layout/activity_actionbarcontrollistview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolrecyclerview.xml b/samples/res/layout/activity_actionbarcontrolrecyclerview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolrecyclerview.xml rename to samples/res/layout/activity_actionbarcontrolrecyclerview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolscrollview.xml b/samples/res/layout/activity_actionbarcontrolscrollview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolscrollview.xml rename to samples/res/layout/activity_actionbarcontrolscrollview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolwebview.xml b/samples/res/layout/activity_actionbarcontrolwebview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_actionbarcontrolwebview.xml rename to samples/res/layout/activity_actionbarcontrolwebview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_fillgap3listview.xml b/samples/res/layout/activity_fillgap3listview.xml similarity index 98% rename from observablescrollview-samples/src/main/res/layout/activity_fillgap3listview.xml rename to samples/res/layout/activity_fillgap3listview.xml index 8f3ef98c..5857dcc3 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_fillgap3listview.xml +++ b/samples/res/layout/activity_fillgap3listview.xml @@ -17,8 +17,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:clipChildren="false" - android:orientation="vertical"> + android:clipChildren="false"> + android:clipChildren="false"> + android:clipChildren="false"> + android:clipChildren="false"> - - - - + android:layout_height="@dimen/flexible_space_image_height" + android:scaleType="centerCrop" + android:src="@drawable/example" /> - - diff --git a/observablescrollview-samples/src/main/res/layout/activity_fillgaprecyclerview.xml b/samples/res/layout/activity_fillgaprecyclerview.xml similarity index 73% rename from observablescrollview-samples/src/main/res/layout/activity_fillgaprecyclerview.xml rename to samples/res/layout/activity_fillgaprecyclerview.xml index 850b4e77..a883e74f 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_fillgaprecyclerview.xml +++ b/samples/res/layout/activity_fillgaprecyclerview.xml @@ -14,24 +14,16 @@ limitations under the License. --> + android:clipChildren="false"> - - - - + android:layout_height="@dimen/flexible_space_image_height" + android:scaleType="centerCrop" + android:src="@drawable/example" /> - - diff --git a/observablescrollview-samples/src/main/res/layout/activity_fillgapscrollview.xml b/samples/res/layout/activity_fillgapscrollview.xml similarity index 75% rename from observablescrollview-samples/src/main/res/layout/activity_fillgapscrollview.xml rename to samples/res/layout/activity_fillgapscrollview.xml index 024456a8..e99d8d1d 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_fillgapscrollview.xml +++ b/samples/res/layout/activity_fillgapscrollview.xml @@ -14,29 +14,22 @@ limitations under the License. --> + android:clipChildren="false"> - - - - + android:layout_height="@dimen/flexible_space_image_height" + android:scaleType="centerCrop" + android:src="@drawable/example" /> @@ -45,7 +38,6 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/flexible_space_image_height" android:background="@android:color/white" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" @@ -86,13 +78,4 @@ android:textSize="20sp" /> - - diff --git a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacetoolbarscrollview.xml b/samples/res/layout/activity_flexiblespacetoolbarscrollview.xml similarity index 95% rename from observablescrollview-samples/src/main/res/layout/activity_flexiblespacetoolbarscrollview.xml rename to samples/res/layout/activity_flexiblespacetoolbarscrollview.xml index e4423f24..df9939f5 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacetoolbarscrollview.xml +++ b/samples/res/layout/activity_flexiblespacetoolbarscrollview.xml @@ -16,8 +16,7 @@ + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/res/layout/activity_flexiblespacewithimagegridview.xml b/samples/res/layout/activity_flexiblespacewithimagegridview.xml new file mode 100644 index 00000000..2178a1ba --- /dev/null +++ b/samples/res/layout/activity_flexiblespacewithimagegridview.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagelistview.xml b/samples/res/layout/activity_flexiblespacewithimagelistview.xml similarity index 84% rename from observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagelistview.xml rename to samples/res/layout/activity_flexiblespacewithimagelistview.xml index 19a73a72..ef804d31 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagelistview.xml +++ b/samples/res/layout/activity_flexiblespacewithimagelistview.xml @@ -16,8 +16,7 @@ + android:layout_height="match_parent"> - - + android:paddingEnd="@dimen/margin_standard" + android:paddingLeft="@dimen/margin_standard" + android:paddingStart="@dimen/margin_standard"> diff --git a/samples/res/layout/activity_flexiblespacewithimagerecyclerview.xml b/samples/res/layout/activity_flexiblespacewithimagerecyclerview.xml new file mode 100644 index 00000000..ae976366 --- /dev/null +++ b/samples/res/layout/activity_flexiblespacewithimagerecyclerview.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagescrollview.xml b/samples/res/layout/activity_flexiblespacewithimagescrollview.xml similarity index 89% rename from observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagescrollview.xml rename to samples/res/layout/activity_flexiblespacewithimagescrollview.xml index cc599387..a1cc5c71 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_flexiblespacewithimagescrollview.xml +++ b/samples/res/layout/activity_flexiblespacewithimagescrollview.xml @@ -16,8 +16,7 @@ + android:layout_height="match_parent"> - - diff --git a/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab.xml b/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab.xml new file mode 100644 index 00000000..c9c9af33 --- /dev/null +++ b/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab2.xml b/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab2.xml new file mode 100644 index 00000000..eb07dd05 --- /dev/null +++ b/samples/res/layout/activity_flexiblespacewithimagewithviewpagertab2.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/observablescrollview-samples/src/main/res/layout/activity_fragmentactionbarcontrol.xml b/samples/res/layout/activity_fragmentactionbarcontrol.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_fragmentactionbarcontrol.xml rename to samples/res/layout/activity_fragmentactionbarcontrol.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_fragmenttransition.xml b/samples/res/layout/activity_fragmenttransition.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_fragmenttransition.xml rename to samples/res/layout/activity_fragmenttransition.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_handletouchgridview.xml b/samples/res/layout/activity_handletouchgridview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_handletouchgridview.xml rename to samples/res/layout/activity_handletouchgridview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_handletouchlistview.xml b/samples/res/layout/activity_handletouchlistview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_handletouchlistview.xml rename to samples/res/layout/activity_handletouchlistview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_handletouchrecyclerview.xml b/samples/res/layout/activity_handletouchrecyclerview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_handletouchrecyclerview.xml rename to samples/res/layout/activity_handletouchrecyclerview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_handletouchscrollview.xml b/samples/res/layout/activity_handletouchscrollview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_handletouchscrollview.xml rename to samples/res/layout/activity_handletouchscrollview.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_handletouchwebview.xml b/samples/res/layout/activity_handletouchwebview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_handletouchwebview.xml rename to samples/res/layout/activity_handletouchwebview.xml diff --git a/samples/res/layout/activity_main.xml b/samples/res/layout/activity_main.xml new file mode 100644 index 00000000..9725c36d --- /dev/null +++ b/samples/res/layout/activity_main.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/res/layout/activity_parallaxtoolbargridview.xml b/samples/res/layout/activity_parallaxtoolbargridview.xml new file mode 100644 index 00000000..0d9cce6d --- /dev/null +++ b/samples/res/layout/activity_parallaxtoolbargridview.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/samples/res/layout/activity_parallaxtoolbarlistview.xml b/samples/res/layout/activity_parallaxtoolbarlistview.xml new file mode 100644 index 00000000..695da3d8 --- /dev/null +++ b/samples/res/layout/activity_parallaxtoolbarlistview.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/activity_parallaxtoolbarscrollview.xml b/samples/res/layout/activity_parallaxtoolbarscrollview.xml similarity index 97% rename from observablescrollview-samples/src/main/res/layout/activity_parallaxtoolbarscrollview.xml rename to samples/res/layout/activity_parallaxtoolbarscrollview.xml index 6b49d682..65ba6ca2 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_parallaxtoolbarscrollview.xml +++ b/samples/res/layout/activity_parallaxtoolbarscrollview.xml @@ -16,8 +16,7 @@ + android:layout_height="match_parent"> + android:clipChildren="false"> + + + + + + + + + + + + + + + + diff --git a/samples/res/layout/activity_viewpagertab2.xml b/samples/res/layout/activity_viewpagertab2.xml new file mode 100644 index 00000000..510ed95c --- /dev/null +++ b/samples/res/layout/activity_viewpagertab2.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/observablescrollview-samples/src/main/res/layout/activity_viewpagertabfragment.xml b/samples/res/layout/activity_viewpagertabfragment.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/activity_viewpagertabfragment.xml rename to samples/res/layout/activity_viewpagertabfragment.xml diff --git a/observablescrollview-samples/src/main/res/layout/divider.xml b/samples/res/layout/divider.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/divider.xml rename to samples/res/layout/divider.xml diff --git a/observablescrollview-samples/src/main/res/layout/fragment_actionbarcontrollistview.xml b/samples/res/layout/fragment_actionbarcontrollistview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_actionbarcontrollistview.xml rename to samples/res/layout/fragment_actionbarcontrollistview.xml diff --git a/samples/res/layout/fragment_flexiblespacewithimagegridview.xml b/samples/res/layout/fragment_flexiblespacewithimagegridview.xml new file mode 100644 index 00000000..7d323b88 --- /dev/null +++ b/samples/res/layout/fragment_flexiblespacewithimagegridview.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/samples/res/layout/fragment_flexiblespacewithimagelistview.xml b/samples/res/layout/fragment_flexiblespacewithimagelistview.xml new file mode 100644 index 00000000..52d222c4 --- /dev/null +++ b/samples/res/layout/fragment_flexiblespacewithimagelistview.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/samples/res/layout/fragment_flexiblespacewithimagerecyclerview.xml b/samples/res/layout/fragment_flexiblespacewithimagerecyclerview.xml new file mode 100644 index 00000000..17499df7 --- /dev/null +++ b/samples/res/layout/fragment_flexiblespacewithimagerecyclerview.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/samples/res/layout/fragment_flexiblespacewithimagescrollview.xml b/samples/res/layout/fragment_flexiblespacewithimagescrollview.xml new file mode 100644 index 00000000..07ea3e4d --- /dev/null +++ b/samples/res/layout/fragment_flexiblespacewithimagescrollview.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/fragment_fragmenttransition_default.xml b/samples/res/layout/fragment_fragmenttransition_default.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_fragmenttransition_default.xml rename to samples/res/layout/fragment_fragmenttransition_default.xml diff --git a/observablescrollview-samples/src/main/res/layout/fragment_fragmenttransition_second.xml b/samples/res/layout/fragment_fragmenttransition_second.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_fragmenttransition_second.xml rename to samples/res/layout/fragment_fragmenttransition_second.xml diff --git a/observablescrollview-samples/src/main/res/layout/activity_main.xml b/samples/res/layout/fragment_gridview.xml similarity index 77% rename from observablescrollview-samples/src/main/res/layout/activity_main.xml rename to samples/res/layout/fragment_gridview.xml index f2dd4cbb..65241a4d 100644 --- a/observablescrollview-samples/src/main/res/layout/activity_main.xml +++ b/samples/res/layout/fragment_gridview.xml @@ -13,9 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - \ No newline at end of file + android:numColumns="2" /> diff --git a/samples/res/layout/fragment_listview.xml b/samples/res/layout/fragment_listview.xml new file mode 100644 index 00000000..08d5d41b --- /dev/null +++ b/samples/res/layout/fragment_listview.xml @@ -0,0 +1,19 @@ + + diff --git a/samples/res/layout/fragment_recyclerview.xml b/samples/res/layout/fragment_recyclerview.xml new file mode 100644 index 00000000..49f09590 --- /dev/null +++ b/samples/res/layout/fragment_recyclerview.xml @@ -0,0 +1,20 @@ + + diff --git a/observablescrollview-samples/src/main/res/layout/fragment_scrollview.xml b/samples/res/layout/fragment_scrollview.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_scrollview.xml rename to samples/res/layout/fragment_scrollview.xml diff --git a/observablescrollview-samples/src/main/res/layout/fragment_scrollview_noheader.xml b/samples/res/layout/fragment_scrollview_noheader.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_scrollview_noheader.xml rename to samples/res/layout/fragment_scrollview_noheader.xml diff --git a/samples/res/layout/fragment_scrollviewwithfab.xml b/samples/res/layout/fragment_scrollviewwithfab.xml new file mode 100644 index 00000000..f80233d7 --- /dev/null +++ b/samples/res/layout/fragment_scrollviewwithfab.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/main/res/layout/fragment_viewpagertabfragment_parent.xml b/samples/res/layout/fragment_viewpagertabfragment_parent.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/fragment_viewpagertabfragment_parent.xml rename to samples/res/layout/fragment_viewpagertabfragment_parent.xml diff --git a/samples/res/layout/fragment_webview.xml b/samples/res/layout/fragment_webview.xml new file mode 100644 index 00000000..b02d9ec3 --- /dev/null +++ b/samples/res/layout/fragment_webview.xml @@ -0,0 +1,19 @@ + + diff --git a/observablescrollview-samples/src/main/res/layout/gradient_header.xml b/samples/res/layout/gradient_header.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/gradient_header.xml rename to samples/res/layout/gradient_header.xml diff --git a/observablescrollview-samples/src/main/res/layout/list_item_handletouch.xml b/samples/res/layout/list_item_handletouch.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/list_item_handletouch.xml rename to samples/res/layout/list_item_handletouch.xml diff --git a/observablescrollview-samples/src/main/res/layout/list_item_main.xml b/samples/res/layout/list_item_main.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/list_item_main.xml rename to samples/res/layout/list_item_main.xml diff --git a/samples/res/layout/padding.xml b/samples/res/layout/padding.xml new file mode 100644 index 00000000..b3550e37 --- /dev/null +++ b/samples/res/layout/padding.xml @@ -0,0 +1,19 @@ + + diff --git a/observablescrollview-samples/src/main/res/layout/recycler_header.xml b/samples/res/layout/recycler_header.xml similarity index 100% rename from observablescrollview-samples/src/main/res/layout/recycler_header.xml rename to samples/res/layout/recycler_header.xml diff --git a/samples/res/layout/tab_indicator.xml b/samples/res/layout/tab_indicator.xml new file mode 100644 index 00000000..3bf50059 --- /dev/null +++ b/samples/res/layout/tab_indicator.xml @@ -0,0 +1,25 @@ + + \ No newline at end of file diff --git a/observablescrollview-samples/src/main/res/menu/menu_main.xml b/samples/res/menu/menu_main.xml similarity index 95% rename from observablescrollview-samples/src/main/res/menu/menu_main.xml rename to samples/res/menu/menu_main.xml index 1a525fde..bf2b06e9 100644 --- a/observablescrollview-samples/src/main/res/menu/menu_main.xml +++ b/samples/res/menu/menu_main.xml @@ -22,6 +22,7 @@ android:id="@+id/menu_about" android:orderInCategory="100" android:title="@string/menu_about" + android:icon="@drawable/ic_action_info" app:showAsAction="ifRoom" /> diff --git a/samples/res/values-ar/strings.xml b/samples/res/values-ar/strings.xml new file mode 100644 index 00000000..8f4439fd --- /dev/null +++ b/samples/res/values-ar/strings.xml @@ -0,0 +1,20 @@ + + + + الفضاء مرن + + diff --git a/observablescrollview-samples/src/main/res/values-w820dp/dimens.xml b/samples/res/values-w820dp/dimens.xml similarity index 100% rename from observablescrollview-samples/src/main/res/values-w820dp/dimens.xml rename to samples/res/values-w820dp/dimens.xml diff --git a/observablescrollview-samples/src/main/res/values/colors.xml b/samples/res/values/colors.xml similarity index 100% rename from observablescrollview-samples/src/main/res/values/colors.xml rename to samples/res/values/colors.xml diff --git a/observablescrollview-samples/src/main/res/values/dimens.xml b/samples/res/values/dimens.xml similarity index 100% rename from observablescrollview-samples/src/main/res/values/dimens.xml rename to samples/res/values/dimens.xml diff --git a/observablescrollview-samples/src/main/res/values/strings.xml b/samples/res/values/strings.xml similarity index 95% rename from observablescrollview-samples/src/main/res/values/strings.xml rename to samples/res/values/strings.xml index ad39bf57..bb7879a5 100644 --- a/observablescrollview-samples/src/main/res/values/strings.xml +++ b/samples/res/values/strings.xml @@ -25,6 +25,9 @@ ScrollView & Action Bar WebView & Action Bar Flexible Space + Flexible Space + Flexible Space + Flexible Space Flexible Space Another implementation of Fill Gap & ListView Another implementation of Fill Gap & RecyclerView @@ -32,7 +35,9 @@ Fill Gap & ListView Fill Gap & RecyclerView Fill Gap & ScrollView + Flexible Space Flexible Space + Flexible Space ListView on Fragment & Action Bar ActionBar & Toolbar manipulation with Fragment Handling touch with GridView @@ -40,6 +45,8 @@ Handling touch with RecyclerView Handling touch with ScrollView Handling touch with WebView + Parallax gridView & toolbar + Parallax listView & toolbar Parallax scrollView & toolbar ListView, scrolling from bottom RecyclerView, scrolling from bottom @@ -62,6 +69,7 @@ ViewPager & Tab & Different fragments ViewPager & Tab & ListView ViewPager & Tab & ScrollView + ViewPager & Tab & ScrollView & FAB 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 diff --git a/observablescrollview-samples/src/main/res/values/strings_license.xml b/samples/res/values/strings_license.xml similarity index 100% rename from observablescrollview-samples/src/main/res/values/strings_license.xml rename to samples/res/values/strings_license.xml diff --git a/samples/res/values/styles.xml b/samples/res/values/styles.xml new file mode 100644 index 00000000..5e90b566 --- /dev/null +++ b/samples/res/values/styles.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + diff --git a/observablescrollview-samples/src/androidTest/java/com/github/ksoichiro/app/ApplicationTest.java b/samples/src/androidTest/java/com/github/ksoichiro/app/ApplicationTest.java similarity index 100% rename from observablescrollview-samples/src/androidTest/java/com/github/ksoichiro/app/ApplicationTest.java rename to samples/src/androidTest/java/com/github/ksoichiro/app/ApplicationTest.java diff --git a/observablescrollview-samples/src/debug/res/drawable-xhdpi/ic_launcher.png b/samples/src/debug/res/drawable-xhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/debug/res/drawable-xhdpi/ic_launcher.png rename to samples/src/debug/res/drawable-xhdpi/ic_launcher.png diff --git a/observablescrollview-samples/src/debug/res/drawable-xxhdpi/ic_launcher.png b/samples/src/debug/res/drawable-xxhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/debug/res/drawable-xxhdpi/ic_launcher.png rename to samples/src/debug/res/drawable-xxhdpi/ic_launcher.png diff --git a/observablescrollview-samples/src/debug/res/drawable-xxxhdpi/ic_launcher.png b/samples/src/debug/res/drawable-xxxhdpi/ic_launcher.png similarity index 100% rename from observablescrollview-samples/src/debug/res/drawable-xxxhdpi/ic_launcher.png rename to samples/src/debug/res/drawable-xxxhdpi/ic_launcher.png diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java similarity index 78% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java index b8f81299..26af1c1f 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/AboutActivity.java @@ -16,19 +16,22 @@ package com.github.ksoichiro.android.observablescrollview.samples; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.util.Linkify; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; -public class AboutActivity extends ActionBarActivity { +public class AboutActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { @@ -39,8 +42,8 @@ protected void onCreate(Bundle savedInstanceState) { ab.setDisplayHomeAsUpEnabled(true); ab.setHomeButtonEnabled(true); } - ((TextView) findViewById(R.id.app_version)).setText(getString(R.string.msg_app_version, BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH)); - ((TextView) findViewById(R.id.lib_version)).setText(getString(R.string.msg_lib_version, BuildConfig.LIB_VERSION)); + ((TextView) findViewById(R.id.app_version)).setText(getString(R.string.msg_app_version, getVersionName(), VersionInfo.BUILD)); + ((TextView) findViewById(R.id.lib_version)).setText(getString(R.string.msg_lib_version, VersionInfo.LIBRARY_VERSION)); initLicenses(); } @@ -62,7 +65,7 @@ private void initLicenses() { String[] licenseList = getResources().getStringArray(R.array.license_list); content.addView(createItemsText(softwareList)); for (int i = 0; i < softwareList.length; i++) { - content.addView(createDivider(inflater)); + content.addView(createDivider(inflater, content)); content.addView(createHeader(softwareList[i])); content.addView(createHtmlText(licenseList[i])); } @@ -103,7 +106,19 @@ private TextView createHtmlText(final String s, final int margin) { return text; } - private View createDivider(final LayoutInflater inflater) { - return inflater.inflate(R.layout.divider, null); + private View createDivider(final LayoutInflater inflater, final ViewGroup parent) { + return inflater.inflate(R.layout.divider, parent, false); + } + + private String getVersionName() { + final PackageManager manager = getPackageManager(); + String versionName; + try { + final PackageInfo info = manager.getPackageInfo(getPackageName(), PackageManager.GET_META_DATA); + versionName = info.versionName; + } catch (PackageManager.NameNotFoundException e) { + versionName = ""; + } + return versionName; } } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java similarity index 97% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java index a8b9dd52..70de6577 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlGridViewActivity.java @@ -46,6 +46,9 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java similarity index 98% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java index 7d73f997..f798d7d1 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlListViewActivity.java @@ -63,6 +63,9 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java similarity index 97% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java index 5deef2eb..d5127ce4 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlRecyclerViewActivity.java @@ -49,6 +49,9 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java similarity index 97% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java index c7012e22..f7a78b1e 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlScrollViewActivity.java @@ -45,6 +45,9 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java similarity index 97% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java index c27d91db..e193b3a2 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ActionBarControlWebViewActivity.java @@ -46,6 +46,9 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { ActionBar ab = getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java similarity index 91% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java index 3ee7edfb..d94cb195 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseActivity.java @@ -17,7 +17,7 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.content.res.TypedArray; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.util.TypedValue; import android.view.View; @@ -28,7 +28,7 @@ import java.util.ArrayList; -public abstract class BaseActivity extends ActionBarActivity { +public abstract class BaseActivity extends AppCompatActivity { private static final int NUM_OF_ITEMS = 100; private static final int NUM_OF_ITEMS_FEW = 3; @@ -51,7 +51,7 @@ public static ArrayList getDummyData() { } public static ArrayList getDummyData(int num) { - ArrayList items = new ArrayList(); + ArrayList items = new ArrayList<>(); for (int i = 1; i <= num; i++) { items.add("Item " + i); } @@ -67,7 +67,7 @@ protected void setDummyDataFew(ListView listView) { } protected void setDummyData(ListView listView, int num) { - listView.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, getDummyData(num))); + listView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, getDummyData(num))); } protected void setDummyDataWithHeader(ListView listView, int headerHeight) { @@ -89,7 +89,7 @@ protected void setDummyDataWithHeader(ListView listView, View headerView, int nu } protected void setDummyData(GridView gridView) { - gridView.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, getDummyData())); + gridView.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, getDummyData())); } protected void setDummyData(RecyclerView recyclerView) { diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java similarity index 84% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java index 0722d4ef..17574263 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/BaseFragment.java @@ -26,6 +26,8 @@ import android.widget.GridView; import android.widget.ListView; +import com.github.ksoichiro.android.observablescrollview.ObservableGridView; + import java.util.ArrayList; public abstract class BaseFragment extends Fragment { @@ -56,7 +58,7 @@ protected int getScreenHeight() { } protected void setDummyData(ListView listView) { - listView.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, getDummyData())); + listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, getDummyData())); } protected void setDummyDataWithHeader(ListView listView, View headerView) { @@ -65,7 +67,12 @@ protected void setDummyDataWithHeader(ListView listView, View headerView) { } protected void setDummyData(GridView gridView) { - gridView.setAdapter(new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, getDummyData())); + gridView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, getDummyData())); + } + + protected void setDummyDataWithHeader(ObservableGridView gridView, View headerView) { + gridView.addHeaderView(headerView); + setDummyData(gridView); } protected void setDummyData(RecyclerView recyclerView) { diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java similarity index 78% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java index 7979d106..880c2eb6 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2BaseActivity.java @@ -16,6 +16,7 @@ package com.github.ksoichiro.android.observablescrollview.samples; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; import com.github.ksoichiro.android.observablescrollview.Scrollable; /** @@ -25,11 +26,6 @@ */ public abstract class FillGap2BaseActivity extends FillGapBaseActivity { protected float getHeaderTranslationY(int scrollY) { - final int headerHeight = mHeaderBar.getHeight(); - int headerTranslationY = 0; - if (0 <= -scrollY + mFlexibleSpaceImageHeight - headerHeight) { - headerTranslationY = -scrollY + mFlexibleSpaceImageHeight - headerHeight; - } - return headerTranslationY; + return ScrollUtils.getFloat(-scrollY + mFlexibleSpaceImageHeight - mHeaderBar.getHeight(), 0, Float.MAX_VALUE); } } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java index d1bf69dd..b7e551bd 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ListViewActivity.java @@ -20,6 +20,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.nineoldandroids.view.ViewHelper; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap2ListViewActivity extends FillGap2BaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java index 07d2edfe..84309e40 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2RecyclerViewActivity.java @@ -22,6 +22,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.nineoldandroids.view.ViewHelper; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap2RecyclerViewActivity extends FillGap2BaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java similarity index 95% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java index 10fb089b..7b5af3ef 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap2ScrollViewActivity.java @@ -19,6 +19,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap2ScrollViewActivity extends FillGap2BaseActivity implements ObservableScrollViewCallbacks { @Override protected int getLayoutResId() { diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java similarity index 98% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java index 9f17c96e..bb1b69d2 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3BaseActivity.java @@ -17,6 +17,7 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.widget.Toolbar; import android.view.MotionEvent; import android.view.View; @@ -114,7 +115,7 @@ public void run() { } @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mInitialTranslationY = savedInstanceState.getFloat(STATE_TRANSLATION_Y); } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java similarity index 95% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java index a1efc934..529d50a5 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ListViewActivity.java @@ -19,6 +19,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap3ListViewActivity extends FillGap3BaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java index 6ccef2ea..69082535 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3RecyclerViewActivity.java @@ -21,6 +21,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap3RecyclerViewActivity extends FillGap3BaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java similarity index 95% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java index 7d8432fc..2614f678 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGap3ScrollViewActivity.java @@ -19,6 +19,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGap3ScrollViewActivity extends FillGap3BaseActivity implements ObservableScrollViewCallbacks { @Override protected int getLayoutResId() { diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java similarity index 89% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java index c7a9386e..1efcd807 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapBaseActivity.java @@ -17,7 +17,6 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.os.Bundle; -import android.support.v7.widget.Toolbar; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; @@ -31,6 +30,11 @@ import com.nineoldandroids.view.ViewHelper; import com.nineoldandroids.view.ViewPropertyAnimator; +/** + * Warning: This example does not work on Android 2.3. + * + * @param Scrollable + */ public abstract class FillGapBaseActivity extends BaseActivity implements ObservableScrollViewCallbacks { protected View mHeader; @@ -40,7 +44,7 @@ public abstract class FillGapBaseActivity extends BaseActi protected int mActionBarSize; protected int mIntersectionHeight; - private View mImageHolder; + private View mImage; private View mHeaderBackground; private int mPrevScrollY; private boolean mGapIsChanging; @@ -52,8 +56,6 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getLayoutResId()); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); - mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); mActionBarSize = getActionBarSize(); @@ -61,7 +63,7 @@ protected void onCreate(Bundle savedInstanceState) { // within mIntersectionHeight. mIntersectionHeight = getResources().getDimensionPixelSize(R.dimen.intersection_height); - mImageHolder = findViewById(R.id.image_holder); + mImage = findViewById(R.id.image); mHeader = findViewById(R.id.header); mHeaderBar = findViewById(R.id.header_bar); mHeaderBackground = findViewById(R.id.header_background); @@ -75,14 +77,6 @@ protected void onCreate(Bundle savedInstanceState) { ScrollUtils.addOnGlobalLayoutListener((View) scrollable, new Runnable() { @Override public void run() { - // mListBackgroundView makes ListView's background except header view. - if (mListBackgroundView != null) { - final View contentView = getWindow().getDecorView().findViewById(android.R.id.content); - // mListBackgroundView's should fill its parent vertically - // but the height of the content view is 0 on 'onCreate'. - mListBackgroundView.getLayoutParams().height = contentView.getHeight(); - } - mReady = true; updateViews(scrollable.getCurrentScrollY(), false); } @@ -113,7 +107,7 @@ protected void updateViews(int scrollY, boolean animated) { return; } // Translate image - ViewHelper.setTranslationY(mImageHolder, -scrollY / 2); + ViewHelper.setTranslationY(mImage, -scrollY / 2); // Translate header ViewHelper.setTranslationY(mHeader, getHeaderTranslationY(scrollY)); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java index a06f2d05..d7b7f5c9 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapListViewActivity.java @@ -20,6 +20,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.nineoldandroids.view.ViewHelper; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGapListViewActivity extends FillGapBaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java index 5984ef84..78c0a5e1 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapRecyclerViewActivity.java @@ -22,6 +22,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.nineoldandroids.view.ViewHelper; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGapRecyclerViewActivity extends FillGapBaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java similarity index 95% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java index 4d6f2c74..2926ca0e 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FillGapScrollViewActivity.java @@ -19,6 +19,9 @@ import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +/** + * Warning: This example does not work on Android 2.3. + */ public class FillGapScrollViewActivity extends FillGapBaseActivity implements ObservableScrollViewCallbacks { @Override protected int getLayoutResId() { diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java similarity index 89% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java index 8eea12f7..529b14ea 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarScrollViewActivity.java @@ -17,6 +17,7 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.os.Bundle; +import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.TextView; @@ -40,7 +41,10 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_flexiblespacetoolbarscrollview); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } mFlexibleSpaceView = findViewById(R.id.flexible_space); mTitleView = (TextView) findViewById(R.id.title); @@ -80,12 +84,7 @@ public void onUpOrCancelMotionEvent(ScrollState scrollState) { private void updateFlexibleSpaceText(final int scrollY) { ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY); - int adjustedScrollY = scrollY; - if (scrollY < 0) { - adjustedScrollY = 0; - } else if (mFlexibleSpaceHeight < scrollY) { - adjustedScrollY = mFlexibleSpaceHeight; - } + int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight); float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight(); float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight; @@ -93,7 +92,6 @@ private void updateFlexibleSpaceText(final int scrollY) { ViewHelper.setPivotY(mTitleView, 0); ViewHelper.setScaleX(mTitleView, 1 + scale); ViewHelper.setScaleY(mTitleView, 1 + scale); - ViewHelper.setTranslationY(mTitleView, ViewHelper.getTranslationY(mFlexibleSpaceView) + mFlexibleSpaceView.getHeight() - mTitleView.getHeight() * (1 + scale)); int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale)); int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight); ViewHelper.setTranslationY(mTitleView, titleTranslationY); diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarWebViewActivity.java new file mode 100644 index 00000000..6a9719f1 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceToolbarWebViewActivity.java @@ -0,0 +1,126 @@ +/* + * 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.samples; + +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.webkit.WebView; +import android.widget.FrameLayout; +import android.widget.TextView; + +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.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class FlexibleSpaceToolbarWebViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { + + private View mFlexibleSpaceView; + private View mToolbarView; + private TextView mTitleView; + private int mFlexibleSpaceHeight; + private View mWebViewContainer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flexiblespacetoolbarwebview); + + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + mFlexibleSpaceView = findViewById(R.id.flexible_space); + mTitleView = (TextView) findViewById(R.id.title); + mTitleView.setText(getTitle()); + setTitle(null); + mToolbarView = findViewById(R.id.toolbar); + + mWebViewContainer = findViewById(R.id.webViewContainer); + + final ObservableScrollView scrollView = (ObservableScrollView) findViewById(R.id.scroll); + scrollView.setScrollViewCallbacks(this); + + WebView webView = (WebView) findViewById(R.id.webView); + webView.loadUrl("file:///android_asset/lipsum.html"); + + mFlexibleSpaceHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_height); + int flexibleSpaceAndToolbarHeight = mFlexibleSpaceHeight + getActionBarSize(); + + final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) webView.getLayoutParams(); + layoutParams.topMargin = flexibleSpaceAndToolbarHeight; + webView.setLayoutParams(layoutParams); + + mFlexibleSpaceView.getLayoutParams().height = flexibleSpaceAndToolbarHeight; + + ScrollUtils.addOnGlobalLayoutListener(mTitleView, new Runnable() { + @Override + public void run() { + updateFlexibleSpaceText(scrollView.getCurrentScrollY()); + } + }); + } + + @Override + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { + updateFlexibleSpaceText(scrollY); + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } + + private void updateFlexibleSpaceText(final int scrollY) { + ViewHelper.setTranslationY(mFlexibleSpaceView, -scrollY); + int adjustedScrollY = (int) ScrollUtils.getFloat(scrollY, 0, mFlexibleSpaceHeight); + + // Special logic for WebView. + adjustTopMargin(mWebViewContainer, adjustedScrollY <= mFlexibleSpaceHeight ? 0 : mFlexibleSpaceHeight + getActionBarSize()); + + float maxScale = (float) (mFlexibleSpaceHeight - mToolbarView.getHeight()) / mToolbarView.getHeight(); + float scale = maxScale * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight; + + ViewHelper.setPivotX(mTitleView, 0); + ViewHelper.setPivotY(mTitleView, 0); + ViewHelper.setScaleX(mTitleView, 1 + scale); + ViewHelper.setScaleY(mTitleView, 1 + scale); + int maxTitleTranslationY = mToolbarView.getHeight() + mFlexibleSpaceHeight - (int) (mTitleView.getHeight() * (1 + scale)); + int titleTranslationY = (int) (maxTitleTranslationY * ((float) mFlexibleSpaceHeight - adjustedScrollY) / mFlexibleSpaceHeight); + ViewHelper.setTranslationY(mTitleView, titleTranslationY); + } + + private void adjustTopMargin(View view, int topMargin) { + final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams(); + + if (layoutParams.topMargin == topMargin) { + return; + } + + layoutParams.topMargin = topMargin; + + view.setLayoutParams(layoutParams); + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageBaseFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageBaseFragment.java new file mode 100644 index 00000000..47d19fc1 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageBaseFragment.java @@ -0,0 +1,79 @@ +/* + * 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.samples; + +import android.os.Bundle; +import android.view.View; + +import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +import com.github.ksoichiro.android.observablescrollview.ScrollState; +import com.github.ksoichiro.android.observablescrollview.Scrollable; + +public abstract class FlexibleSpaceWithImageBaseFragment extends BaseFragment + implements ObservableScrollViewCallbacks { + + public static final String ARG_SCROLL_Y = "ARG_SCROLL_Y"; + + public void setArguments(int scrollY) { + if (0 <= scrollY) { + Bundle args = new Bundle(); + args.putInt(ARG_SCROLL_Y, scrollY); + setArguments(args); + } + } + + public void setScrollY(int scrollY, int threshold) { + View view = getView(); + if (view == null) { + return; + } + Scrollable scrollView = (Scrollable) view.findViewById(R.id.scroll); + if (scrollView == null) { + return; + } + scrollView.scrollVerticallyTo(scrollY); + } + + protected void updateFlexibleSpace(int scrollY) { + updateFlexibleSpace(scrollY, getView()); + } + + protected abstract void updateFlexibleSpace(int scrollY, View view); + + @Override + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { + if (getView() == null) { + return; + } + updateFlexibleSpace(scrollY, getView()); + } + + @Override + public final void onDownMotionEvent() { + // We don't use this callback in this pattern. + } + + @Override + public final void onUpOrCancelMotionEvent(ScrollState scrollState) { + // We don't use this callback in this pattern. + } + + protected S getScrollable() { + View view = getView(); + return view == null ? null : (S) view.findViewById(R.id.scroll); + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewActivity.java new file mode 100644 index 00000000..4b1a2987 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewActivity.java @@ -0,0 +1,182 @@ +/* + * 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.samples; + +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.widget.AbsListView; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; + +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.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; + +/** + * This example depends on {@code ObservableGridView#addHeaderView()} method introduced in v1.6.0. + */ +public class FlexibleSpaceWithImageGridViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { + + private static final float MAX_TEXT_SCALE_DELTA = 0.3f; + + private View mImageView; + private View mOverlayView; + private View mListBackgroundView; + private TextView mTitleView; + private View mFab; + private int mActionBarSize; + private int mFlexibleSpaceShowFabOffset; + private int mFlexibleSpaceImageHeight; + private int mFabMargin; + private boolean mFabIsShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flexiblespacewithimagegridview); + + mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + mFlexibleSpaceShowFabOffset = getResources().getDimensionPixelSize(R.dimen.flexible_space_show_fab_offset); + mActionBarSize = getActionBarSize(); + mImageView = findViewById(R.id.image); + mOverlayView = findViewById(R.id.overlay); + ObservableGridView gridView = (ObservableGridView) findViewById(R.id.list); + gridView.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, + mFlexibleSpaceImageHeight); + paddingView.setLayoutParams(lp); + + // This is required to disable header's list selector effect + paddingView.setClickable(true); + + gridView.addHeaderView(paddingView); + setDummyData(gridView); + mTitleView = (TextView) findViewById(R.id.title); + mTitleView.setText(getTitle()); + setTitle(null); + mFab = findViewById(R.id.fab); + mFab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(FlexibleSpaceWithImageGridViewActivity.this, "FAB is clicked", Toast.LENGTH_SHORT).show(); + } + }); + mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); + ViewHelper.setScaleX(mFab, 0); + ViewHelper.setScaleY(mFab, 0); + + // mListBackgroundView makes ListView's background except header view. + mListBackgroundView = findViewById(R.id.list_background); + } + + @Override + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { + // Translate overlay and image + 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)); + + // Translate list background + ViewHelper.setTranslationY(mListBackgroundView, Math.max(0, -scrollY + mFlexibleSpaceImageHeight)); + + // Change alpha of overlay + ViewHelper.setAlpha(mOverlayView, ScrollUtils.getFloat((float) scrollY / flexibleRange, 0, 1)); + + // Scale title text + float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); + setPivotXToTitle(); + ViewHelper.setPivotY(mTitleView, 0); + ViewHelper.setScaleX(mTitleView, scale); + ViewHelper.setScaleY(mTitleView, scale); + + // Translate title text + int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale); + int titleTranslationY = maxTitleTranslationY - scrollY; + ViewHelper.setTranslationY(mTitleView, titleTranslationY); + + // Translate FAB + 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); + } + + // Show/hide FAB + if (fabTranslationY < mFlexibleSpaceShowFabOffset) { + hideFab(); + } else { + showFab(); + } + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setPivotXToTitle() { + Configuration config = getResources().getConfiguration(); + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT + && config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + ViewHelper.setPivotX(mTitleView, findViewById(android.R.id.content).getWidth()); + } else { + ViewHelper.setPivotX(mTitleView, 0); + } + } + + 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; + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewFragment.java new file mode 100644 index 00000000..91b9716d --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageGridViewFragment.java @@ -0,0 +1,135 @@ +/* + * 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.samples; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.github.ksoichiro.android.observablescrollview.ObservableGridView; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class FlexibleSpaceWithImageGridViewFragment extends FlexibleSpaceWithImageBaseFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_flexiblespacewithimagegridview, container, false); + + final ObservableGridView gridView = (ObservableGridView) view.findViewById(R.id.scroll); + // Set padding view for GridView. This is the flexible space. + View paddingView = new View(getActivity()); + final int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, + flexibleSpaceImageHeight); + paddingView.setLayoutParams(lp); + + // This is required to disable header's list selector effect + paddingView.setClickable(true); + + gridView.addHeaderView(paddingView); + setDummyData(gridView); + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + gridView.setTouchInterceptionViewGroup((ViewGroup) view.findViewById(R.id.fragment_root)); + + // 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(gridView, new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + int offset = scrollY % flexibleSpaceImageHeight; + setSelectionFromTop(gridView, 0, -offset); + } + }); + updateFlexibleSpace(scrollY, view); + } else { + updateFlexibleSpace(0, view); + } + + gridView.setScrollViewCallbacks(this); + + updateFlexibleSpace(0, view); + + return view; + } + + @SuppressWarnings("NewApi") + @Override + public void setScrollY(int scrollY, int threshold) { + View view = getView(); + if (view == null) { + return; + } + ObservableGridView gridView = (ObservableGridView) view.findViewById(R.id.scroll); + if (gridView == null) { + return; + } + View firstVisibleChild = gridView.getChildAt(0); + if (firstVisibleChild != null) { + int offset = scrollY; + int position = 0; + if (threshold < scrollY) { + int baseHeight = firstVisibleChild.getHeight(); + position = scrollY / baseHeight; + offset = scrollY % baseHeight; + } + setSelectionFromTop(gridView, position, -offset); + } + } + + @Override + protected void updateFlexibleSpace(int scrollY, View view) { + int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + + View listBackgroundView = view.findViewById(R.id.list_background); + + // Translate list background + ViewHelper.setTranslationY(listBackgroundView, Math.max(0, -scrollY + flexibleSpaceImageHeight)); + + // Also pass this event to parent Activity + FlexibleSpaceWithImageWithViewPagerTabActivity parentActivity = + (FlexibleSpaceWithImageWithViewPagerTabActivity) getActivity(); + if (parentActivity != null) { + parentActivity.onScrollChanged(scrollY, (ObservableGridView) view.findViewById(R.id.scroll)); + } + } + + /* + * setSelectionFromTop method has been moved from ListView to AbsListView since API level 21, + * so for API level 21-, we need to use other method to scroll with offset. + * smoothScrollToPositionFromTop seems to work, but it's from API level 11. + * We can't use GridView for Gingerbread. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setSelectionFromTop(ObservableGridView gridView, int position, int offset) { + if (Build.VERSION_CODES.LOLLIPOP <= Build.VERSION.SDK_INT) { + gridView.setSelectionFromTop(position, offset); + } else if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) { + gridView.smoothScrollToPositionFromTop(position, offset, 0); + } + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java similarity index 75% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java index 4d04b235..ed9d07b9 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewActivity.java @@ -16,12 +16,15 @@ package com.github.ksoichiro.android.observablescrollview.samples; -import android.graphics.Color; +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; -import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.AbsListView; +import android.widget.FrameLayout; import android.widget.TextView; +import android.widget.Toast; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; @@ -33,9 +36,7 @@ public class FlexibleSpaceWithImageListViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { private static final float MAX_TEXT_SCALE_DELTA = 0.3f; - private static final boolean TOOLBAR_IS_STICKY = false; - private View mToolbar; private View mImageView; private View mOverlayView; private View mListBackgroundView; @@ -45,7 +46,6 @@ public class FlexibleSpaceWithImageListViewActivity extends BaseActivity impleme private int mFlexibleSpaceShowFabOffset; private int mFlexibleSpaceImageHeight; private int mFabMargin; - private int mToolbarColor; private boolean mFabIsShown; @Override @@ -53,17 +53,9 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_flexiblespacewithimagelistview); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); - mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); mFlexibleSpaceShowFabOffset = getResources().getDimensionPixelSize(R.dimen.flexible_space_show_fab_offset); mActionBarSize = getActionBarSize(); - mToolbarColor = getResources().getColor(R.color.primary); - - mToolbar = findViewById(R.id.toolbar); - if (!TOOLBAR_IS_STICKY) { - mToolbar.setBackgroundColor(Color.TRANSPARENT); - } mImageView = findViewById(R.id.image); mOverlayView = findViewById(R.id.overlay); ObservableListView listView = (ObservableListView) findViewById(R.id.list); @@ -84,22 +76,18 @@ protected void onCreate(Bundle savedInstanceState) { mTitleView.setText(getTitle()); setTitle(null); mFab = findViewById(R.id.fab); + mFab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(FlexibleSpaceWithImageListViewActivity.this, "FAB is clicked", Toast.LENGTH_SHORT).show(); + } + }); mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); ViewHelper.setScaleX(mFab, 0); ViewHelper.setScaleY(mFab, 0); // mListBackgroundView makes ListView's background except header view. mListBackgroundView = findViewById(R.id.list_background); - final View contentView = getWindow().getDecorView().findViewById(android.R.id.content); - contentView.post(new Runnable() { - @Override - public void run() { - // mListBackgroundView's should fill its parent vertically - // but the height of the content view is 0 on 'onCreate'. - // So we should get it with post(). - mListBackgroundView.getLayoutParams().height = contentView.getHeight(); - } - }); } @Override @@ -118,7 +106,7 @@ public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) // Scale title text float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); - ViewHelper.setPivotX(mTitleView, 0); + setPivotXToTitle(); ViewHelper.setPivotY(mTitleView, 0); ViewHelper.setScaleX(mTitleView, scale); ViewHelper.setScaleY(mTitleView, scale); @@ -126,9 +114,6 @@ public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) // Translate title text int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale); int titleTranslationY = maxTitleTranslationY - scrollY; - if (TOOLBAR_IS_STICKY) { - titleTranslationY = Math.max(0, titleTranslationY); - } ViewHelper.setTranslationY(mTitleView, titleTranslationY); // Translate FAB @@ -137,31 +122,24 @@ public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) -scrollY + mFlexibleSpaceImageHeight - mFab.getHeight() / 2, mActionBarSize - mFab.getHeight() / 2, maxFabTranslationY); - ViewHelper.setTranslationX(mFab, mOverlayView.getWidth() - mFabMargin - mFab.getWidth()); - ViewHelper.setTranslationY(mFab, fabTranslationY); + 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); + } // Show/hide FAB - if (ViewHelper.getTranslationY(mFab) < mFlexibleSpaceShowFabOffset) { + if (fabTranslationY < mFlexibleSpaceShowFabOffset) { hideFab(); } else { showFab(); } - - if (TOOLBAR_IS_STICKY) { - // Change alpha of toolbar background - if (-scrollY + mFlexibleSpaceImageHeight <= mActionBarSize) { - mToolbar.setBackgroundColor(ScrollUtils.getColorWithAlpha(1, mToolbarColor)); - } else { - mToolbar.setBackgroundColor(ScrollUtils.getColorWithAlpha(0, mToolbarColor)); - } - } else { - // Translate Toolbar - if (scrollY < mFlexibleSpaceImageHeight) { - ViewHelper.setTranslationY(mToolbar, 0); - } else { - ViewHelper.setTranslationY(mToolbar, -scrollY); - } - } } @Override @@ -172,6 +150,17 @@ public void onDownMotionEvent() { public void onUpOrCancelMotionEvent(ScrollState scrollState) { } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setPivotXToTitle() { + Configuration config = getResources().getConfiguration(); + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT + && config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + ViewHelper.setPivotX(mTitleView, findViewById(android.R.id.content).getWidth()); + } else { + ViewHelper.setPivotX(mTitleView, 0); + } + } + private void showFab() { if (!mFabIsShown) { ViewPropertyAnimator.animate(mFab).cancel(); diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewFragment.java new file mode 100644 index 00000000..41916528 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageListViewFragment.java @@ -0,0 +1,118 @@ +/* + * 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.samples; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; + +import com.github.ksoichiro.android.observablescrollview.ObservableListView; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class FlexibleSpaceWithImageListViewFragment extends FlexibleSpaceWithImageBaseFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_flexiblespacewithimagelistview, container, false); + + final ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); + // Set padding view for ListView. This is the flexible space. + View paddingView = new View(getActivity()); + final int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + AbsListView.LayoutParams lp = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, + flexibleSpaceImageHeight); + paddingView.setLayoutParams(lp); + + // This is required to disable header's list selector effect + paddingView.setClickable(true); + + listView.addHeaderView(paddingView); + setDummyData(listView); + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + listView.setTouchInterceptionViewGroup((ViewGroup) view.findViewById(R.id.fragment_root)); + + // 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(listView, new Runnable() { + @SuppressLint("NewApi") + @Override + public void run() { + int offset = scrollY % flexibleSpaceImageHeight; + listView.setSelectionFromTop(0, -offset); + } + }); + updateFlexibleSpace(scrollY, view); + } else { + updateFlexibleSpace(0, view); + } + + listView.setScrollViewCallbacks(this); + + updateFlexibleSpace(0, view); + + return view; + } + + @SuppressWarnings("NewApi") + @Override + public void setScrollY(int scrollY, int threshold) { + View view = getView(); + if (view == null) { + return; + } + ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); + if (listView == null) { + return; + } + View firstVisibleChild = listView.getChildAt(0); + if (firstVisibleChild != null) { + int offset = scrollY; + int position = 0; + if (threshold < scrollY) { + int baseHeight = firstVisibleChild.getHeight(); + position = scrollY / baseHeight; + offset = scrollY % baseHeight; + } + listView.setSelectionFromTop(position, -offset); + } + } + + @Override + protected void updateFlexibleSpace(int scrollY, View view) { + int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + + View listBackgroundView = view.findViewById(R.id.list_background); + + // Translate list background + ViewHelper.setTranslationY(listBackgroundView, Math.max(0, -scrollY + flexibleSpaceImageHeight)); + + // Also pass this event to parent Activity + FlexibleSpaceWithImageWithViewPagerTabActivity parentActivity = + (FlexibleSpaceWithImageWithViewPagerTabActivity) getActivity(); + if (parentActivity != null) { + parentActivity.onScrollChanged(scrollY, (ObservableListView) view.findViewById(R.id.scroll)); + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewActivity.java new file mode 100644 index 00000000..05aa8a65 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewActivity.java @@ -0,0 +1,128 @@ +package com.github.ksoichiro.android.observablescrollview.samples; + +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +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.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class FlexibleSpaceWithImageRecyclerViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { + + private static final float MAX_TEXT_SCALE_DELTA = 0.3f; + + private View mImageView; + private View mOverlayView; + private View mRecyclerViewBackground; + private TextView mTitleView; + private int mActionBarSize; + private int mFlexibleSpaceImageHeight; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flexiblespacewithimagerecyclerview); + + mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + mActionBarSize = getActionBarSize(); + + ObservableRecyclerView recyclerView = (ObservableRecyclerView) findViewById(R.id.recycler); + recyclerView.setScrollViewCallbacks(this); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(false); + final View headerView = LayoutInflater.from(this).inflate(R.layout.recycler_header, null); + headerView.post(new Runnable() { + @Override + public void run() { + headerView.getLayoutParams().height = mFlexibleSpaceImageHeight; + } + }); + setDummyDataWithHeader(recyclerView, headerView); + + mImageView = findViewById(R.id.image); + mOverlayView = findViewById(R.id.overlay); + + mTitleView = (TextView) findViewById(R.id.title); + mTitleView.setText(getTitle()); + setTitle(null); + + // mRecyclerViewBackground makes RecyclerView's background except header view. + mRecyclerViewBackground = findViewById(R.id.list_background); + + //since you cannot programmatically add a header view to a RecyclerView we added an empty view as the header + // in the adapter and then are shifting the views OnCreateView to compensate + final float scale = 1 + MAX_TEXT_SCALE_DELTA; + mRecyclerViewBackground.post(new Runnable() { + @Override + public void run() { + ViewHelper.setTranslationY(mRecyclerViewBackground, mFlexibleSpaceImageHeight); + } + }); + ViewHelper.setTranslationY(mOverlayView, mFlexibleSpaceImageHeight); + mTitleView.post(new Runnable() { + @Override + public void run() { + ViewHelper.setTranslationY(mTitleView, (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale)); + ViewHelper.setPivotX(mTitleView, 0); + ViewHelper.setPivotY(mTitleView, 0); + ViewHelper.setScaleX(mTitleView, scale); + ViewHelper.setScaleY(mTitleView, scale); + } + }); + } + + + @Override + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { + // Translate overlay and image + 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)); + + // Translate list background + ViewHelper.setTranslationY(mRecyclerViewBackground, Math.max(0, -scrollY + mFlexibleSpaceImageHeight)); + + // Change alpha of overlay + ViewHelper.setAlpha(mOverlayView, ScrollUtils.getFloat((float) scrollY / flexibleRange, 0, 1)); + + // Scale title text + float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); + setPivotXToTitle(); + ViewHelper.setPivotY(mTitleView, 0); + ViewHelper.setScaleX(mTitleView, scale); + ViewHelper.setScaleY(mTitleView, scale); + + // Translate title text + int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale); + int titleTranslationY = maxTitleTranslationY - scrollY; + ViewHelper.setTranslationY(mTitleView, titleTranslationY); + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setPivotXToTitle() { + Configuration config = getResources().getConfiguration(); + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT + && config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + ViewHelper.setPivotX(mTitleView, findViewById(android.R.id.content).getWidth()); + } else { + ViewHelper.setPivotX(mTitleView, 0); + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewFragment.java new file mode 100644 index 00000000..eb59162c --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageRecyclerViewFragment.java @@ -0,0 +1,116 @@ +/* + * 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.samples; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class FlexibleSpaceWithImageRecyclerViewFragment extends FlexibleSpaceWithImageBaseFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_flexiblespacewithimagerecyclerview, container, false); + + final ObservableRecyclerView recyclerView = (ObservableRecyclerView) view.findViewById(R.id.scroll); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + recyclerView.setHasFixedSize(false); + final View headerView = LayoutInflater.from(getActivity()).inflate(R.layout.recycler_header, null); + final int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + headerView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, flexibleSpaceImageHeight)); + setDummyDataWithHeader(recyclerView, headerView); + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + recyclerView.setTouchInterceptionViewGroup((ViewGroup) view.findViewById(R.id.fragment_root)); + + // 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(recyclerView, new Runnable() { + @Override + public void run() { + int offset = scrollY % flexibleSpaceImageHeight; + RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); + if (lm != null && lm instanceof LinearLayoutManager) { + ((LinearLayoutManager) lm).scrollToPositionWithOffset(0, -offset); + } + } + }); + updateFlexibleSpace(scrollY, view); + } else { + updateFlexibleSpace(0, view); + } + + recyclerView.setScrollViewCallbacks(this); + + return view; + } + + @Override + public void setScrollY(int scrollY, int threshold) { + View view = getView(); + if (view == null) { + return; + } + ObservableRecyclerView recyclerView = (ObservableRecyclerView) view.findViewById(R.id.scroll); + if (recyclerView == null) { + return; + } + View firstVisibleChild = recyclerView.getChildAt(0); + if (firstVisibleChild != null) { + int offset = scrollY; + int position = 0; + if (threshold < scrollY) { + int baseHeight = firstVisibleChild.getHeight(); + position = scrollY / baseHeight; + offset = scrollY % baseHeight; + } + RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); + if (lm != null && lm instanceof LinearLayoutManager) { + ((LinearLayoutManager) lm).scrollToPositionWithOffset(position, -offset); + } + } + } + + @Override + protected void updateFlexibleSpace(int scrollY, View view) { + int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + + View recyclerViewBackground = view.findViewById(R.id.list_background); + + // Translate list background + ViewHelper.setTranslationY(recyclerViewBackground, Math.max(0, -scrollY + flexibleSpaceImageHeight)); + + // Also pass this event to parent Activity + FlexibleSpaceWithImageWithViewPagerTabActivity parentActivity = + (FlexibleSpaceWithImageWithViewPagerTabActivity) getActivity(); + if (parentActivity != null) { + parentActivity.onScrollChanged(scrollY, (ObservableRecyclerView) view.findViewById(R.id.scroll)); + } + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java similarity index 80% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java index 33852ebd..8330329f 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewActivity.java @@ -16,11 +16,12 @@ package com.github.ksoichiro.android.observablescrollview.samples; -import android.graphics.Color; +import android.os.Build; import android.os.Bundle; -import android.support.v7.widget.Toolbar; import android.view.View; +import android.widget.FrameLayout; import android.widget.TextView; +import android.widget.Toast; import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; @@ -32,9 +33,7 @@ public class FlexibleSpaceWithImageScrollViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { private static final float MAX_TEXT_SCALE_DELTA = 0.3f; - private static final boolean TOOLBAR_IS_STICKY = false; - private View mToolbar; private View mImageView; private View mOverlayView; private ObservableScrollView mScrollView; @@ -44,7 +43,6 @@ public class FlexibleSpaceWithImageScrollViewActivity extends BaseActivity imple private int mFlexibleSpaceShowFabOffset; private int mFlexibleSpaceImageHeight; private int mFabMargin; - private int mToolbarColor; private boolean mFabIsShown; @Override @@ -52,17 +50,10 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_flexiblespacewithimagescrollview); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); - mFlexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); mFlexibleSpaceShowFabOffset = getResources().getDimensionPixelSize(R.dimen.flexible_space_show_fab_offset); mActionBarSize = getActionBarSize(); - mToolbarColor = getResources().getColor(R.color.primary); - mToolbar = findViewById(R.id.toolbar); - if (!TOOLBAR_IS_STICKY) { - mToolbar.setBackgroundColor(Color.TRANSPARENT); - } mImageView = findViewById(R.id.image); mOverlayView = findViewById(R.id.overlay); mScrollView = (ObservableScrollView) findViewById(R.id.scroll); @@ -71,6 +62,12 @@ protected void onCreate(Bundle savedInstanceState) { mTitleView.setText(getTitle()); setTitle(null); mFab = findViewById(R.id.fab); + mFab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(FlexibleSpaceWithImageScrollViewActivity.this, "FAB is clicked", Toast.LENGTH_SHORT).show(); + } + }); mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); ViewHelper.setScaleX(mFab, 0); ViewHelper.setScaleY(mFab, 0); @@ -115,9 +112,6 @@ public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) // Translate title text int maxTitleTranslationY = (int) (mFlexibleSpaceImageHeight - mTitleView.getHeight() * scale); int titleTranslationY = maxTitleTranslationY - scrollY; - if (TOOLBAR_IS_STICKY) { - titleTranslationY = Math.max(0, titleTranslationY); - } ViewHelper.setTranslationY(mTitleView, titleTranslationY); // Translate FAB @@ -126,31 +120,24 @@ public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) -scrollY + mFlexibleSpaceImageHeight - mFab.getHeight() / 2, mActionBarSize - mFab.getHeight() / 2, maxFabTranslationY); - ViewHelper.setTranslationX(mFab, mOverlayView.getWidth() - mFabMargin - mFab.getWidth()); - ViewHelper.setTranslationY(mFab, fabTranslationY); + 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); + } // Show/hide FAB - if (ViewHelper.getTranslationY(mFab) < mFlexibleSpaceShowFabOffset) { + if (fabTranslationY < mFlexibleSpaceShowFabOffset) { hideFab(); } else { showFab(); } - - if (TOOLBAR_IS_STICKY) { - // Change alpha of toolbar background - if (-scrollY + mFlexibleSpaceImageHeight <= mActionBarSize) { - mToolbar.setBackgroundColor(ScrollUtils.getColorWithAlpha(1, mToolbarColor)); - } else { - mToolbar.setBackgroundColor(ScrollUtils.getColorWithAlpha(0, mToolbarColor)); - } - } else { - // Translate Toolbar - if (scrollY < mFlexibleSpaceImageHeight) { - ViewHelper.setTranslationY(mToolbar, 0); - } else { - ViewHelper.setTranslationY(mToolbar, -scrollY); - } - } } @Override diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewFragment.java new file mode 100644 index 00000000..f8d0ca02 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageScrollViewFragment.java @@ -0,0 +1,84 @@ +/* + * 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.samples; + +import android.os.Bundle; +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.ScrollUtils; +import com.github.ksoichiro.android.observablescrollview.Scrollable; + +public class FlexibleSpaceWithImageScrollViewFragment extends FlexibleSpaceWithImageBaseFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_flexiblespacewithimagescrollview, container, false); + + final ObservableScrollView scrollView = (ObservableScrollView) view.findViewById(R.id.scroll); + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + scrollView.setTouchInterceptionViewGroup((ViewGroup) view.findViewById(R.id.fragment_root)); + + // 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); + } + }); + updateFlexibleSpace(scrollY, view); + } else { + updateFlexibleSpace(0, view); + } + + scrollView.setScrollViewCallbacks(this); + + return view; + } + + @Override + protected void updateFlexibleSpace(int scrollY) { + // Sometimes scrollable.getCurrentScrollY() and the real scrollY has different values. + // As a workaround, we should call scrollVerticallyTo() to make sure that they match. + Scrollable s = getScrollable(); + s.scrollVerticallyTo(scrollY); + + // If scrollable.getCurrentScrollY() and the real scrollY has the same values, + // calling scrollVerticallyTo() won't invoke scroll (or onScrollChanged()), so we call it here. + // Calling this twice is not a problem as long as updateFlexibleSpace(int, View) has idempotence. + updateFlexibleSpace(scrollY, getView()); + } + + @Override + protected void updateFlexibleSpace(int scrollY, View view) { + ObservableScrollView scrollView = (ObservableScrollView) view.findViewById(R.id.scroll); + + // Also pass this event to parent Activity + FlexibleSpaceWithImageWithViewPagerTabActivity parentActivity = + (FlexibleSpaceWithImageWithViewPagerTabActivity) getActivity(); + if (parentActivity != null) { + parentActivity.onScrollChanged(scrollY, scrollView); + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTab2Activity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTab2Activity.java new file mode 100644 index 00000000..43b395d8 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTab2Activity.java @@ -0,0 +1,345 @@ +/* + * 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.samples; + +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import android.widget.TextView; + +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.view.ViewHelper; + +/** + *

This uses TouchInterceptionFrameLayout to move Fragments.

+ * + *

There is an unsolved problem: it doesn't scroll smoothly + * when the flexible space is changing.
+ * If it's a big problem to you, please also check + * FlexibleSpaceWithImageWithViewPagerTabActivity.

+ * + *

SlidingTabLayout and SlidingTabStrip are from google/iosched:
+ * https://github.com/google/iosched

+ */ +public class FlexibleSpaceWithImageWithViewPagerTab2Activity extends BaseActivity implements ObservableScrollViewCallbacks { + + private static final float MAX_TEXT_SCALE_DELTA = 0.3f; + private static final int INVALID_POINTER = -1; + + private View mImageView; + private View mOverlayView; + private TextView mTitleView; + private TouchInterceptionFrameLayout mInterceptionLayout; + private ViewPager mPager; + private NavigationAdapter mPagerAdapter; + private VelocityTracker mVelocityTracker; + private OverScroller mScroller; + private float mBaseTranslationY; + private int mMaximumVelocity; + private int mActivePointerId = INVALID_POINTER; + private int mSlop; + private int mFlexibleSpaceHeight; + private int mTabHeight; + private boolean mScrolled; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flexiblespacewithimagewithviewpagertab2); + + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + + ViewCompat.setElevation(findViewById(R.id.header), getResources().getDimension(R.dimen.toolbar_elevation)); + mPagerAdapter = new NavigationAdapter(getSupportFragmentManager()); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mPagerAdapter); + mImageView = findViewById(R.id.image); + mOverlayView = findViewById(R.id.overlay); + // Padding for ViewPager must be set outside the ViewPager itself + // because with padding, EdgeEffect of ViewPager become strange. + mFlexibleSpaceHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + mTabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); + findViewById(R.id.pager_wrapper).setPadding(0, mFlexibleSpaceHeight, 0, 0); + mTitleView = (TextView) findViewById(R.id.title); + mTitleView.setText(getTitle()); + setTitle(null); + + 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); + ((FrameLayout.LayoutParams) slidingTabLayout.getLayoutParams()).topMargin = mFlexibleSpaceHeight - mTabHeight; + + ViewConfiguration vc = ViewConfiguration.get(this); + mSlop = vc.getScaledTouchSlop(); + mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); + mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.container); + mInterceptionLayout.setScrollInterceptionListener(mInterceptionListener); + mScroller = new OverScroller(getApplicationContext()); + ScrollUtils.addOnGlobalLayoutListener(mInterceptionLayout, new Runnable() { + @Override + public void run() { + // Extra space is required to move mInterceptionLayout when it's scrolled. + // It's better to adjust its height when it's laid out + // than to adjust the height when scroll events (onMoveMotionEvent) occur + // because it causes lagging. + // See #87: https://github.com/ksoichiro/Android-ObservableScrollView/issues/87 + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mInterceptionLayout.getLayoutParams(); + lp.height = getScreenHeight() + mFlexibleSpaceHeight; + mInterceptionLayout.requestLayout(); + + updateFlexibleSpace(); + } + }); + } + + @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) { + 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 flexibleSpace = mFlexibleSpaceHeight - mTabHeight; + int translationY = (int) ViewHelper.getTranslationY(mInterceptionLayout); + boolean scrollingUp = 0 < diffY; + boolean scrollingDown = diffY < 0; + if (scrollingUp) { + if (translationY < 0) { + mScrolled = true; + return true; + } + } else if (scrollingDown) { + if (-flexibleSpace < translationY) { + mScrolled = true; + return true; + } + } + mScrolled = false; + return false; + } + + @Override + public void onDownMotionEvent(MotionEvent ev) { + mActivePointerId = ev.getPointerId(0); + mScroller.forceFinished(true); + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + mBaseTranslationY = ViewHelper.getTranslationY(mInterceptionLayout); + mVelocityTracker.addMovement(ev); + } + + @Override + public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) { + int flexibleSpace = mFlexibleSpaceHeight - mTabHeight; + float translationY = ScrollUtils.getFloat(ViewHelper.getTranslationY(mInterceptionLayout) + diffY, -flexibleSpace, 0); + MotionEvent e = MotionEvent.obtainNoHistory(ev); + e.offsetLocation(0, translationY - mBaseTranslationY); + mVelocityTracker.addMovement(e); + updateFlexibleSpace(translationY); + } + + @Override + public void onUpOrCancelMotionEvent(MotionEvent ev) { + mScrolled = false; + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocityY = (int) mVelocityTracker.getYVelocity(mActivePointerId); + mActivePointerId = INVALID_POINTER; + mScroller.forceFinished(true); + int baseTranslationY = (int) ViewHelper.getTranslationY(mInterceptionLayout); + + int minY = -(mFlexibleSpaceHeight - mTabHeight); + int maxY = 0; + mScroller.fling(0, baseTranslationY, 0, velocityY, 0, 0, minY, maxY); + new Handler().post(new Runnable() { + @Override + public void run() { + updateLayout(); + } + }); + } + }; + + private void updateLayout() { + boolean needsUpdate = false; + float translationY = 0; + if (mScroller.computeScrollOffset()) { + translationY = mScroller.getCurrY(); + int flexibleSpace = mFlexibleSpaceHeight - mTabHeight; + if (-flexibleSpace <= translationY && translationY <= 0) { + needsUpdate = true; + } else if (translationY < -flexibleSpace) { + translationY = -flexibleSpace; + needsUpdate = true; + } else if (0 < translationY) { + translationY = 0; + needsUpdate = true; + } + } + + if (needsUpdate) { + updateFlexibleSpace(translationY); + + new Handler().post(new Runnable() { + @Override + public void run() { + updateLayout(); + } + }); + } + } + + private 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 updateFlexibleSpace() { + updateFlexibleSpace(ViewHelper.getTranslationY(mInterceptionLayout)); + } + + private void updateFlexibleSpace(float translationY) { + ViewHelper.setTranslationY(mInterceptionLayout, translationY); + int minOverlayTransitionY = getActionBarSize() - mOverlayView.getHeight(); + ViewHelper.setTranslationY(mImageView, ScrollUtils.getFloat(-translationY / 2, minOverlayTransitionY, 0)); + + // Change alpha of overlay + float flexibleRange = mFlexibleSpaceHeight - getActionBarSize(); + ViewHelper.setAlpha(mOverlayView, ScrollUtils.getFloat(-translationY / flexibleRange, 0, 1)); + + // Scale title text + float scale = 1 + ScrollUtils.getFloat((flexibleRange + translationY - mTabHeight) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); + setPivotXToTitle(); + ViewHelper.setPivotY(mTitleView, 0); + ViewHelper.setScaleX(mTitleView, scale); + ViewHelper.setScaleY(mTitleView, scale); + } + + private Fragment getCurrentFragment() { + return mPagerAdapter.getItemAt(mPager.getCurrentItem()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setPivotXToTitle() { + Configuration config = getResources().getConfiguration(); + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT + && config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + ViewHelper.setPivotX(mTitleView, findViewById(android.R.id.content).getWidth()); + } else { + ViewHelper.setPivotX(mTitleView, 0); + } + } + + /** + * 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]; + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTabActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTabActivity.java new file mode 100644 index 00000000..c32c38f8 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FlexibleSpaceWithImageWithViewPagerTabActivity.java @@ -0,0 +1,266 @@ +/* + * 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.samples; + +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.widget.TextView; + +import com.github.ksoichiro.android.observablescrollview.CacheFragmentStatePagerAdapter; +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; + +/** + *

Another implementation of FlexibleImage pattern + ViewPager.

+ *

+ *

This is a completely different approach comparing to FlexibleImageWithViewPager2Activity. + *

+ *

Descriptions of this pattern:

+ *
    + *
  • When the current tab is changed, tabs will be translated in Y-axis + * using scrollY of the new page's Fragment.
  • + *
  • The parent Activity and children Fragments strongly depend on each other, + * so if you need to use this pattern, maybe you should extract some interfaces from them.
    + * (This is just an example, so we won't do it here.)
  • + *
  • The parent Activity and children Fragments communicate bidirectionally: + * the parent Activity will update the Fragment's state when the tab is changed, + * and Fragments will tell the parent Activity to update the tab's translationY.
  • + *
+ *

+ *

SlidingTabLayout and SlidingTabStrip are from google/iosched:
+ * https://github.com/google/iosched

+ */ +public class FlexibleSpaceWithImageWithViewPagerTabActivity extends BaseActivity { + + protected static final float MAX_TEXT_SCALE_DELTA = 0.3f; + + private ViewPager mPager; + private NavigationAdapter mPagerAdapter; + private SlidingTabLayout mSlidingTabLayout; + private int mFlexibleSpaceHeight; + private int mTabHeight; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_flexiblespacewithimagewithviewpagertab); + + mPagerAdapter = new NavigationAdapter(getSupportFragmentManager()); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mPagerAdapter); + mFlexibleSpaceHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + mTabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); + + TextView titleView = (TextView) findViewById(R.id.title); + titleView.setText(R.string.title_activity_flexiblespacewithimagewithviewpagertab); + + mSlidingTabLayout = (SlidingTabLayout) findViewById(R.id.sliding_tabs); + mSlidingTabLayout.setCustomTabView(R.layout.tab_indicator, android.R.id.text1); + mSlidingTabLayout.setSelectedIndicatorColors(getResources().getColor(R.color.accent)); + mSlidingTabLayout.setDistributeEvenly(true); + mSlidingTabLayout.setViewPager(mPager); + + // Initialize the first Fragment's state when layout is completed. + ScrollUtils.addOnGlobalLayoutListener(mSlidingTabLayout, new Runnable() { + @Override + public void run() { + translateTab(0, false); + } + }); + } + + /** + * Called by children Fragments when their scrollY are changed. + * They all call this method even when they are inactive + * but this Activity should listen only the active child, + * so each Fragments will pass themselves for Activity to check if they are active. + * + * @param scrollY scroll position of Scrollable + * @param s caller Scrollable view + */ + public void onScrollChanged(int scrollY, Scrollable s) { + FlexibleSpaceWithImageBaseFragment fragment = + (FlexibleSpaceWithImageBaseFragment) mPagerAdapter.getItemAt(mPager.getCurrentItem()); + if (fragment == null) { + return; + } + View view = fragment.getView(); + if (view == null) { + return; + } + Scrollable scrollable = (Scrollable) view.findViewById(R.id.scroll); + if (scrollable == null) { + return; + } + if (scrollable == s) { + // This method is called by not only the current fragment but also other fragments + // when their scrollY is changed. + // So we need to check the caller(S) is the current fragment. + int adjustedScrollY = Math.min(scrollY, mFlexibleSpaceHeight - mTabHeight); + translateTab(adjustedScrollY, false); + propagateScroll(adjustedScrollY); + } + } + + private void translateTab(int scrollY, boolean animated) { + int flexibleSpaceImageHeight = getResources().getDimensionPixelSize(R.dimen.flexible_space_image_height); + int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); + View imageView = findViewById(R.id.image); + View overlayView = findViewById(R.id.overlay); + TextView titleView = (TextView) findViewById(R.id.title); + + // Translate overlay and image + float flexibleRange = flexibleSpaceImageHeight - getActionBarSize(); + int minOverlayTransitionY = tabHeight - overlayView.getHeight(); + ViewHelper.setTranslationY(overlayView, ScrollUtils.getFloat(-scrollY, minOverlayTransitionY, 0)); + ViewHelper.setTranslationY(imageView, ScrollUtils.getFloat(-scrollY / 2, minOverlayTransitionY, 0)); + + // Change alpha of overlay + ViewHelper.setAlpha(overlayView, ScrollUtils.getFloat((float) scrollY / flexibleRange, 0, 1)); + + // Scale title text + float scale = 1 + ScrollUtils.getFloat((flexibleRange - scrollY - tabHeight) / flexibleRange, 0, MAX_TEXT_SCALE_DELTA); + setPivotXToTitle(titleView); + ViewHelper.setPivotY(titleView, 0); + ViewHelper.setScaleX(titleView, scale); + ViewHelper.setScaleY(titleView, scale); + + // Translate title text + int maxTitleTranslationY = flexibleSpaceImageHeight - tabHeight - getActionBarSize(); + int titleTranslationY = maxTitleTranslationY - scrollY; + ViewHelper.setTranslationY(titleView, titleTranslationY); + + // If tabs are moving, cancel it to start a new animation. + ViewPropertyAnimator.animate(mSlidingTabLayout).cancel(); + // Tabs will move between the top of the screen to the bottom of the image. + float translationY = ScrollUtils.getFloat(-scrollY + mFlexibleSpaceHeight - mTabHeight, 0, mFlexibleSpaceHeight - mTabHeight); + if (animated) { + // Animation will be invoked only when the current tab is changed. + ViewPropertyAnimator.animate(mSlidingTabLayout) + .translationY(translationY) + .setDuration(200) + .start(); + } else { + // When Fragments' scroll, translate tabs immediately (without animation). + ViewHelper.setTranslationY(mSlidingTabLayout, translationY); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setPivotXToTitle(View view) { + final TextView mTitleView = (TextView) view.findViewById(R.id.title); + Configuration config = getResources().getConfiguration(); + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT + && config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + ViewHelper.setPivotX(mTitleView, view.findViewById(android.R.id.content).getWidth()); + } else { + ViewHelper.setPivotX(mTitleView, 0); + } + } + + private void propagateScroll(int scrollY) { + // Set scrollY for the fragments that are not created yet + mPagerAdapter.setScrollY(scrollY); + + // 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 + FlexibleSpaceWithImageBaseFragment f = + (FlexibleSpaceWithImageBaseFragment) mPagerAdapter.getItemAt(i); + if (f == null) { + continue; + } + + View view = f.getView(); + if (view == null) { + continue; + } + f.setScrollY(scrollY, mFlexibleSpaceHeight); + f.updateFlexibleSpace(scrollY); + } + } + + /** + * This adapter provides three 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) { + FlexibleSpaceWithImageBaseFragment f; + final int pattern = position % 4; + switch (pattern) { + case 0: { + f = new FlexibleSpaceWithImageScrollViewFragment(); + break; + } + case 1: { + f = new FlexibleSpaceWithImageListViewFragment(); + break; + } + case 2: { + f = new FlexibleSpaceWithImageRecyclerViewFragment(); + break; + } + case 3: + default: { + f = new FlexibleSpaceWithImageGridViewFragment(); + break; + } + } + f.setArguments(mScrollY); + return f; + } + + @Override + public int getCount() { + return TITLES.length; + } + + @Override + public CharSequence getPageTitle(int position) { + return TITLES[position]; + } + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java similarity index 93% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java index d4bef833..5a175764 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentActionBarControlListViewFragment.java @@ -18,7 +18,7 @@ import android.os.Bundle; import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -55,11 +55,14 @@ public void onDownMotionEvent() { @Override public void onUpOrCancelMotionEvent(ScrollState scrollState) { - ActionBarActivity activity = (ActionBarActivity) getActivity(); + AppCompatActivity activity = (AppCompatActivity) getActivity(); if (activity == null) { return; } ActionBar ab = activity.getSupportActionBar(); + if (ab == null) { + return; + } if (scrollState == ScrollState.UP) { if (ab.isShowing()) { ab.hide(); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionDefaultFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionDefaultFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionDefaultFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionDefaultFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java similarity index 76% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java index ff63644a..7a896bb7 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/FragmentTransitionSecondFragment.java @@ -17,7 +17,8 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,16 +43,22 @@ public void onDestroyView() { } private void showToolbar() { - ActionBarActivity activity = (ActionBarActivity) getActivity(); + AppCompatActivity activity = (AppCompatActivity) getActivity(); if (activity != null) { - activity.getSupportActionBar().show(); + ActionBar ab = activity.getSupportActionBar(); + if (ab != null) { + ab.show(); + } } } private void hideToolbar() { - ActionBarActivity activity = (ActionBarActivity) getActivity(); + AppCompatActivity activity = (AppCompatActivity) getActivity(); if (activity != null) { - activity.getSupportActionBar().hide(); + ActionBar ab = activity.getSupportActionBar(); + if (ab != null) { + ab.hide(); + } } } } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchGridViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchGridViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchGridViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchListViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchListViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchRecyclerViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchRecyclerViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchScrollViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchScrollViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchWebViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchWebViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/HandleTouchWebViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java similarity index 66% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java index 5d65f3ed..db950ab3 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/MainActivity.java @@ -16,17 +16,21 @@ package com.github.ksoichiro.android.observablescrollview.samples; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.SimpleAdapter; +import android.widget.Spinner; import java.text.Collator; import java.util.ArrayList; @@ -37,8 +41,8 @@ import java.util.Map; -public class MainActivity extends ActionBarActivity implements AdapterView.OnItemClickListener { - private static final String CATEGORY_SAMPLES = BuildConfig.APPLICATION_ID; +public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener { + private static final String CATEGORY_SAMPLES = MainActivity.class.getPackage().getName(); private static final String TAG_CLASS_NAME = "className"; private static final String TAG_DESCRIPTION = "description"; private static final String TAG_INTENT = "intent"; @@ -51,18 +55,56 @@ public int compare(Map lhs, Map rhs) { return collator.compare(lhs.get("className"), rhs.get("className")); } }; + private ListView listView; + + // Quickly navigate through the examples. + enum Filter { + All, + GridView, + RecyclerView, + ScrollView, + ListView, + WebView, + Toolbar, + ActionBar, + FlexibleSpace, + Parallax, + ViewPager, + } + + Filter currentFilter = Filter.All; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView listView = (ListView) findViewById(android.R.id.list); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + listView = (ListView) findViewById(android.R.id.list); + listView.setOnItemClickListener(this); + + Spinner spinner = (Spinner) findViewById(R.id.spinner_toolbar); + spinner.setAdapter(new FilterAdapter(this)); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + currentFilter = Filter.values()[position]; + refreshData(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + private void refreshData() { listView.setAdapter(new SimpleAdapter(this, getData(), R.layout.list_item_main, new String[]{TAG_CLASS_NAME, TAG_DESCRIPTION,}, new int[]{R.id.className, R.id.description,})); - listView.setOnItemClickListener(this); } @Override @@ -82,9 +124,10 @@ public boolean onOptionsItemSelected(final MenuItem menu) { } private List> getData() { - List> data = new ArrayList>(); + List> data = new ArrayList<>(); Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.setPackage(getApplicationContext().getPackageName()); mainIntent.addCategory(CATEGORY_SAMPLES); PackageManager pm = getPackageManager(); @@ -110,12 +153,16 @@ private List> getData() { if (nameLabel.contains(".")) { nameLabel = nameLabel.replaceAll("[^.]*\\.", ""); } - addItem(data, - nameLabel, - nextLabel, - activityIntent( - info.activityInfo.applicationInfo.packageName, - info.activityInfo.name)); + + // Filter logic. + if (currentFilter == Filter.All || nameLabel.contains(currentFilter.name())) { + addItem(data, + nameLabel, + nextLabel, + activityIntent( + info.activityInfo.applicationInfo.packageName, + info.activityInfo.name)); + } } } @@ -132,7 +179,7 @@ protected Intent activityIntent(String pkg, String componentName) { protected void addItem(List> data, String className, String description, Intent intent) { - Map temp = new HashMap(); + Map temp = new HashMap<>(); temp.put(TAG_CLASS_NAME, className); temp.put(TAG_DESCRIPTION, description); temp.put(TAG_INTENT, intent); @@ -147,4 +194,11 @@ public void onItemClick(AdapterView parent, View view, int position, long id) Intent intent = (Intent) map.get(TAG_INTENT); startActivity(intent); } + + private class FilterAdapter extends ArrayAdapter { + public FilterAdapter(Context context) { + super(context, android.R.layout.simple_spinner_item, Filter.values()); + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + } + } } \ No newline at end of file diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarGridViewActivity.java new file mode 100644 index 00000000..3bf08ed0 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarGridViewActivity.java @@ -0,0 +1,96 @@ +/* + * 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.samples; + +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.view.View; +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; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +/** + * This example depends on {@code ObservableGridView#addHeaderView()} method introduced in v1.6.0. + */ +public class ParallaxToolbarGridViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { + + private View mImageView; + private View mToolbarView; + private View mListBackgroundView; + private ObservableGridView mGridView; + private int mParallaxImageHeight; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_parallaxtoolbargridview) ; + + 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); + + mGridView = (ObservableGridView) findViewById(R.id.list); + mGridView.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); + + // This is required to disable header's list selector effect + paddingView.setClickable(true); + + mGridView.addHeaderView(paddingView); + setDummyData(mGridView); + + // mListBackgroundView makes ListView's background except header view. + mListBackgroundView = findViewById(R.id.list_background); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + onScrollChanged(mGridView.getCurrentScrollY(), false, false); + } + + @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); + + // Translate list background + ViewHelper.setTranslationY(mListBackgroundView, Math.max(0, -scrollY + mParallaxImageHeight)); + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarListViewActivity.java new file mode 100644 index 00000000..d060cd30 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarListViewActivity.java @@ -0,0 +1,93 @@ +/* + * 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.samples; + +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.view.View; +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; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; + +public class ParallaxToolbarListViewActivity extends BaseActivity implements ObservableScrollViewCallbacks { + + 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); + + // This is required to disable header's list selector effect + paddingView.setClickable(true); + + mListView.addHeaderView(paddingView); + setDummyData(mListView); + + // mListBackgroundView makes ListView's background except header view. + mListBackgroundView = findViewById(R.id.list_background); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + onScrollChanged(mListView.getCurrentScrollY(), false, false); + } + + @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); + + // Translate list background + ViewHelper.setTranslationY(mListBackgroundView, Math.max(0, -scrollY + mParallaxImageHeight)); + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java index 5d3a3f0a..05e79955 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ParallaxToolbarScrollViewActivity.java @@ -59,7 +59,7 @@ protected void onRestoreInstanceState(Bundle savedInstanceState) { @Override public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { int baseColor = getResources().getColor(R.color.primary); - float alpha = 1 - (float) Math.max(0, mParallaxImageHeight - scrollY) / mParallaxImageHeight; + float alpha = Math.min(1, (float) scrollY / mParallaxImageHeight); mToolbarView.setBackgroundColor(ScrollUtils.getColorWithAlpha(alpha, baseColor)); ViewHelper.setTranslationY(mImageView, scrollY / 2); } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomListViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomListViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomRecyclerViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ScrollFromBottomRecyclerViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleHeaderRecyclerAdapter.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleHeaderRecyclerAdapter.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleHeaderRecyclerAdapter.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleHeaderRecyclerAdapter.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleRecyclerAdapter.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleRecyclerAdapter.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleRecyclerAdapter.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SimpleRecyclerAdapter.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java similarity index 94% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java index 19630427..db36a16c 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpBaseActivity.java @@ -19,11 +19,13 @@ import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; +import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; +import android.widget.Toast; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; import com.github.ksoichiro.android.observablescrollview.ScrollState; @@ -79,6 +81,12 @@ public abstract class SlidingUpBaseActivity extends BaseAc private boolean mHeaderColorChangedToBottom; private boolean mHeaderIsAtBottom; private boolean mHeaderIsNotAtBottom; + private View.OnClickListener fabClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(SlidingUpBaseActivity.this, "floating action button clicked", Toast.LENGTH_SHORT).show(); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -89,9 +97,12 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(mToolbar); ViewHelper.setScaleY(mToolbar, 0); - getSupportActionBar().setHomeButtonEnabled(true); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(""); + ActionBar ab = getSupportActionBar(); + if (ab != null) { + ab.setHomeButtonEnabled(true); + ab.setDisplayHomeAsUpEnabled(true); + ab.setTitle(""); + } mToolbarColor = getResources().getColor(R.color.primary); mToolbar.setBackgroundColor(Color.TRANSPARENT); @@ -119,6 +130,7 @@ public void onClick(View v) { mScrollable = createScrollable(); mFab = findViewById(R.id.fab); + mFab.setOnClickListener(fabClickListener); mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); mInterceptionLayout = (TouchInterceptionFrameLayout) findViewById(R.id.scroll_wrapper); @@ -179,8 +191,17 @@ public void onUpOrCancelMotionEvent(ScrollState scrollState) { @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); + + // slight fix for untappable floating action button for larger screens + Rect fabRect = new Rect(); + mFab.getHitRect(fabRect); + // if the user's touch is within the floating action button's touch area, don't intercept + if (fabRect.contains((int) ev.getX(), (int) ev.getY())) { + return false; + } else { + return minInterceptionLayoutY < (int) ViewHelper.getY(mInterceptionLayout) + || (moving && mScrollable.getCurrentScrollY() - diffY < 0); + } } @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpGridViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpGridViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpGridViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java similarity index 95% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java index f355bcca..3b2790fa 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpListViewActivity.java @@ -18,15 +18,11 @@ import android.view.View; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.Toast; import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; -import java.util.ArrayList; -import java.util.List; - public class SlidingUpListViewActivity extends SlidingUpBaseActivity implements ObservableScrollViewCallbacks { @Override diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpRecyclerViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpRecyclerViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpScrollViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpScrollViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpWebViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpWebViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/SlidingUpWebViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderListViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderListViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderRecyclerViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderRecyclerViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderScrollViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderScrollViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderWebViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderWebViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/StickyHeaderWebViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java similarity index 99% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java index 5acf7412..ab7556c6 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlBaseActivity.java @@ -99,7 +99,7 @@ public void onAnimationUpdate(ValueAnimator animation) { ViewHelper.setTranslationY(mToolbar, translationY); ViewHelper.setTranslationY((View) mScrollable, translationY); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) ((View) mScrollable).getLayoutParams(); - lp.height = (int) -translationY + getScreenHeight(); + lp.height = (int) -translationY + getScreenHeight() - lp.topMargin; ((View) mScrollable).requestLayout(); } }); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlGridViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlGridViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlGridViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlGridViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlListViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlListViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java similarity index 85% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java index 3fbfc854..02d83ec9 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlRecyclerViewActivity.java @@ -17,16 +17,11 @@ package com.github.ksoichiro.android.observablescrollview.samples; import android.support.v7.widget.LinearLayoutManager; -import android.util.Log; -import android.widget.AbsListView; -import com.github.ksoichiro.android.observablescrollview.ObservableListView; import com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView; public class ToolbarControlRecyclerViewActivity extends ToolbarControlBaseActivity { - private static final String TAG = ToolbarControlRecyclerViewActivity.class.getSimpleName(); - @Override protected int getLayoutResId() { return R.layout.activity_toolbarcontrolrecyclerview; diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlScrollViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlScrollViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlWebViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlWebViewActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlWebViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ToolbarControlWebViewActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2Activity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2Activity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2Activity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2Activity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2GridViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2GridViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2GridViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2GridViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ListViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ListViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ListViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ListViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2RecyclerViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2RecyclerViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2RecyclerViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2RecyclerViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ScrollViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ScrollViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ScrollViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2ScrollViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2WebViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2WebViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2WebViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTab2WebViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java similarity index 96% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java index 76394542..b62cc006 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabActivity.java @@ -254,7 +254,7 @@ 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; + final int pattern = position % 4; switch (pattern) { case 0: { f = new ViewPagerTabScrollViewFragment(); @@ -274,8 +274,7 @@ protected Fragment createItem(int position) { } break; } - case 2: - default: { + case 2: { f = new ViewPagerTabRecyclerViewFragment(); if (0 < mScrollY) { Bundle args = new Bundle(); @@ -284,6 +283,16 @@ protected Fragment createItem(int position) { } break; } + case 3: + default: { + f = new ViewPagerTabGridViewFragment(); + if (0 < mScrollY) { + Bundle args = new Bundle(); + args.putInt(ViewPagerTabGridViewFragment.ARG_INITIAL_POSITION, 1); + f.setArguments(args); + } + break; + } } return f; } diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentActivity.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentActivity.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentGridViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentGridViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentGridViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentGridViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentListViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentListViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentListViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentListViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java similarity index 99% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java index 01deef91..00d9464f 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentParentFragment.java @@ -20,7 +20,7 @@ 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.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -56,7 +56,7 @@ public class ViewPagerTabFragmentParentFragment extends BaseFragment implements public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_viewpagertabfragment_parent, container, false); - ActionBarActivity parentActivity = (ActionBarActivity) getActivity(); + AppCompatActivity parentActivity = (AppCompatActivity) getActivity(); mPagerAdapter = new NavigationAdapter(getChildFragmentManager()); mPager = (ViewPager) view.findViewById(R.id.pager); mPager.setAdapter(mPagerAdapter); diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentRecyclerViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentRecyclerViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentRecyclerViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentRecyclerViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentScrollViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentScrollViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentScrollViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentScrollViewFragment.java diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentWebViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentWebViewFragment.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentWebViewFragment.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabFragmentWebViewFragment.java diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabGridViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabGridViewFragment.java new file mode 100644 index 00000000..5f53f231 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabGridViewFragment.java @@ -0,0 +1,64 @@ +/* + * 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.samples; + +import android.app.Activity; +import android.os.Bundle; +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; +import com.github.ksoichiro.android.observablescrollview.ScrollUtils; + +public class ViewPagerTabGridViewFragment extends BaseFragment { + + 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_gridview, container, false); + + Activity parentActivity = getActivity(); + final ObservableGridView gridView = (ObservableGridView) view.findViewById(R.id.scroll); + setDummyDataWithHeader(gridView, inflater.inflate(R.layout.padding, gridView, false)); + + 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(gridView, new Runnable() { + @Override + public void run() { + // scrollTo() doesn't work, should use setSelection() + gridView.setSelection(initialPosition); + } + }); + } + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + gridView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.root)); + + gridView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); + } + return view; + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java similarity index 97% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java index 0eaca69c..cdaed4b3 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewActivity.java @@ -169,7 +169,11 @@ private void propagateToolbarState(boolean isShown) { continue; } - ObservableListView listView = (ObservableListView) f.getView().findViewById(R.id.scroll); + View view = f.getView(); + if (view == null) { + continue; + } + ObservableListView listView = (ObservableListView) view.findViewById(R.id.scroll); if (isShown) { // Scroll up if (0 < listView.getCurrentScrollY()) { diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java new file mode 100644 index 00000000..c709cdd1 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabListViewFragment.java @@ -0,0 +1,64 @@ +/* + * 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.samples; + +import android.app.Activity; +import android.os.Bundle; +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 BaseFragment { + + 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); + setDummyDataWithHeader(listView, inflater.inflate(R.layout.padding, listView, false)); + + 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); + } + }); + } + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + listView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.root)); + + listView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); + } + return view; + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java new file mode 100644 index 00000000..9cbf8c81 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabRecyclerViewFragment.java @@ -0,0 +1,67 @@ +/* + * 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.samples; + +import android.app.Activity; +import android.os.Bundle; +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 BaseFragment { + + 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); + setDummyDataWithHeader(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); + } + }); + } + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + recyclerView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.root)); + + recyclerView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); + } + return view; + } +} diff --git a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java similarity index 94% rename from observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java rename to samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java index 5755753f..fb296c9d 100644 --- a/observablescrollview-samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewActivity.java @@ -55,7 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { mHeaderView = findViewById(R.id.header); ViewCompat.setElevation(mHeaderView, getResources().getDimension(R.dimen.toolbar_elevation)); mToolbarView = findViewById(R.id.toolbar); - mPagerAdapter = new NavigationAdapter(getSupportFragmentManager()); + mPagerAdapter = newViewPagerAdapter(); mPager = (ViewPager) findViewById(R.id.pager); mPager.setAdapter(mPagerAdapter); @@ -169,7 +169,11 @@ private void propagateToolbarState(boolean isShown) { continue; } - ObservableScrollView scrollView = (ObservableScrollView) f.getView().findViewById(R.id.scroll); + View view = f.getView(); + if (view == null) { + continue; + } + ObservableScrollView scrollView = (ObservableScrollView) view.findViewById(R.id.scroll); if (isShown) { // Scroll up if (0 < scrollView.getCurrentScrollY()) { @@ -184,6 +188,10 @@ private void propagateToolbarState(boolean isShown) { } } + protected NavigationAdapter newViewPagerAdapter() { + return new NavigationAdapter(getSupportFragmentManager()); + } + private boolean toolbarIsShown() { return ViewHelper.getTranslationY(mHeaderView) == 0; } @@ -210,7 +218,7 @@ private void hideToolbar() { propagateToolbarState(false); } - private static class NavigationAdapter extends CacheFragmentStatePagerAdapter { + protected 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"}; @@ -224,9 +232,13 @@ public void setScrollY(int scrollY) { mScrollY = scrollY; } + protected Fragment newFragment() { + return new ViewPagerTabScrollViewFragment(); + } + @Override protected Fragment createItem(int position) { - Fragment f = new ViewPagerTabScrollViewFragment(); + Fragment f = newFragment(); if (0 <= mScrollY) { Bundle args = new Bundle(); args.putInt(ViewPagerTabScrollViewFragment.ARG_SCROLL_Y, mScrollY); diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java new file mode 100644 index 00000000..94b22e81 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewFragment.java @@ -0,0 +1,61 @@ +/* + * 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.samples; + +import android.app.Activity; +import android.os.Bundle; +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 BaseFragment { + + 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); + } + }); + } + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + scrollView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.root)); + + scrollView.setScrollViewCallbacks((ObservableScrollViewCallbacks) parentActivity); + } + return view; + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabActivity.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabActivity.java new file mode 100644 index 00000000..ad47c45e --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabActivity.java @@ -0,0 +1,46 @@ +/* + * 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.samples; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +/** + * This example shows how to handle scroll events on both the parent Activity and Fragments. + * (Handling FAB is not the main purpose) + * + * SlidingTabLayout and SlidingTabStrip are from google/iosched: + * https://github.com/google/iosched + */ +public class ViewPagerTabScrollViewWithFabActivity extends ViewPagerTabScrollViewActivity { + + @Override + protected NavigationAdapter newViewPagerAdapter() { + return new NavigationAdapter(getSupportFragmentManager()); + } + + private static class NavigationAdapter extends ViewPagerTabScrollViewActivity.NavigationAdapter { + public NavigationAdapter(FragmentManager fm) { + super(fm); + } + + @Override + protected Fragment newFragment() { + return new ViewPagerTabScrollViewWithFabFragment(); + } + } +} diff --git a/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabFragment.java b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabFragment.java new file mode 100644 index 00000000..625217e2 --- /dev/null +++ b/samples/src/main/java/com/github/ksoichiro/android/observablescrollview/samples/ViewPagerTabScrollViewWithFabFragment.java @@ -0,0 +1,138 @@ +/* + * 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.samples; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +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.ScrollUtils; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; + +/** + * This example shows how to handle scroll events on both the parent Activity and Fragments. + * (Handling FAB is not the main purpose) + */ +public class ViewPagerTabScrollViewWithFabFragment extends BaseFragment implements ObservableScrollViewCallbacks { + + public static final String ARG_SCROLL_Y = "ARG_SCROLL_Y"; + private View mFab; + private int mFabMargin; + private boolean mFabIsShown; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_scrollviewwithfab, container, false); + mFab = view.findViewById(R.id.fab); + mFabMargin = getResources().getDimensionPixelSize(R.dimen.margin_standard); + mFabIsShown = true; + + // Translate FAB + ScrollUtils.addOnGlobalLayoutListener(mFab, new Runnable() { + @Override + public void run() { + float fabTranslationY = view.getHeight() - mFabMargin - mFab.getHeight(); + 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 = view.getWidth() - mFabMargin - mFab.getWidth(); + lp.topMargin = (int) fabTranslationY; + mFab.requestLayout(); + } else { + ViewHelper.setTranslationX(mFab, view.getWidth() - mFabMargin - mFab.getWidth()); + ViewHelper.setTranslationY(mFab, fabTranslationY); + } + } + }); + + 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); + } + }); + } + + // TouchInterceptionViewGroup should be a parent view other than ViewPager. + // This is a workaround for the issue #117: + // https://github.com/ksoichiro/Android-ObservableScrollView/issues/117 + scrollView.setTouchInterceptionViewGroup((ViewGroup) parentActivity.findViewById(R.id.root)); + } + scrollView.setScrollViewCallbacks(this); + + return view; + } + + @Override + public void onScrollChanged(int scrollY, boolean firstScroll, boolean dragging) { + if (getActivity() != null && getActivity() instanceof ObservableScrollViewCallbacks) { + ((ObservableScrollViewCallbacks) getActivity()).onScrollChanged(scrollY, firstScroll, dragging); + } + } + + @Override + public void onDownMotionEvent() { + if (getActivity() != null && getActivity() instanceof ObservableScrollViewCallbacks) { + ((ObservableScrollViewCallbacks) getActivity()).onDownMotionEvent(); + } + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + if (getActivity() != null && getActivity() instanceof ObservableScrollViewCallbacks) { + ((ObservableScrollViewCallbacks) getActivity()).onUpOrCancelMotionEvent(scrollState); + } + + if (scrollState == ScrollState.UP) { + hideFab(); + } else if (scrollState == ScrollState.DOWN) { + showFab(); + } + } + + 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; + } + } +} diff --git a/observablescrollview-samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java b/samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java similarity index 100% rename from observablescrollview-samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java rename to samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java diff --git a/samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java b/samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java new file mode 100644 index 00000000..803c4fb4 --- /dev/null +++ b/samples/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java @@ -0,0 +1,168 @@ +/* + * 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; + } + } +} diff --git a/settings.gradle b/settings.gradle index 63bd0ca3..69de25de 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':observablescrollview', ':observablescrollview-samples' +include ':library', ':samples' diff --git a/website/.bowerrc b/website/.bowerrc new file mode 100644 index 00000000..69fad358 --- /dev/null +++ b/website/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..46aaef49 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +bower_components/ +public/lib/ +public/docs/ +www/ +repo/ +*.log diff --git a/website/bower.json b/website/bower.json new file mode 100644 index 00000000..669446cf --- /dev/null +++ b/website/bower.json @@ -0,0 +1,36 @@ +{ + "name": "Android-ObservableScrollView", + "description": "bower components for Android-ObservableScrollView", + "dependencies": { + "jquery": "1.11.2", + "bootstrap": "3.3.4", + "roboto-fontface": "0.4.0", + "highlightjs": "8.5.0", + "respond-minmax": "1.4.2", + "html5shiv": "3.7.2", + "octicons": "2.2.2" + }, + "overrides": { + "jquery": { + "main": "dist/*.min.*" + }, + "bootstrap": { + "main": ["dist/css/*.min.css", "dist/js/*.min.js", "dist/fonts/*"] + }, + "roboto-fontface": { + "main": ["fonts/Roboto-Thin*", "fonts/Roboto-Light*", "fonts/Roboto-Regular*"] + }, + "highlightjs": { + "main": ["*.js"] + }, + "respond-minmax": { + "main": "dest/*.min.js" + }, + "html5shiv": { + "main": "dist/*.min.js" + }, + "octicons": { + "main": ["octicons/*.css", "octicons/*.svg", "octicons/*.woff", "octicons/*.ttf", "octicons/*.eot"] + } + } +} diff --git a/website/gulpfile.js b/website/gulpfile.js new file mode 100644 index 00000000..443e7413 --- /dev/null +++ b/website/gulpfile.js @@ -0,0 +1,162 @@ +var gulp = require('gulp'); +var mainBowerFiles = require('main-bower-files'); +var git = require('gulp-git'); +var harp = require('harp'); +var del = require('del'); +var gutil = require('gulp-util'); + +var ghToken = process.env.GH_TOKEN; + +// You should replace this configs to your project's values. +var project = { + githubRepoOwner: 'ksoichiro', + name: 'Android-ObservableScrollView', +}; + +if (process.env.GH_TOKEN) { + // Travis CI + var gitBaseUrl = 'github.com/' + project.githubRepoOwner + '/' + project.name + '.git'; + project['gitUrl'] = 'https://' + gitBaseUrl; + project['gitPushUrl'] = 'https://' + ghToken + '@' + gitBaseUrl; +} else { + // Local + var gitBaseUrl = 'github.com:' + project.githubRepoOwner + '/' + project.name + '.git'; + project['gitUrl'] = 'git@' + gitBaseUrl; + project['gitPushUrl'] = 'origin'; +} + +var paths = { + bower: "./bower_components", + harp: { + project: ".", + output: "./www" + }, + docs: "../docs", + dest: { + root: "./www", + lib: "./public/lib", + docs: "./public/docs" + }, + repo: "repo" +}; + +var port = 9000; + +gulp.task('cleanDocs', function(cb) { + del([paths.dest.docs], cb); +}); + +gulp.task('cleanBowerFiles', function(cb) { + del([paths.dest.lib], cb); +}); + +gulp.task('clean', ['cleanDocs', 'cleanBowerFiles'], function(cb) { + del([paths.dest.root, paths.repo], cb); +}); + +gulp.task('build', ['copy'], function(cb) { + // This task is for production, so BASE_URL should be a project name. + // $BASE_URL is referenced in harp.json, and it will be replaced by harp (envy). + process.env.BASE_URL = '/' + project.name + harp.compile(paths.harp.project, paths.harp.output, function(err) { + if (err) { + gutil.log('build failed: ' + err); + } + gutil.log('Compile done'); + cb(); + }); +}); + +gulp.task('copyDocs', ['cleanDocs'], function() { + return gulp.src(paths.docs + '/**/*') + .pipe(gulp.dest(paths.dest.docs)); +}); + +gulp.task('copyBowerFiles', ['cleanBowerFiles'], function() { + // Return streams so that the dependent tasks can detect when it has done. + return gulp.src(mainBowerFiles(), { base: paths.bower }) + .pipe(gulp.dest(paths.dest.lib)); +}); + +gulp.task('copy', ['copyDocs', 'copyBowerFiles'], function() { +}); + +gulp.task('remove-repo', ['build'], function(cb) { + // Remove old directory, if exists. + gutil.log('removing repo..'); + del([paths.repo], cb); +}); + +gulp.task('git-clone', ['remove-repo'], function(cb) { + // Clone to build directory, commit files to gh-pages branch, and push it. + git.clone(project.gitUrl, {args: paths.repo}, function(err) { + if (err) { + gutil.log('Failed to clone: ' + err); + } else { + gutil.log('clone done'); + } + cb(); + }); +}); + +gulp.task('deploy', ['git-clone'], function(cb) { + gutil.log('check out gh-pages...'); + git.checkout('origin/gh-pages', {args: '-b gh-pages', cwd: paths.repo}, function(err) { + if (err) { + gutil.log('Failed to check out branch: ' + err); + throw err; + } else { + gutil.log('copying files...'); + gulp.src(paths.harp.output + '/**') + .pipe(gulp.dest(paths.repo)) + .on('finish', function() { + gutil.log('finished to copy'); + + git.exec({args: 'status --porcelain', cwd: paths.repo}, function (err, stdout) { + if (err) { + gutil.log('Failed to check diff: ' + err); + throw err; + } else if (stdout) { + // There are some changes for gh-pages + gutil.log('There are some changes to commit.'); + gulp.src(paths.repo) + .pipe(git.add({args: '-A', cwd: paths.repo})) + .pipe(git.commit('Updated website.', {cwd: paths.repo})) + .on('end', function() { + gutil.log('finish'); + git.push(project.gitPushUrl, 'gh-pages', {args: '--quiet', /*quiet: true, */cwd: paths.repo}, function(err) { + if (err) { + gutil.log('Failed to push: ' + err); + throw err; + } else { + gutil.log('Pushed successfully.'); + } + cb(); + }); + }); + } else { + gutil.log('Nothing to commit.'); + cb(); + } + }); + }); + } + }); +}); + +gulp.task('start', ['copy'], function() { + // This task is for development locally, so BASE_URL should be empty. + // $BASE_URL is referenced in harp.json, and it will be replaced by harp (envy). + process.env.BASE_URL = '' + harp.server(paths.harp.project, { port: port }, function(err) { + if (err) { + gutil.log('Failed to start'); + gutil.log(err); + } else { + gulp.watch([paths.docs + '/**/*', '!' + paths.docs + '/**/*.sw*'], ['copyDocs']); + gulp.watch(paths.bower + '/**/*', ['copyBowerFiles']); + gutil.log('Started server: http://localhost:' + port); + gutil.log('Press Ctrl+C to quit'); + } + }); +}); diff --git a/website/harp.json b/website/harp.json new file mode 100644 index 00000000..8b74c9fe --- /dev/null +++ b/website/harp.json @@ -0,0 +1,15 @@ +{ + "globals": { + "baseUrl": "$BASE_URL", + "title": "Android-ObservableScrollView", + "name": "Android-ObservableScrollView", + "description": "Android library to observe scroll events on scrollable views.", + "githubUser": "ksoichiro", + "githubRepo": "Android-ObservableScrollView", + "githubUrl": "https://github.com/ksoichiro/Android-ObservableScrollView", + "ogType": "website", + "ogUrl": "http://ksoichiro.github.io/Android-ObservableScrollView", + "copyrightYear": "2014", + "copyrightHolder": "Soichiro Kashima" + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..820c9fe5 --- /dev/null +++ b/website/package.json @@ -0,0 +1,27 @@ +{ + "name": "Android-ObservableScrollView", + "description": "Web site for Android-ObservableScrollView", + "repository": { + "type": "git", + "url": "https://github.com/ksoichiro/Android-ObservableScrollView.git" + }, + "dependencies": { + }, + "devDependencies": { + "bower": "1.3.12", + "gulp": "3.8.11", + "del": "1.1.1", + "harp": "0.17.0", + "main-bower-files": "2.7.0", + "gulp-git": "1.2.1", + "gulp-util": "3.0.4" + }, + "scripts": { + "prepublish": "bower install --config.interactive=false", + "start": "gulp start", + "copy": "gulp copy", + "build": "gulp build", + "deploy": "gulp deploy", + "clean": "gulp clean" + } +} diff --git a/website/public/404.ejs b/website/public/404.ejs new file mode 100644 index 00000000..9365e9b5 --- /dev/null +++ b/website/public/404.ejs @@ -0,0 +1,2 @@ +

404

+

Page Not Found

diff --git a/website/public/_data.json b/website/public/_data.json new file mode 100644 index 00000000..b6a7fe24 --- /dev/null +++ b/website/public/_data.json @@ -0,0 +1,24 @@ +{ + "index": { + "title": "Android-ObservableScrollView", + "layout": false + }, + "quick-start": { + "title": "Quick start" + }, + "example": { + "title": "Try the example app" + }, + "basic": { + "title": "Basic techniques" + }, + "advanced": { + "title": "Advanced techniques" + }, + "contributor": { + "title": "For contributors" + }, + "reference": { + "title": "Reference" + } +} diff --git a/website/public/_footer.ejs b/website/public/_footer.ejs new file mode 100644 index 00000000..bb47b70c --- /dev/null +++ b/website/public/_footer.ejs @@ -0,0 +1,16 @@ + + + + + + + diff --git a/website/public/_head.ejs b/website/public/_head.ejs new file mode 100644 index 00000000..4ebe9af4 --- /dev/null +++ b/website/public/_head.ejs @@ -0,0 +1,47 @@ + + + + +<%- title %> | <%- title === name ? description : name %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/public/_layout.ejs b/website/public/_layout.ejs new file mode 100644 index 00000000..b1072230 --- /dev/null +++ b/website/public/_layout.ejs @@ -0,0 +1,20 @@ + + + +<%- partial("_head") %> + + + +<%- partial("_nav") %> + +
+
+
+<%- yield %> +
+
+
+ +<%- partial("_footer") %> + + diff --git a/website/public/_nav.ejs b/website/public/_nav.ejs new file mode 100644 index 00000000..3a02ffc9 --- /dev/null +++ b/website/public/_nav.ejs @@ -0,0 +1,20 @@ + diff --git a/website/public/android-icon-144x144.png b/website/public/android-icon-144x144.png new file mode 100755 index 00000000..504f74db Binary files /dev/null and b/website/public/android-icon-144x144.png differ diff --git a/website/public/android-icon-192x192.png b/website/public/android-icon-192x192.png new file mode 100755 index 00000000..a6a10ce8 Binary files /dev/null and b/website/public/android-icon-192x192.png differ diff --git a/website/public/android-icon-36x36.png b/website/public/android-icon-36x36.png new file mode 100755 index 00000000..c770edde Binary files /dev/null and b/website/public/android-icon-36x36.png differ diff --git a/website/public/android-icon-48x48.png b/website/public/android-icon-48x48.png new file mode 100755 index 00000000..66e484a7 Binary files /dev/null and b/website/public/android-icon-48x48.png differ diff --git a/website/public/android-icon-72x72.png b/website/public/android-icon-72x72.png new file mode 100755 index 00000000..d127245e Binary files /dev/null and b/website/public/android-icon-72x72.png differ diff --git a/website/public/android-icon-96x96.png b/website/public/android-icon-96x96.png new file mode 100755 index 00000000..0afa873a Binary files /dev/null and b/website/public/android-icon-96x96.png differ diff --git a/website/public/apple-icon-114x114.png b/website/public/apple-icon-114x114.png new file mode 100755 index 00000000..813b306f Binary files /dev/null and b/website/public/apple-icon-114x114.png differ diff --git a/website/public/apple-icon-120x120.png b/website/public/apple-icon-120x120.png new file mode 100755 index 00000000..ff3eec0a Binary files /dev/null and b/website/public/apple-icon-120x120.png differ diff --git a/website/public/apple-icon-144x144.png b/website/public/apple-icon-144x144.png new file mode 100755 index 00000000..504f74db Binary files /dev/null and b/website/public/apple-icon-144x144.png differ diff --git a/website/public/apple-icon-152x152.png b/website/public/apple-icon-152x152.png new file mode 100755 index 00000000..abef3ed4 Binary files /dev/null and b/website/public/apple-icon-152x152.png differ diff --git a/website/public/apple-icon-180x180.png b/website/public/apple-icon-180x180.png new file mode 100755 index 00000000..dbedc0b2 Binary files /dev/null and b/website/public/apple-icon-180x180.png differ diff --git a/website/public/apple-icon-57x57.png b/website/public/apple-icon-57x57.png new file mode 100755 index 00000000..49feffd5 Binary files /dev/null and b/website/public/apple-icon-57x57.png differ diff --git a/website/public/apple-icon-60x60.png b/website/public/apple-icon-60x60.png new file mode 100755 index 00000000..aad6dc65 Binary files /dev/null and b/website/public/apple-icon-60x60.png differ diff --git a/website/public/apple-icon-72x72.png b/website/public/apple-icon-72x72.png new file mode 100755 index 00000000..d127245e Binary files /dev/null and b/website/public/apple-icon-72x72.png differ diff --git a/website/public/apple-icon-76x76.png b/website/public/apple-icon-76x76.png new file mode 100755 index 00000000..3d902389 Binary files /dev/null and b/website/public/apple-icon-76x76.png differ diff --git a/website/public/apple-icon-precomposed.png b/website/public/apple-icon-precomposed.png new file mode 100755 index 00000000..40454e05 Binary files /dev/null and b/website/public/apple-icon-precomposed.png differ diff --git a/website/public/apple-icon.png b/website/public/apple-icon.png new file mode 100755 index 00000000..40454e05 Binary files /dev/null and b/website/public/apple-icon.png differ diff --git a/website/public/browserconfig.xml b/website/public/browserconfig.xml new file mode 100755 index 00000000..c5541482 --- /dev/null +++ b/website/public/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/website/public/css/_code.less b/website/public/css/_code.less new file mode 100644 index 00000000..adf78a64 --- /dev/null +++ b/website/public/css/_code.less @@ -0,0 +1,53 @@ +// 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; +} diff --git a/website/public/css/_colors.less b/website/public/css/_colors.less new file mode 100644 index 00000000..04fa3e1e --- /dev/null +++ b/website/public/css/_colors.less @@ -0,0 +1,50 @@ +@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; +} diff --git a/website/public/css/_fonts.less b/website/public/css/_fonts.less new file mode 100644 index 00000000..177635de --- /dev/null +++ b/website/public/css/_fonts.less @@ -0,0 +1,42 @@ +@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; + } +} diff --git a/website/public/css/_footer.less b/website/public/css/_footer.less new file mode 100644 index 00000000..7181131e --- /dev/null +++ b/website/public/css/_footer.less @@ -0,0 +1,28 @@ +@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%; + } +} diff --git a/website/public/css/_layout.less b/website/public/css/_layout.less new file mode 100644 index 00000000..d1fe0402 --- /dev/null +++ b/website/public/css/_layout.less @@ -0,0 +1,49 @@ +@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; + } +} diff --git a/website/public/css/_misc.less b/website/public/css/_misc.less new file mode 100644 index 00000000..c2816960 --- /dev/null +++ b/website/public/css/_misc.less @@ -0,0 +1,47 @@ +// 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; + } +} diff --git a/website/public/css/_mixins.less b/website/public/css/_mixins.less new file mode 100644 index 00000000..b65e40bd --- /dev/null +++ b/website/public/css/_mixins.less @@ -0,0 +1,8 @@ +// Overwrite bootstrap's default +.override-container(@width: 970px) { + @media (min-width: 1200px) { + & { + width: @width; + } + } +} diff --git a/website/public/css/_navbar.less b/website/public/css/_navbar.less new file mode 100644 index 00000000..7e031f8a --- /dev/null +++ b/website/public/css/_navbar.less @@ -0,0 +1,35 @@ +.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; + } + } +} diff --git a/website/public/css/_roboto-fonts.less b/website/public/css/_roboto-fonts.less new file mode 100644 index 00000000..72f4f47e --- /dev/null +++ b/website/public/css/_roboto-fonts.less @@ -0,0 +1,34 @@ +@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); diff --git a/website/public/css/_sidebar.less b/website/public/css/_sidebar.less new file mode 100644 index 00000000..cdb2d354 --- /dev/null +++ b/website/public/css/_sidebar.less @@ -0,0 +1,72 @@ +@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%; + } + } +} + diff --git a/website/public/css/_site-top.less b/website/public/css/_site-top.less new file mode 100644 index 00000000..2124ace3 --- /dev/null +++ b/website/public/css/_site-top.less @@ -0,0 +1,75 @@ +@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; + } + } + } + } +} diff --git a/website/public/css/main.less b/website/public/css/main.less new file mode 100644 index 00000000..9c7a8ebe --- /dev/null +++ b/website/public/css/main.less @@ -0,0 +1,11 @@ +@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'; diff --git a/website/public/favicon-16x16.png b/website/public/favicon-16x16.png new file mode 100755 index 00000000..cb5c8f81 Binary files /dev/null and b/website/public/favicon-16x16.png differ diff --git a/website/public/favicon-32x32.png b/website/public/favicon-32x32.png new file mode 100755 index 00000000..bd8e204d Binary files /dev/null and b/website/public/favicon-32x32.png differ diff --git a/website/public/favicon-96x96.png b/website/public/favicon-96x96.png new file mode 100755 index 00000000..0afa873a Binary files /dev/null and b/website/public/favicon-96x96.png differ diff --git a/website/public/favicon.ico b/website/public/favicon.ico new file mode 100755 index 00000000..f2f510d1 Binary files /dev/null and b/website/public/favicon.ico differ diff --git a/website/public/images/logo.svg b/website/public/images/logo.svg new file mode 100644 index 00000000..3ee47e47 --- /dev/null +++ b/website/public/images/logo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/public/index.ejs b/website/public/index.ejs new file mode 100644 index 00000000..78a9603e --- /dev/null +++ b/website/public/index.ejs @@ -0,0 +1,46 @@ + + + +<%- partial("_head") %> + + + + + +
+
+

<%- title %>

+
+
+ +
+
+ +
+<%- partial('../../README') %> +
+
+
+ +<%- partial("_footer") %> + + diff --git a/website/public/js/main.coffee b/website/public/js/main.coffee new file mode 100644 index 00000000..21f983c5 --- /dev/null +++ b/website/public/js/main.coffee @@ -0,0 +1,43 @@ +# 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() + '#') diff --git a/website/public/manifest.json b/website/public/manifest.json new file mode 100755 index 00000000..013d4a6a --- /dev/null +++ b/website/public/manifest.json @@ -0,0 +1,41 @@ +{ + "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" + } + ] +} \ No newline at end of file diff --git a/website/public/ms-icon-144x144.png b/website/public/ms-icon-144x144.png new file mode 100755 index 00000000..504f74db Binary files /dev/null and b/website/public/ms-icon-144x144.png differ diff --git a/website/public/ms-icon-150x150.png b/website/public/ms-icon-150x150.png new file mode 100755 index 00000000..83eb4c9e Binary files /dev/null and b/website/public/ms-icon-150x150.png differ diff --git a/website/public/ms-icon-310x310.png b/website/public/ms-icon-310x310.png new file mode 100755 index 00000000..140fc141 Binary files /dev/null and b/website/public/ms-icon-310x310.png differ diff --git a/website/public/ms-icon-70x70.png b/website/public/ms-icon-70x70.png new file mode 100755 index 00000000..2beaca27 Binary files /dev/null and b/website/public/ms-icon-70x70.png differ diff --git a/website/public/thumbnail.png b/website/public/thumbnail.png new file mode 100644 index 00000000..4cd0e772 Binary files /dev/null and b/website/public/thumbnail.png differ diff --git a/wercker.yml b/wercker.yml index 3f6a915f..d4fe44a7 100644 --- a/wercker.yml +++ b/wercker.yml @@ -13,7 +13,9 @@ build: echo $ANDROID_UPDATE_FILTER echo $ANDROID_NDK_HOME - android-sdk-update: - filter: tools,platform-tools,android-21,build-tools-21.1.1,extra-android-support,extra-android-m2repository + 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 @@ -23,6 +25,6 @@ build: - script: name: inspect build result code: | - ls -la ./observablescrollview-samples/build/outputs/apk - cp ./observablescrollview-samples/build/outputs/apk/*.apk $WERCKER_REPORT_ARTIFACTS_DIR + ls -la ./samples/build/outputs/apk + cp ./samples/build/outputs/apk/*.apk $WERCKER_REPORT_ARTIFACTS_DIR rm -f $WERCKER_REPORT_ARTIFACTS_DIR/*-unaligned.apk