diff --git a/rxbinding-kotlin/src/main/kotlin/com/jakewharton/rxbinding2/view/RxView.kt b/rxbinding-kotlin/src/main/kotlin/com/jakewharton/rxbinding2/view/RxView.kt index f38ef9a4..f64cce20 100644 --- a/rxbinding-kotlin/src/main/kotlin/com/jakewharton/rxbinding2/view/RxView.kt +++ b/rxbinding-kotlin/src/main/kotlin/com/jakewharton/rxbinding2/view/RxView.kt @@ -234,7 +234,7 @@ inline fun View.preDraws(proceedDrawingPass: Callable): Observable = RxView.scrollChangeEvents(this) diff --git a/rxbinding/src/androidTest/AndroidManifest.xml b/rxbinding/src/androidTest/AndroidManifest.xml index c1208b19..d904cdc6 100644 --- a/rxbinding/src/androidTest/AndroidManifest.xml +++ b/rxbinding/src/androidTest/AndroidManifest.xml @@ -5,6 +5,7 @@ + diff --git a/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTest.java b/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTest.java new file mode 100644 index 00000000..943bf579 --- /dev/null +++ b/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTest.java @@ -0,0 +1,80 @@ +package com.jakewharton.rxbinding2.view; + +import android.app.Instrumentation; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; + +import com.jakewharton.rxbinding2.RecordingObserver; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.reactivex.android.schedulers.AndroidSchedulers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +@RunWith(AndroidJUnit4.class) +public final class RxViewScrollTest { + @Rule public final ActivityTestRule activityRule = + new ActivityTestRule<>(RxViewScrollTestActivity.class); + + private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + private View view; + + @Before + public void setUp() { + RxViewScrollTestActivity activity = activityRule.getActivity(); + view = activity.view; + } + + @SdkSuppress(minSdkVersion = 16) + @Test public void scrollChangeEvents() { + RecordingObserver o = new RecordingObserver<>(); + RxView.scrollChangeEvents(view) + .subscribeOn(AndroidSchedulers.mainThread()) + .subscribe(o); + o.assertNoMoreEvents(); + + + instrumentation.runOnMainSync( + new Runnable() { + @Override public void run() { + view.scrollTo(1, 1); + } + }); + ViewScrollChangeEvent event0 = o.takeNext(); + assertSame(view, event0.view()); + assertEquals(1, event0.scrollX()); + assertEquals(1, event0.scrollY()); + assertEquals(0, event0.oldScrollX()); + assertEquals(0, event0.oldScrollY()); + + instrumentation.runOnMainSync( + new Runnable() { + @Override public void run() { + view.scrollTo(2, 2); + } + }); + ViewScrollChangeEvent event1 = o.takeNext(); + assertSame(view, event1.view()); + assertEquals(2, event1.scrollX()); + assertEquals(2, event1.scrollY()); + assertEquals(1, event1.oldScrollX()); + assertEquals(1, event1.oldScrollY()); + + o.dispose(); + instrumentation.runOnMainSync( + new Runnable() { + @Override public void run() { + view.scrollTo(3, 3); + } + }); + o.assertNoMoreEvents(); + } +} diff --git a/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTestActivity.java b/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTestActivity.java new file mode 100644 index 00000000..2066aede --- /dev/null +++ b/rxbinding/src/androidTest/java/com/jakewharton/rxbinding2/view/RxViewScrollTestActivity.java @@ -0,0 +1,17 @@ +package com.jakewharton.rxbinding2.view; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; + +public final class RxViewScrollTestActivity extends Activity { + View view; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + view = new View(this); + + setContentView(view); + } +} + diff --git a/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/RxView.java b/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/RxView.java index 4c168472..ed7886fb 100644 --- a/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/RxView.java +++ b/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/RxView.java @@ -1,5 +1,6 @@ package com.jakewharton.rxbinding2.view; +import android.os.Build; import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; @@ -295,11 +296,15 @@ public static Observable preDraws(@NonNull View view, * Warning: The created observable keeps a strong reference to {@code view}. Unsubscribe * to free this reference. */ - @RequiresApi(23) + @RequiresApi(16) @CheckResult @NonNull public static Observable scrollChangeEvents(@NonNull View view) { checkNotNull(view, "view == null"); - return new ViewScrollChangeEventObservable(view); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return new ViewScrollChangeEventObservable(view); + } else { + return new ViewScrollChangeEventObservableCompat(view); + } } /** diff --git a/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/ViewScrollChangeEventObservableCompat.java b/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/ViewScrollChangeEventObservableCompat.java new file mode 100644 index 00000000..7afe3049 --- /dev/null +++ b/rxbinding/src/main/java/com/jakewharton/rxbinding2/view/ViewScrollChangeEventObservableCompat.java @@ -0,0 +1,79 @@ +package com.jakewharton.rxbinding2.view; + +import android.support.annotation.RequiresApi; +import android.view.View; +import android.view.ViewTreeObserver; + +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.MainThreadDisposable; + +import static com.jakewharton.rxbinding2.internal.Preconditions.checkMainThread; + +@RequiresApi(16) +final class ViewScrollChangeEventObservableCompat extends Observable { + private final View view; + + ViewScrollChangeEventObservableCompat(View view) { + this.view = view; + } + + @Override + protected void subscribeActual(Observer observer) { + if (!checkMainThread(observer)) { + return; + } + Listener listener = new Listener(view, observer); + observer.onSubscribe(listener); + setOnScrollChangeListenerWith(view, listener); + } + + private void setOnScrollChangeListenerWith(final View v, final Listener listener) { + ViewTreeObserver viewTreeObserver = v.getViewTreeObserver(); + viewTreeObserver.addOnScrollChangedListener(listener); + } + + static final class Listener extends MainThreadDisposable implements OnScrollChangeListener, + ViewTreeObserver.OnScrollChangedListener { + private final View view; + private final Observer observer; + private int oldl, oldt; + + Listener(View view, Observer observer) { + this.view = view; + this.observer = observer; + } + + @Override + public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { + if (!isDisposed()) { + observer.onNext(ViewScrollChangeEvent.create(v, scrollX, scrollY, oldScrollX, oldScrollY)); + } + } + + @Override protected void onDispose() { + ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); + viewTreeObserver.removeOnScrollChangedListener(this); + } + + @Override + public void onScrollChanged() { + this.onScrollChange(view, view.getScrollX(), view.getScrollY(), oldl, oldt); + oldl = view.getScrollX(); + oldt = view.getScrollY(); + } + } + + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); + } +}