In my Android project, I have a ListView with rows containing SwitchCompat items (AppCompat for Switch widget).
My problem occurs when I scroll into the list and getView(...) method of MyAdapter is invoked with a recycled view. I redefine the correct Switch state but the animation is visible.
There is a solution to prevent the animation in this case?
Call jumpDrawablesToCurrentState() to skip the animation
switchCompat.setChecked(true);
switchCompat.jumpDrawablesToCurrentState();
I finally found a solution but seems not really clean:
ViewGroup viewGroup = (ViewGroup) view; // the recycled view
viewGroup.removeView(switch);
switch.setChecked(states[index]);
viewGroup.addView(switch);
If a better solution exists, please share it.
The issue in with animation playing in the list can be present if you use Android Databinding.
To resolve it, run binding.executePendingBindings() method after you set data – it will refresh binding state for the component in current frame and will not wait for the next one to come.
As you have probably guessed already – next frame is the animation
I had the same problem and I managed to solved it using some minimal reflection.
Usage:
To change the switch state without animation call the setChecked(boolean checked, boolean animate) method with false for the animate parameter. If the switch already is animating at the moment this method is being called the animation will be stopped and the switch jumps to the desired position.
SwitchCompatFix.java
import android.content.Context;
import android.support.v7.widget.SwitchCompat;
import android.util.AttributeSet;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Work around for: http://stackoverflow.com/questions/27139262/change-switch-state-without-animation
* Possible fix for bug 101107: https://code.google.com/p/android/issues/detail?id=101107
*
* Version 0.2
* #author Rolf Smit
*/
public class SwitchCompatFix extends SwitchCompat {
public SwitchCompatFix(Context context) {
super(context);
initHack();
}
public SwitchCompatFix(Context context, AttributeSet attrs) {
super(context, attrs);
initHack();
}
public SwitchCompatFix(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHack();
}
private Method methodCancelPositionAnimator = null;
private Method methodSetThumbPosition = null;
private void initHack(){
try {
methodCancelPositionAnimator = SwitchCompat.class.getDeclaredMethod("cancelPositionAnimator");
methodSetThumbPosition = SwitchCompat.class.getDeclaredMethod("setThumbPosition", float.class);
methodCancelPositionAnimator.setAccessible(true);
methodSetThumbPosition.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public void setChecked(boolean checked, boolean animate){
// Java does not support super.super.xxx calls, a call to the SwitchCompat default setChecked method is needed.
super.setChecked(checked);
if(!animate) {
// See original SwitchCompat source:
// Calling the super method may result in setChecked() getting called
// recursively with a different value, so load the REAL value...
checked = isChecked();
// Cancel any running animations (started by super.setChecked()) and immediately move the thumb to the new position
try {
if(methodCancelPositionAnimator != null && methodSetThumbPosition != null) {
methodCancelPositionAnimator.invoke(this);
methodSetThumbPosition.invoke(this, checked ? 1 : 0);
}
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
Note for proguard users:
Because this method uses reflection an additional proguard rule might be needed (if not yet present).
-keep class android.support.v7.widget.SwitchCompat {
private void cancelPositionAnimator();
private void setThumbPosition(float);
}
This additional rule is not needed when you're using one of the following proguard rules (or similar ones):
-keep class android.support.v7.widget.** { *; }
-keep class android.support.v7.** { *; }
Using SwitchCompat and DataBinding
#BindingAdapter({"bind:checkedState"})
public static void setCheckedState(SwitchCompat switchView, boolean checked) {
int visibility = switchView.getVisibility();
switchView.setVisibility(View.INVISIBLE);
switchView.setChecked(checked);
switchView.setVisibility(visibility);
}
Then in xml:
<android.support.v7.widget.SwitchCompat
android:id="#+id/my_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:checkedState="#{my_data.checked}"/>
And don't forget to call executePendingBindings() (thanks AAverin)
For Kotlin developer:
fun SwitchCompat.setCheckedWithoutAnimation(checked: Boolean) {
val beforeVisibility = visibility
visibility = View.INVISIBLE
isChecked = checked
visibility = beforeVisibility
}
And the usage:
mySwitch.setCheckedWithoutAnimation(true)
Kotlin sample of Rolf ツ answer.
class SwitchImproved(context: Context, attributeSet: AttributeSet) : SwitchCompat(context, attributeSet) {
private lateinit var methodCancelPositionAnimator: Method
private lateinit var methodSetThumbPosition: Method
init {
initHack()
}
fun setChecked(checked: Boolean, animate: Boolean = true) {
super.setChecked(checked)
if (!animate) {
methodCancelPositionAnimator.invoke(this)
methodSetThumbPosition.invoke(this, if (isChecked) 1 else 0)
}
}
private fun initHack() {
methodCancelPositionAnimator = SwitchCompat::class.java.getDeclaredMethod("cancelPositionAnimator")
methodSetThumbPosition = SwitchCompat::class.java.getDeclaredMethod("setThumbPosition", Float::class.javaPrimitiveType)
methodCancelPositionAnimator.isAccessible = true
methodSetThumbPosition.isAccessible = true
}
}
In my case, I am using the new material library:
implementation 'com.google.android.material:material:1.1.0-alpha07'
and in the setChecked method of this class there is this condition:
if (getWindowToken() != null && ViewCompat.isLaidOut(this))
So what I did was to create a class that extends from this SwitchMaterial, and deal with "isLaidOut". The code is the next one (omitting constructors):
class SwitchCustomView : SwitchMaterial {
private var laidOutForAnimation = false
fun setChecked(checked: Boolean, animate: Boolean) {
if (!animate) {
laidOutForAnimation = true
}
super.setChecked(checked)
laidOutForAnimation = false
}
override fun isLaidOut(): Boolean {
return if (laidOutForAnimation) {
return false
} else {
super.isLaidOut()
}
}
}
Then just use this class in your xml and call programatically
setChecked(checked: Boolean, animate: Boolean)
Related
I have an EditText that I overrode in order to detect clicks on the compound drawables.
class MyEditText
constructor(
context: Context,
attrs: AttributeSet
) : androidx.appcompat.widget.AppCompatEditText( context, attrs) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
val result = super.onTouchEvent(event)
if (event != null) {
// All advice says to use ACTION_UP but that never gets here
if( event.action == MotionEvent.ACTION_DOWN ){
if(event.x <= this.totalPaddingStart){
onStartDrawableClick()
}
if(event.x >= this.width - this.totalPaddingEnd){
onEndDrawableClick()
}
performClick()
return true
}
}
return result
}
// Overriding this avoids an accessibility warning
override fun performClick(): Boolean {
return super.performClick()
}
public fun onEndDrawableClick(){
Log.e(TAG, "onEndDrawableClick: ")
}
public fun onStartDrawableClick(){
Log.e(TAG, "onStartDrawableClick: ")
}
}
This works but I want to be able to define what happens in onEndDrawableClick() from the MyEditText object instance, not in the class. I cant pass a closure to the constructor since its a view with params for xml instantiation. Is there an elegant way to do this?
(Extra bonus points for figuring out why ACTION_UP is never seen)
You can define callback properties that can be set from outside the class. Also, you can make the MotionEvent parameter non-nullable since the Android function will never pass you a null value. Then you don't have to do the null check.
Also, if you don't want other click events to happen (like if you set a click listener on the TextView) when you click on this item, you should not call super when the icon is clicked. And you should call through when the touch misses the icon instead of returning true. Example of rearranging the logic like this below.
class MyEditText(
context: Context,
attrs: AttributeSet
) : androidx.appcompat.widget.AppCompatEditText(context, attrs) {
var onEndDrawableClick: (()->Unit)? = null
var onStartDrawableClick: (()->Unit)? = null
override fun onTouchEvent(event: MotionEvent): Boolean {
if( event.action == MotionEvent.ACTION_DOWN ){
if(event.x <= this.totalPaddingStart){
onStartDrawableClick?.invoke()
performClick()
return true
}
if(event.x >= this.width - this.totalPaddingEnd){
onEndDrawableClick?.invoke()
performClick()
return true
}
}
return super.onTouchEvent(event)
}
// Overriding this avoids an accessibility warning
override fun performClick(): Boolean {
return super.performClick()
}
}
Handling a click that behaves as users are probably accustomed, i.e. has visual/audible feedback on touch down, is cancellable by moving your finger off the target before releasing, and firing only if released over the touch target; is not trivial. You might want to build this UI component out of a RelativeLayout or ConstraintLayout that contains an EditText and two Buttons in it to provide a nicer experience.
Apologies for my rusty Kotlin, but something like this should work:
var endClickHandler: View.OnClickListener?
public fun onEndDrawableClick(){
Log.e(TAG, "onEndDrawableClick: ")
endClickHandler?.onClick(this)
}
Core version: androidx.core:core:1.0.1
Is there any proper way for creating WindowInsetsCompat's instance?
As I see, it has the private constructor:
private WindowInsetsCompat(Object insets) {
mInsets = insets;
}
It's called from 5 methods and 1 static wrapper, that has package-private visibility:
static WindowInsetsCompat wrap(Object insets) {
return insets == null ? null : new WindowInsetsCompat(insets);
}
That wrap method only used at 6 methods inside ViewCompat, that's all.
So, can we create an instance of WindowInsetsCompat somehow?
Or, the only way is cmd+c/cmd+v?
You have two solutions to create an instance of WindowInsetsCompat.
First solution
You can use reflection to call the private constructor:
package com.example.myapplication;
import androidx.core.view.WindowInsetsCompat;
import java.lang.reflect.Constructor;
public class WindowInsetsCompatHelper {
public static WindowInsetsCompat createWindowInsetsCompat(Object insets) {
try {
Constructor<WindowInsetsCompat> constructor = WindowInsetsCompat.class.getDeclaredConstructor(Object.class);
constructor.setAccessible(true);
return constructor.newInstance(insets);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
Second solution
It requires to create a helper class in the package androidx.core.view. After that you can directly use the wrap method:
package androidx.core.view;
public class WindowInsetsCompatHelper {
public static WindowInsetsCompat createWindowInsetsCompat(Object insets) {
return WindowInsetsCompat.wrap(insets);
}
}
When I copy and paste Code B (Java Code) into Android Studio 3.1.2, I choose to convert to Kotlin code automatically.
So I get the shown Code A in Kotlin, but with the following error. Why?
Why is that error occurring when Android Studio converts the Java code into Kotlin code automatically?
BTW, Code B (Java code) works well.
Error
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter state
at ui.DialogChangePassword.showDialog(DialogChangePassword.kt)
Code A (Kotlin Code)
class DialogChangePassword(context: Context, attrs: AttributeSet) : DialogPreference(context, attrs) {
private var mView: View? = null
init {
dialogLayoutResource = R.layout.item_custom_password_dialog
}
override fun onCreateDialogView(): View? {
// TODO Auto-generated method stub
mView = super.onCreateDialogView()
return mView
}
override fun onDialogClosed(positiveResult: Boolean) {
super.onDialogClosed(positiveResult)
}
override fun showDialog(state: Bundle) {
// Call show on default first so we can
// override the handlers
super.showDialog(state)
val d = dialog as AlertDialog
d.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
if (IsInputOKAndSavePassword()) {
d.dismiss()
}
}
}
private fun IsInputOKAndSavePassword(): Boolean {
return true
}
}
Code B (Java Code)
public class DialogChangePassword extends DialogPreference {
private View mView;
public DialogChangePassword(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.item_custom_password_dialog);
}
#Override
protected View onCreateDialogView() {
// TODO Auto-generated method stub
mView = super.onCreateDialogView();
return mView;
}
#Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
}
#Override
protected void showDialog(Bundle state) {
// Call show on default first so we can
// override the handlers
super.showDialog(state);
final AlertDialog d = (AlertDialog) getDialog();
d.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View v) {
if (IsInputOKAndSavePassword()){
d.dismiss();
}
}
});
}
private boolean IsInputOKAndSavePassword(){
boolean result=true;
return result;
}
}
Kotlin treats null as a first class citizen by distinguishing nullable types (e.g. Bundle?) vs non-nullable types (e.g. Bundle).
as you know, a Bundle instance can be null depending on what phase of the component's lifecycle you're currently in. for example, the first time an Activity instance is created, onCreate() is called with a null Bundle because there is no state to be restored. if that same Activity is re-created due to a configuration change, however, onCreate() can be called with a non-null Bundle instance (in which you may have stored some data to help recreate the associated screen).
given as much, the showDialog signature should be written in such a way that allows for the possibility that state could possibly be null, like so:
override fun showDialog(state: Bundle?) {
...
}
hope that helps!
I am trying to bind the text color of a TextView in Android. Here is my (truncated) xaml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:MvxBind=" TextColor CategoryTextColor(Category)"/>
where CategoryTextColorValueConverter is as follows:
public class CategoryTextColorConverter : MvxValueConverter<ShowCategory, Color>
{
protected override Color Convert (ShowCategory value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == ShowCategory.AllShows)
{
return Color.Blue;
}
return Color.Red;
}
}
The converter is getting called and returns a colour as expected, but the text colour never changes on the TextView. I have a similar binding for the background colour that works fine. I've seen here In MvvmCross how do I do custom bind properties that maybe I need to create a custom binding, but I cannot find MvxBaseAndroidTargetBinding. Perhaps I need to install a separate package from nuget?
The only thing you have to do, is installing the MvvMCross Color Plugin, because TextColor comes with it. It is not included in Core. Your posted solution works.
See: https://github.com/MvvmCross/MvvmCross/wiki/MvvmCross-plugins#color
Tip: You don't have to write platform specific ValueConvertes, if you use MvxColorValueConverter<ShowCategory>, you can share it across different platforms.
public class CategoryTextColorConverter : MvxColorValueConverter<ShowCategory>
{
protected override MvxColor Convert(ShowCategory value, object parameter, CultureInfo culture)
{
if (value == ShowCategory.AllShows)
{
return MvxColors.Blue;
}
return MvxColors.Red;
}
}
The MVVMCross bindings work by binding to an object's properties and while android:textColor works fine in Android XML it is a shortcut to the object's underlying method, SetTextColor which you can't bind to directly. What you can do is create a class that extends the TextView and have a bindable TextColor property and then use that in your Android XML and bind to that. For example:
public class ExtendedTextView : TextView
{
public ExtendedTextView(Context context): base (context) { }
public ExtendedTextView(Context context, IAttributeSet attrs) : base (context, attrs) { }
public ExtendedTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) { }
public ExtendedTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base (context, attrs, defStyleAttr, defStyleRes) { }
protected ExtendedTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { }
public Color BindableTextColor // property to bind to in XML
{
get { return new Color(CurrentTextColor); }
set { SetTextColor(value);}
}
}
Thanks for the input all - ideally I would have used #sven-michael's cross-platform solution but I ended up implementing a custom binding in Android.
My code is below. Note that MvxBaseAndroidTargetBinding is now called MvxAndroidTargetBinding.
public class TextColorBinding : MvxAndroidTargetBinding {
private readonly TextView _textView;
public TextColorBinding(TextView textView) : base(textView)
{
_textView = textView;
}
public static void Register(IMvxTargetBindingFactoryRegistry registry) {
registry.RegisterFactory(new MvxCustomBindingFactory<TextView>("TextColor", (textView) => new TextColorBinding(textView)));
}
#region implemented abstract members of MvxTargetBinding
public override Type TargetType
{
get { return typeof(Color); }
}
#endregion
#region implemented abstract members of MvxConvertingTargetBinding
protected override void SetValueImpl (object target, object value)
{
var color = (Color)value;
if (color != null) {
var textView = (TextView)target;
try {
textView.SetTextColor (color);
} catch (Exception ex) {
MvxTrace.Error (ex.ToLongString ());
throw;
}
} else {
MvxBindingTrace.Trace (MvxTraceLevel.Warning, "Value was not a valid Color");
}
}
#endregion
}
Don't forget to register the binding in your Setup.cs as follows too:
protected override void FillTargetFactories(MvvmCross.Binding.Bindings.Target.Construction.IMvxTargetBindingFactoryRegistry registry)
{
TextColorBinding.Register (registry);
base.FillTargetFactories(registry);
}
For this purpose, color adjustment and other uses can use the following code sample:
Android.Graphics.Color color = new Android.Graphics.Color(element.CurrentTextColor);
Would anyone know how to test for the appearance of a Toast message in android espresso? In robotium its easy & I used but started working in espresso but dont getting the exact command.
This slightly long statement works for me:
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
....
onView(withText(R.string.TOAST_STRING)).inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView())))).check(matches(isDisplayed()));
The accepted answer is a good one but didn't work for me. So I searched a bit and found this blog article.
This gave me an idea of how to do it and I updated the solution above.
First I implemented the ToastMatcher:
import android.os.IBinder;
import android.support.test.espresso.Root;
import android.view.WindowManager;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
public class ToastMatcher extends TypeSafeMatcher<Root> {
#Override
public void describeTo(Description description) {
description.appendText("is toast");
}
#Override
public boolean matchesSafely(Root root) {
int type = root.getWindowLayoutParams().get().type;
if (type == WindowManager.LayoutParams.TYPE_TOAST) {
IBinder windowToken = root.getDecorView().getWindowToken();
IBinder appToken = root.getDecorView().getApplicationWindowToken();
if (windowToken == appToken) {
// windowToken == appToken means this window isn't contained by any other windows.
// if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
return true;
}
}
return false;
}
}
Then I implemented my check methods like this:
public void isToastMessageDisplayed(int textId) {
onView(withText(textId)).inRoot(MobileViewMatchers.isToast()).check(matches(isDisplayed()));
}
MobileViewMatchers is a container for accessing the matchers. There I defined the static method isToast().
public static Matcher<Root> isToast() {
return new ToastMatcher();
}
This works like a charm for me.
First make sure to import:
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
Inside your class you probably have a rule like this:
#Rule
public ActivityTestRule<MyNameActivity> activityTestRule =
new ActivityTestRule<>(MyNameActivity.class);
Inside your test:
MyNameActivity activity = activityTestRule.getActivity();
onView(withText(R.string.toast_text)).
inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))).
check(matches(isDisplayed()));
This worked for me, and it was pretty easy to use.
If you're using the newest Android Testing Tools from Jetpack, you know, that ActivityTestRule is deprecated and you should use ActivityScenario or ActivityScenarioRule(which contains the first).
Prerequisites. Create decorView variable and assign it before tests;
#Rule
public ActivityScenarioRule<FeedActivity> activityScenarioRule = new ActivityScenarioRule<>(FeedActivity.class);
private View decorView;
#Before
public void setUp() {
activityScenarioRule.getScenario().onActivity(new ActivityScenario.ActivityAction<FeedActivity>() {
#Override
public void perform(FeedActivityactivity activity) {
decorView = activity.getWindow().getDecorView();
}
});
}
Test itself
#Test
public void given_when_thenShouldShowToast() {
String expectedWarning = getApplicationContext().getString(R.string.error_empty_list);
onView(withId(R.id.button))
.perform(click());
onView(withText(expectedWarning))
.inRoot(withDecorView(not(decorView)))// Here we use decorView
.check(matches(isDisplayed()));
}
getApplicationContext() can be taken from androidx.test.core.app.ApplicationProvider.getApplicationContext;
First create a cutom Toast Matcher which we can use in our test cases -
public class ToastMatcher extends TypeSafeMatcher<Root> {
#Override public void describeTo(Description description) {
description.appendText("is toast");
}
#Override public boolean matchesSafely(Root root) {
int type = root.getWindowLayoutParams().get().type;
if ((type == WindowManager.LayoutParams.TYPE_TOAST)) {
IBinder windowToken = root.getDecorView().getWindowToken();
IBinder appToken = root.getDecorView().getApplicationWindowToken();
if (windowToken == appToken) {
//means this window isn't contained by any other windows.
return true;
}
}
return false;
}
}
1. Test if the Toast Message is Displayed
onView(withText(R.string.mssage)).inRoot(new ToastMatcher())
.check(matches(isDisplayed()));
2. Test if the Toast Message is not Displayed
onView(withText(R.string.mssage)).inRoot(new ToastMatcher())
.check(matches(not(isDisplayed())));
3. Test id the Toast contains specific Text Message
onView(withText(R.string.mssage)).inRoot(new ToastMatcher())
.check(matches(withText("Invalid Name"));
Thanks,
Anuja
Note - this answer is from This POST.
Though the question has an accepted answer - which BTW does not work for me - I'd like to add my solution in Kotlin which I derived from Thomas R.'s answer:
package somepkg
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.Root
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
import android.view.WindowManager.LayoutParams.TYPE_TOAST
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
/**
* This class allows to match Toast messages in tests with Espresso.
*
* Idea taken from: https://stackoverflow.com/a/33387980
*
* Usage in test class:
*
* import somepkg.ToastMatcher.Companion.onToast
*
* // To assert a toast does *not* pop up:
* onToast("text").check(doesNotExist())
* onToast(textId).check(doesNotExist())
*
* // To assert a toast does pop up:
* onToast("text").check(matches(isDisplayed()))
* onToast(textId).check(matches(isDisplayed()))
*/
class ToastMatcher(private val maxFailures: Int = DEFAULT_MAX_FAILURES) : TypeSafeMatcher<Root>() {
/** Restrict number of false results from matchesSafely to avoid endless loop */
private var failures = 0
override fun describeTo(description: Description) {
description.appendText("is toast")
}
public override fun matchesSafely(root: Root): Boolean {
val type = root.windowLayoutParams.get().type
#Suppress("DEPRECATION") // TYPE_TOAST is deprecated in favor of TYPE_APPLICATION_OVERLAY
if (type == TYPE_TOAST || type == TYPE_APPLICATION_OVERLAY) {
val windowToken = root.decorView.windowToken
val appToken = root.decorView.applicationWindowToken
if (windowToken === appToken) {
// windowToken == appToken means this window isn't contained by any other windows.
// if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
return true
}
}
// Method is called again if false is returned which is useful because a toast may take some time to pop up. But for
// obvious reasons an infinite wait isn't of help. So false is only returned as often as maxFailures specifies.
return (++failures >= maxFailures)
}
companion object {
/** Default for maximum number of retries to wait for the toast to pop up */
private const val DEFAULT_MAX_FAILURES = 5
fun onToast(text: String, maxRetries: Int = DEFAULT_MAX_FAILURES) = onView(withText(text)).inRoot(isToast(maxRetries))!!
fun onToast(textId: Int, maxRetries: Int = DEFAULT_MAX_FAILURES) = onView(withText(textId)).inRoot(isToast(maxRetries))!!
fun isToast(maxRetries: Int = DEFAULT_MAX_FAILURES): Matcher<Root> {
return ToastMatcher(maxRetries)
}
}
}
I hope this will be of help for later readers - the usage is described in the comment.
I write my custom toast matcher:
import android.view.WindowManager
import androidx.test.espresso.Root
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
class ToastMatcher : TypeSafeMatcher<Root>() {
override fun describeTo(description: Description) {
description.appendText("is toast")
}
override fun matchesSafely(root: Root): Boolean {
val type = root.getWindowLayoutParams().get().type
if (type == WindowManager.LayoutParams.TYPE_TOAST) {
val windowToken = root.getDecorView().getWindowToken()
val appToken = root.getDecorView().getApplicationWindowToken()
if (windowToken === appToken) {
return true
}
}
return false
}
}
And use like this:
onView(withText(R.string.please_input_all_fields)).inRoot(ToastMatcher()).check(matches(isDisplayed()))
For kotlin, I had to use the apply extension function, and this worked for me.
1- declare your ToastMatcher class in the androidTest folder:
class ToastMatcher : TypeSafeMatcher<Root?>() {
override fun matchesSafely(item: Root?): Boolean {
val type: Int? = item?.windowLayoutParams?.get()?.type
if (type == WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW) {
val windowToken: IBinder = item.decorView.windowToken
val appToken: IBinder = item.decorView.applicationWindowToken
if (windowToken === appToken) { // means this window isn't contained by any other windows.
return true
}
}
return false
}
override fun describeTo(description: Description?) {
description?.appendText("is toast")
}
}
2- Then you use like this to test that the toast message actually displays
onView(withText(R.string.invalid_phone_number))
.inRoot(ToastMatcher().apply {
matches(isDisplayed())
});
Attribution to ToastMatcher class:
/**
* Author: http://www.qaautomated.com/2016/01/how-to-test-toast-message-using-espresso.html
*/
I would say for toast messages first define your rule
#Rule
public ActivityTestRule<AuthActivity> activityTestRule =
new ActivityTestRule<>(AuthActivity.class);
then whatever toast message text you are looking for type it in between quotation
for example I used "Invalid email address"
onView(withText("Invalid email address"))
.inRoot(withDecorView(not(activityTestRule.getActivity().getWindow().getDecorView())))
.check(matches(isDisplayed()));
I would like to suggest an alternative method, especially if you need to check that particular toast is NOT displayed
The problem here that
onView(viewMatcher)
.inRoot(RootMatchers.isPlatformPopup())
.check(matches(not(isDisplayed())))
or
onView(viewMatcher)
.inRoot(RootMatchers.isPlatformPopup())
.check(doesNotExist())
or any other custom inRoot checks
are throwing NoMatchingRootException even before the code passes to check method
You may just catch the exception and complete the test but that's not a good option since throwing and catching NoMatchingRootException consumes a lot of time in a comparison with the default test case. Seems that Espresso is waiting for the Root for a while
For this case is suggest just to give up with espresso here and use UiAutomator for this assertion. The Espresso and UiAutomator frameworks could easily work together in one environment.
val device: UiDevice
get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun assertPopupIsNotDisplayed() {
device.waitForIdle()
assertFalse(device.hasObject(By.text(yourText))))
}
fun assertPopupIsDisplayed() {
device.waitForIdle()
assertTrue(device.hasObject(By.text(yourText))))
}
Using ActivityScenarioRule and Java
Some imports for the code
import android.view.View;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
1. Declare the rule
//Change YourActivity by the activity you are testing
#Rule
public ActivityScenarioRule<YourActivity> activityRule
= new ActivityScenarioRule<>(YourActivity.class);
2. Initialize the decor view
private View decorView;
#Before
public void loadDecorView() {
activityRule.getScenario().onActivity(
activity -> decorView = activity.getWindow().getDecorView()
);
}
3. Finally test it
#Test
public void testWithToasts() {
//Arrange and act code
//Modify toast_msg to your own string resource
onView(withText(R.string.toast_msg)).
inRoot(RootMatchers.withDecorView(not(decorView)))
.check(matches(isDisplayed()));
}
I'm pretty new to this, but I made a base class 'BaseTest' that has all of my actions (swiping, clicking, etc.) and verifications (checking text views for content, etc.).
protected fun verifyToastMessageWithText(text: String, activityTestRule: ActivityTestRule<*>) {
onView(withText(text)).inRoot(withDecorView(not(activityTestRule.activity.window.decorView))).check(matches(isDisplayed()))
}
protected fun verifyToastMessageWithStringResource(id: Int, activityTestRule: ActivityTestRule<*>) {
onView(withText(id)).inRoot(withDecorView(not(activityTestRule.activity.window.decorView))).check(matches(isDisplayed()))
}
this works for me
onView(withId(R.id.inputField)).check(matches(withText("Lalala")));
The way Toasts are implemented makes it possible to detect a toast has been displayed. However there is no way to see if a Toast has been requested, thru a call to show()) or to block between the period of time between show() and when the toast has become visible. This is opens up unresolvable timing issues (that you can only address thru sleep & hope).
If you really really want to verify this, here's a not-so-pretty alternative using Mockito and a test spy:
public interface Toaster {
public void showToast(Toast t);
private static class RealToaster {
#Override
public void showToast(Toast t) {
t.show();
}
public static Toaster makeToaster() {
return new RealToaster();
}
}
Then in your test
public void testMyThing() {
Toaster spyToaster = Mockito.spy(Toaster.makeToaster());
getActivity().setToaster(spyToaster);
onView(withId(R.button)).perform(click());
getInstrumentation().runOnMainSync(new Runnable() {
#Override
public void run() {
// must do this on the main thread because the matcher will be interrogating a view...
Mockito.verify(spyToaster).showToast(allOf(withDuration(Toast.LENGTH_SHORT), withView(withText("hello world"));
});
}
// create a matcher that calls getDuration() on the toast object
Matcher<Toast> withDuration(int)
// create a matcher that calls getView() and applies the given view matcher
Matcher<Toast> withView(Matcher<View> viewMatcher)
another answer regarding this
if(someToast == null)
someToast = Toast.makeText(this, "sdfdsf", Toast.LENGTH_LONG);
boolean isShown = someToast.getView().isShown();