Skip to content

Add Snackbar action observables #416

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package com.jakewharton.rxbinding2.support.design.widget

import android.support.design.widget.Snackbar
import android.view.View
import io.reactivex.Observable
import kotlin.Int
import kotlin.Suppress
Expand All @@ -16,3 +17,19 @@ import kotlin.Suppress
* to free this reference.
*/
inline fun Snackbar.dismisses(): Observable<Int> = RxSnackbar.dismisses(this)

/**
* Create an observable which emits the action click events from `view`.
*
* *Warning:* The created observable keeps a strong reference to `view`. Unsubscribe
* to free this reference.
*/
inline fun Snackbar.actionClicks(resId: Int): Observable<View> = RxSnackbar.actionClicks(this, resId)

/**
* Create an observable which emits the action click events from `view`.
*
* *Warning:* The created observable keeps a strong reference to `view`. Unsubscribe
* to free this reference.
*/
inline fun Snackbar.actionClicks(text: CharSequence): Observable<View> = RxSnackbar.actionClicks(this, text)
1 change: 1 addition & 0 deletions rxbinding-design/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
androidTestCompile rootProject.ext.supportTestRunner
androidTestCompile rootProject.ext.supportTestRules
androidTestCompile rootProject.ext.supportTestEspresso
androidTestCompile rootProject.ext.supportTestEspressoContrib
androidTestCompile rootProject.ext.rxAndroid
}

Expand Down
4 changes: 4 additions & 0 deletions rxbinding-design/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
android:name="com.jakewharton.rxbinding2.support.design.widget.RxSwipeDismissBehaviorTestActivity"
android:theme="@style/Theme.AppCompat"
/>
<activity
android:name="com.jakewharton.rxbinding2.support.design.widget.RxSnackbarTestActivity"
android:theme="@style/Theme.AppCompat"
/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,51 @@
import android.content.Context;
import android.support.design.widget.Snackbar;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.contrib.CountingIdlingResource;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.widget.FrameLayout;
import com.jakewharton.rxbinding2.RecordingObserver;
import com.jakewharton.rxbinding2.support.design.R;
import io.reactivex.android.schedulers.AndroidSchedulers;
import org.junit.After;
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 android.support.design.widget.Snackbar.Callback.DISMISS_EVENT_MANUAL;
import static android.support.design.widget.Snackbar.LENGTH_SHORT;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class) public final class RxSnackbarTest {
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
private final Context rawContext = InstrumentationRegistry.getContext();
private final Context context = new ContextThemeWrapper(rawContext, R.style.Theme_AppCompat);
private final FrameLayout parent = new FrameLayout(context);
@Rule public final ActivityTestRule<RxSnackbarTestActivity> activityRule =
new ActivityTestRule<>(RxSnackbarTestActivity.class);
private Snackbar snackbar;
private CountingIdlingResource idler;

@Before public void setUp() {
RxSnackbarTestActivity activity = activityRule.getActivity();
snackbar = activity.snackbar;

idler = new CountingIdlingResource("counting idler");
Espresso.registerIdlingResources(idler);
}

@After public void teardown() {
Espresso.unregisterIdlingResources(idler);
}

@Test public void dismisses() {
final Snackbar view = Snackbar.make(parent, "Hey", LENGTH_SHORT);
Expand Down Expand Up @@ -55,4 +82,47 @@
});
o.assertNoMoreEvents();
}

@Test public void actionClicks() {
Snackbar.Callback callback = new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
idler.decrement();
}
};
snackbar.addCallback(callback);

final String actionText = "Action";

RecordingObserver<View> o = new RecordingObserver<>();
RxSnackbar.actionClicks(snackbar, actionText)
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(o);
o.assertNoMoreEvents(); // No initial value.

show();
onView(withText(actionText)).perform(click());
assertNotNull(o.takeNext());

show();
onView(withText(actionText)).perform(click());
assertNotNull(o.takeNext());

o.dispose();
show();
onView(withText(actionText)).perform(click());
o.assertNoMoreEvents();

snackbar.removeCallback(callback);
}

private void show() {
idler.increment();
instrumentation.runOnMainSync(new Runnable() {
@Override public void run() {
snackbar.show();
}
});
instrumentation.waitForIdleSync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jakewharton.rxbinding2.support.design.widget;

import android.app.Activity;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.widget.FrameLayout;

import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;

public final class RxSnackbarTestActivity extends Activity {

Snackbar snackbar;

@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

FrameLayout parent = new FrameLayout(this);
snackbar = Snackbar.make(parent, "Hey", LENGTH_INDEFINITE);

setContentView(parent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.view.View;
import io.reactivex.Observable;

import static com.jakewharton.rxbinding2.internal.Preconditions.checkNotNull;
Expand All @@ -23,6 +24,31 @@ public static Observable<Integer> dismisses(@NonNull Snackbar view) {
return new SnackbarDismissesObservable(view);
}

/**
* Create an observable which emits the action click events from {@code view}.
* <p>
* <em>Warning:</em> The created observable keeps a strong reference to {@code view}. Unsubscribe
* to free this reference.
*/
@CheckResult @NonNull
public static Observable<View> actionClicks(@NonNull Snackbar view, int resId) {
checkNotNull(view, "view == null");
return new SnackbarActionObservable(view, resId);
}

/**
* Create an observable which emits the action click events from {@code view}.
* <p>
* <em>Warning:</em> The created observable keeps a strong reference to {@code view}. Unsubscribe
* to free this reference.
*/
@CheckResult @NonNull
public static Observable<View> actionClicks(@NonNull Snackbar view, @NonNull CharSequence text) {
checkNotNull(view, "view == null");
checkNotNull(text, "text == null");
return new SnackbarActionObservable(view, text);
}

private RxSnackbar() {
throw new AssertionError("No instances.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.jakewharton.rxbinding2.support.design.widget;

import android.support.design.widget.Snackbar;
import android.view.View;
import android.view.View.OnClickListener;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.MainThreadDisposable;

import static com.jakewharton.rxbinding2.internal.Preconditions.checkMainThread;

final class SnackbarActionObservable extends Observable<View> {
private final Snackbar view;
private final int resId;
private final CharSequence text;

SnackbarActionObservable(Snackbar view, int resId) {
this.view = view;
this.text = null;
this.resId = resId;
}

SnackbarActionObservable(Snackbar view, CharSequence text) {
this.view = view;
this.text = text;
this.resId = -1;
}

@Override protected void subscribeActual(Observer<? super View> observer) {
if (!checkMainThread(observer)) {
return;
}
Listener listener = new Listener(view, observer);
observer.onSubscribe(listener);
if (text == null) {
view.setAction(resId, listener.callback);
} else {
view.setAction(text, listener.callback);
}
}

final class Listener extends MainThreadDisposable {
private final Snackbar snackbar;
private final OnClickListener callback;

Listener(Snackbar snackbar, final Observer<? super View> observer) {
this.snackbar = snackbar;
this.callback = new OnClickListener() {
@Override
public void onClick(View view) {
if (!isDisposed()) {
observer.onNext(view);
}
}
};
}

@Override protected void onDispose() {
// Provide stub OnClickListener implementation
// This is necessary because Snackbar will hide Action text if setAction is called with
// listener set to null
if (text == null) {
snackbar.setAction(resId, new EmptyActionListener());
} else {
snackbar.setAction(text, new EmptyActionListener());
}
}
}

private static final class EmptyActionListener implements OnClickListener {
@Override public void onClick(View v) {
}
}
}