First I describe the context, i.e. what I'm trying to do
Second I explain you in which cases I encounter the bug and what is it (it's the current behavior)
Third, I explain you the expected behavior
Then I explain you how to reproduce the bug
Finally I show you the minimal and executable code that can lead to the bug
I am using the widget AutoScrollTextView
available between others in the API https://github.com/ronghao/AutoScrollTextView. AutoScrollTextView
is a TextView
that can automatically autoscroll vertically and moreover, if the text is too long, horizontally. So an AutoScrollTextView
can contain at least one text to be automatically scrolled (see the illustration in the GitHub repository).
AutoScrollTextView
in the main activity, I don't see any bug. If I use it in the fragment of the main activity, I think it would be the case too (I didn't test it).AutoScrollTextView
in each of at least two fragments of a ViewPager
(2 instances of this same fragment class are used for the Viewpager
), with at least one text to be automatically scrolled by each AutoScrollTextView
, the app crashes with the following error (NB: this ViewPager
belongs to the main activity): E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.myapplication, PID: 2814 java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.sendAccessibilityEventUnchecked(android.view.accessibility.AccessibilityEvent)' on a null object reference at android.view.ViewRootImpl$SendWindowContentChangedAccessibilityEvent.run(ViewRootImpl.java:9304) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6944) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
The error, however, is difficult to trigger:
As you can see, there isn't any indication of the line that causes this crash.
I have taken the library, and isolated the minimal part of it that lead to this bug.
I have set up the minimal executable app with only the main activity, the ViewPager
, and the fragment (2 instances of this same fragment class are used for the Viewpager
).
I have set up this minimal executable app with the isolated part of the library. You will find the sources below (even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug). Also: with the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.
I have tried to use Android's Timer
instead of an Handler
but the error still occurres.
MarqueeTextView::scrollTo(currentScrollPos, 0);
seems to trigger this error.
Normally, the app of course should not crash. In other words, there shouldn't be any triggered error (especially the one I've quoted above). With the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.
Below, I provide you all the minimal and executable sources you need (even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug). After having created the corresponding files and copy/paste these sources in them, run this minimal and executable app several times. Even if the error will or won't be shown for each run, normally you would see it at least one time after 5 runs max. You can use your smartphone to test this app, or maybe an Android Studio's emulator (but I didn't test on an emulator).
NB: with the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.
Could you tell me why this bug occures (I was unable to debug it) and why it seems so random? How could I correct it?
First, I will give you the sources of the isolated part of the library
Then, I will give you the sources of the app with the ViewPager
, etc.
Even if there are "many" files, each one has been minimalized and contains only the strict minimal lines in order to make you able to reproduce the bug. With the sources I give you, it's totally normal that you don't see text scrolling (nor text at all). It's because I've completely isolated the part of the API that triggers the bug, and the scrolling text wasn't related to this error, so it's not part of my isolated sources.
package com.example.myapplication.libs.autoscrolling_text_view;
import android.content.Context;
import androidx.appcompat.widget.AppCompatTextView;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
*
* @author haohao on 2017/9/21 下午 02:33
* @version v1.0
*/
public class MarqueeTextView extends AppCompatTextView {
private int currentScrollPos = 0;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
final TimerTask task = new TimerTask() {
@Override
public void run() {
currentScrollPos += 1;
scrollTo(currentScrollPos, 0);
}
};
public MarqueeTextView(Context context) {
super(context);
}
public void postStartScroll(int delay) { // Bug when reset AND then scheduleAtFixedRate are executed
int speed = 6;
pool.scheduleAtFixedRate(task, delay, speed, TimeUnit.MILLISECONDS);
}
}
package com.example.myapplication.libs.autoscrolling_text_view;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import android.widget.ViewSwitcher;
/**
* MarqueeSwitcher {@link android.widget.TextSwitcher} t
*
* @author haohao on 2017/9/21 下午 03:57
* @version v1.0
*/
public class MarqueeSwitcher extends ViewSwitcher {
/**
* Creates a new empty TextSwitcher for the given context and with the
* specified set attributes.
*
* @param context the application environment
* @param attrs a collection of attributes
*/
public MarqueeSwitcher(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Sets the text of the next view and switches to the next view. This can
* be used to animate the old text out and animate the next text in.
*
*/
public void setText() {
final MarqueeTextView t = getNextView();
t.postStartScroll(1500); // BUG HERE (Cf. MarqueeTextView::postStartScroll)
}
public MarqueeTextView getCurrentView() {
return (MarqueeTextView) ((RelativeLayout) super.getCurrentView()).getChildAt(0);
}
public MarqueeTextView getNextView() {
return (MarqueeTextView) ((RelativeLayout) super.getNextView()).getChildAt(0);
}
}
package com.example.myapplication.libs.autoscrolling_text_view;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ViewSwitcher;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 父类:上下滚动
*
* @author haohao on 2017/9/21 下午 02:28
* @version v1.0
*/
public class BaseScrollTextView extends MarqueeSwitcher
implements ViewSwitcher.ViewFactory {
private static final int FLAG_START_AUTO_SCROLL = 1000;
private ArrayList<String> textList;
private Handler handler;
public BaseScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
textList = new ArrayList<>();
handler = new MyHandler(this);
setFactory(this);
}
/**
* 设置数据源
*/
public void setTextList(List<String> titles) {
textList.addAll(titles);
}
/**
* 开始轮播
*/
public void startAutoScroll() {
handler.sendEmptyMessage(FLAG_START_AUTO_SCROLL);
}
@Override
public View makeView() {
RelativeLayout layout = new RelativeLayout(getContext());
MarqueeTextView textView = new MarqueeTextView(getContext());
layout.addView(textView);
return layout;
}
private static class MyHandler extends Handler {
WeakReference<BaseScrollTextView> textViewWeakReference;
private MyHandler(BaseScrollTextView autoScrollTextView) {
textViewWeakReference = new WeakReference<>(autoScrollTextView);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (null != textViewWeakReference) {
BaseScrollTextView autoScrollTextView = textViewWeakReference.get();
if (msg.what == FLAG_START_AUTO_SCROLL) {
if (autoScrollTextView.textList.size() > 0) {
autoScrollTextView.setText();
}
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false"
android:zAdjustment="top">
<translate
android:duration="400"
android:fromYDelta="100%"
android:interpolator="@android:anim/accelerate_interpolator"
android:toYDelta="0"/>
<alpha
android:duration="400"
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
</set>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false"
android:zAdjustment="bottom">
<translate
android:duration="400"
android:fromYDelta="0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toYDelta="-100%"/>
<alpha
android:duration="400"
android:fromAlpha="1.0"
android:toAlpha="0.0"/>
</set>
Viewpager
; the fragment uses AutoScrollTextView
which randomly triggers the bug)apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 22
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation("com.google.guava:guava:28.2-android")
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
package com.example.myapplication;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.common.collect.Lists;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<FragmentHomeSlide> slides;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FragmentHomeSlide slide_1 = new FragmentHomeSlide();
final FragmentHomeSlide slide_2 = new FragmentHomeSlide();
slides = Lists.newArrayList(slide_1, slide_2);
final ViewPager view_pager = findViewById(R.id.view_pager);
assert getFragmentManager() != null;
view_pager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager(), FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
@Override
@NonNull
public Fragment getItem(final int position) {
return slides.get(position);
}
@Override
public int getCount() {
return slides.size();
}
});
}
}
package com.example.myapplication;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.example.myapplication.libs.autoscrolling_text_view.BaseScrollTextView;
import java.util.ArrayList;
import java.util.Arrays;
public class FragmentHomeSlide extends Fragment {
@Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
View inflated = inflater.inflate(R.layout.home_slide, container, false);
setWidgets(inflated);
return inflated;
}
private void setWidgets(View inflated) {
String[] text_presentation = new String[1];
text_presentation[0] = "Foo";
BaseScrollTextView baseScrollTextView = inflated.findViewById(R.id.main_autoscroll_text1);
baseScrollTextView.setTextList(new ArrayList<>(Arrays.asList(text_presentation)));
baseScrollTextView.startAutoScroll();
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.myapplication.libs.autoscrolling_text_view.BaseScrollTextView
android:id="@+id/main_autoscroll_text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
I think I have solved the problem by using runOnUiThread
in the method MarqueeTextView::TimerTask::run
. Since this change, I didn't see any bug. But I still don't know why this bug occurred, and why this modification solved it (I know it's better to use runOnUiThread
in this situation since scrollTo
is used, but I don't know what is the relationship between this call and the bug). If someone could comment my answer, to explain me, please :-) .
You can compare the changes (presented below) with the original file in my question.
So the only changes I've made are contained in the following file (com.example.myapplication.libs.autoscrolling_text_view.MarqueeTextView):
package com.example.myapplication.libs.autoscrolling_text_view;
import android.content.Context;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
*
* @author haohao on 2017/9/21 下午 02:33
* @version v1.0
*/
public class MarqueeTextView extends AppCompatTextView {
private int currentScrollPos = 0;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
final TimerTask task = new TimerTask() {
@Override
public void run() {
((AppCompatActivity) getContext()).runOnUiThread(new Runnable() {
@Override
public void run() {
currentScrollPos += 1;
scrollTo(currentScrollPos, 0);
}
});
}
};
public MarqueeTextView(Context context) {
super(context);
}
public void postStartScroll(int delay) { // Bug when reset AND then scheduleAtFixedRate are executed
int speed = 6;
pool.scheduleAtFixedRate(task, delay, speed, TimeUnit.MILLISECONDS);
}
}