diff --git a/build.gradle b/build.gradle
index cdfb0477..0b3b3ba1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,8 +23,8 @@ subprojects {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.5.0'
- classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.1.0'
+ classpath 'com.android.tools.build:gradle:2.1.2'
+ classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.6.3'
}
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e7faee01..9b299e92 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Apr 10 15:27:10 PDT 2013
+#Tue Jul 19 18:40:39 KST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/library/build.gradle b/library/build.gradle
index a1bede3c..68ac4924 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -20,7 +20,7 @@ android {
buildTypes {
debug {
- testCoverageEnabled = true
+ testCoverageEnabled = false
}
}
diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml
index d8476e95..d014aea5 100644
--- a/library/src/androidTest/AndroidManifest.xml
+++ b/library/src/androidTest/AndroidManifest.xml
@@ -26,6 +26,7 @@
+
diff --git a/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/NestedScrollViewActivity.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/NestedScrollViewActivity.java
new file mode 100644
index 00000000..9471eff7
--- /dev/null
+++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/NestedScrollViewActivity.java
@@ -0,0 +1,30 @@
+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.ScrollState;
+import com.github.ksoichiro.android.observablescrollview.Scrollable;
+
+public class NestedScrollViewActivity extends Activity implements ObservableScrollViewCallbacks {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_nestedscrollview);
+ ((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/NestedScrollViewActivityTest.java b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/NestedScrollViewActivityTest.java
new file mode 100644
index 00000000..f8f0d71d
--- /dev/null
+++ b/library/src/androidTest/java/com/github/ksoichiro/android/observablescrollview/test/NestedScrollViewActivityTest.java
@@ -0,0 +1,135 @@
+package com.github.ksoichiro.android.observablescrollview.test;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+
+import com.github.ksoichiro.android.observablescrollview.ObservableNestedScrollView;
+import com.github.ksoichiro.android.observablescrollview.ObservableScrollView;
+import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks;
+import com.github.ksoichiro.android.observablescrollview.ScrollState;
+
+public class NestedScrollViewActivityTest extends ActivityInstrumentationTestCase2 {
+
+ private Activity activity;
+ private ObservableNestedScrollView scrollable;
+ private int[] callbackCounter;
+
+ public NestedScrollViewActivityTest() {
+ super(NestedScrollViewActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ setActivityInitialTouchMode(true);
+ activity = getActivity();
+ scrollable = (ObservableNestedScrollView) activity.findViewById(R.id.scrollable);
+ callbackCounter = new int[2];
+ }
+
+ public void testInitialize() throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new ObservableNestedScrollView(activity);
+ new ObservableNestedScrollView(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 = (ObservableNestedScrollView) 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 = (ObservableNestedScrollView) 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/res/layout/activity_nestedscrollview.xml b/library/src/androidTest/res/layout/activity_nestedscrollview.xml
new file mode 100644
index 00000000..7e4d53f1
--- /dev/null
+++ b/library/src/androidTest/res/layout/activity_nestedscrollview.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableNestedScrollView.java b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableNestedScrollView.java
new file mode 100644
index 00000000..dbc4100a
--- /dev/null
+++ b/library/src/main/java/com/github/ksoichiro/android/observablescrollview/ObservableNestedScrollView.java
@@ -0,0 +1,305 @@
+package com.github.ksoichiro.android.observablescrollview;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.widget.NestedScrollView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * NestedScrollView that its scroll position can be observed.
+ */
+public class ObservableNestedScrollView extends NestedScrollView 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 ObservableNestedScrollView(Context context) {
+ super(context);
+ }
+
+ public ObservableNestedScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ObservableNestedScrollView(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];
+ }
+ };
+ }
+}