I'm trying to achive simple functionality of converting integers into decimal and vice versa (its simplication of my more advanced problem), on a single element of RecyclerView, live upon every user input. The problem is that when i call RecyclerView.Adapter.notifyItemChanged() inside of TextWatcher.afterTextChanged() it ends up in an infinite loop of TextWatcher's methods. Is there any way to achive this kind of behaviour? I want to update my orginal item inside TextWatcher.afterTextChanged().
Code below:
public class TestElementAdapter extends RecyclerView.Adapter<TestElementAdapter.TestElementViewHolder> {
private final Context context;
private final RecyclerView recyclerView;
private List<TestElement> testElements;
public TestElementAdapter(Context context, List<TestElement> testElements, RecyclerView recyclerView) {
this.context = context;
this.testElements = testElements;
this.recyclerView = recyclerView;
testElements.add(new TestElement(1, 1.0F));
}
private class MyTextWatcher<T> implements TextWatcher {
int position = 0;
BiConsumer<TestElement, T> consumer;
Function<String, T> mappingFunction;
public MyTextWatcher(BiConsumer<TestElement, T> consumer, Function<String, T> mappingFunction) {
this.consumer = consumer;
this.mappingFunction = mappingFunction;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s == null || s.length() == 0) {
return;
}
TestElement testElement = testElements.get(position);
consumer.accept(testElement, mappingFunction.apply(s.toString()));
TestElementAdapter.this.recyclerView.post(() -> notifyItemChanged(position));
System.out.println("afterTextChanged");
}
public void updatePosition(int adapterPosition) {
this.position = adapterPosition;
}
}
@NonNull
@Override
public TestElementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.test_element, parent, false);
TestElementViewHolder holder = new TestElementViewHolder(view);
holder.initTextWatchers();
return holder;
}
@Override
public void onBindViewHolder(@NonNull TestElementViewHolder holder, int position) {
TestElement testElement = testElements.get(position);
holder.integer.getText().clear();
holder.decimal.getText().clear();
holder.integer.append(testElement.getInteger() + "");
holder.decimal.append(testElement.getDecimal() + "");
holder.watchers.forEach(watcher -> watcher.updatePosition(holder.getAdapterPosition()));
}
@Override
public int getItemCount() {
return testElements.size();
}
public static class TestElement {
private Integer integer;
private Float decimal;
public TestElement(Integer integer, Float decimal) {
this.integer = integer;
this.decimal = decimal;
}
public Integer getInteger() { return integer; }
public Float getDecimal() { return decimal; }
public void updateByInteger(Integer integer) {
this.integer = integer;
this.decimal = integer * 1.0F;
}
public void updateByDecimal(Float decimal) {
this.decimal = decimal;
this.integer = decimal.intValue();
}
}
public class TestElementViewHolder extends RecyclerView.ViewHolder {
EditText integer;
EditText decimal;
List<MyTextWatcher<?>> watchers = new ArrayList<>();
private MyTextWatcher<Integer> integerWatcher;
private MyTextWatcher<Float> decimalWatcher;
public TestElementViewHolder(@NonNull View view) {
super(view);
integer = view.findViewById(R.id.integerNumber);
decimal = view.findViewById(R.id.decimalNumber);
}
public void initTextWatchers() {
MyTextWatcher<Integer> integerWatcher = new MyTextWatcher<>(this, TestElement::updateByInteger, Integer::parseInt);
integer.addTextChangedListener(integerWatcher);
this.integerWatcher = integerWatcher;
MyTextWatcher<Float> decimalWatcher = new MyTextWatcher<>(this, TestElement::updateByDecimal, Float::parseFloat);
decimal.addTextChangedListener(decimalWatcher);
this.decimalWatcher = decimalWatcher;
watchers.add(this.integerWatcher);
watchers.add(this.decimalWatcher);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="wrap_content"
android:background="@color/white"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Light"
android:orientation="vertical"
android:gravity="top">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" android:id="@+id/linearLayout2">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="number"
android:padding="8dp"
android:id="@+id/integerNumber"
android:gravity="center"
android:layout_weight="1"
/>
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:padding="8dp"
android:id="@+id/decimalNumber"
android:gravity="center"
android:layout_weight="1"
/>
</LinearLayout>
</LinearLayout>
UPDATE:
I added methods to disable and enable TextWatchers in ViewHolder. I disable them line before notifyItemChanged and then enable them in onBindViewHolder.
public void disableWatchers() {
integer.removeTextChangedListener(integerWatcher);
decimal.removeTextChangedListener(decimalWatcher);
}
public void enableWatchers() {
integer.addTextChangedListener(integerWatcher);
decimal.addTextChangedListener(decimalWatcher);
}
It worked for me.
I also changed call to notifyItemChanged from
TestElementAdapter.this.recyclerView.post(() -> notifyItemChanged(position))
to
TestElementAdapter.this.notifyItemChanged(position).
Calling this method from UI thread made EditText not responsive enough. It also works fine, but only for amount of elements that all can fit on my RecyclerView, if there are more elements im getting exception: java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling.... Is there any way to leave call TestElementAdapter.this.notifyItemChanged(position) in TextWatcher.afterTextChanged and avoid this error?
Updated Adapter code:
public class TestElementAdapter extends RecyclerView.Adapter<TestElementAdapter.TestElementViewHolder> {
private final Context context;
private final RecyclerView recyclerView;
private List<TestElement> testElements;
public TestElementAdapter(Context context, List<TestElement> testElements, RecyclerView recyclerView) {
this.context = context;
this.testElements = testElements;
this.recyclerView = recyclerView;
IntStream.range(0, 20).forEach(v -> testElements.add(new TestElement(1, 1.0F)));
}
private class MyTextWatcher<T> implements TextWatcher {
int position = 0;
TestElementViewHolder viewHolder;
BiConsumer<TestElement, T> consumer;
Function<String, T> mappingFunction;
public MyTextWatcher(TestElementViewHolder viewHolder, BiConsumer<TestElement, T> consumer, Function<String, T> mappingFunction) {
this.viewHolder = viewHolder;
this.consumer = consumer;
this.mappingFunction = mappingFunction;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s == null || s.length() == 0) {
return;
}
TestElement testElement = testElements.get(position);
consumer.accept(testElement, mappingFunction.apply(s.toString()));
viewHolder.disableWatchers();
TestElementAdapter.this.notifyItemChanged(position);
System.out.println("afterTextChanged");
}
public void updatePosition(int adapterPosition) {
this.position = adapterPosition;
}
}
@NonNull
@Override
public TestElementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.test_element, parent, false);
TestElementViewHolder holder = new TestElementViewHolder(view);
holder.initTextWatchers();
holder.disableWatchers();
return holder;
}
@Override
public void onBindViewHolder(@NonNull TestElementViewHolder holder, int position) {
TestElement testElement = testElements.get(position);
holder.integer.getText().clear();
holder.decimal.getText().clear();
holder.integer.append(testElement.getInteger() + "");
holder.decimal.append(testElement.getDecimal() + "");
holder.watchers.forEach(watcher -> watcher.updatePosition(holder.getAdapterPosition()));
holder.enableWatchers();
}
@Override
public int getItemCount() {
return testElements.size();
}
public static class TestElement {
private Integer integer;
private Float decimal;
public TestElement(Integer integer, Float decimal) {
this.integer = integer;
this.decimal = decimal;
}
public Integer getInteger() { return integer; }
public Float getDecimal() { return decimal; }
public void updateByInteger(Integer integer) {
this.integer = integer;
this.decimal = integer * 1.0F;
}
public void updateByDecimal(Float decimal) {
this.decimal = decimal;
this.integer = decimal.intValue();
}
}
public class TestElementViewHolder extends RecyclerView.ViewHolder {
EditText integer;
EditText decimal;
List<MyTextWatcher<?>> watchers = new ArrayList<>();
private MyTextWatcher<Integer> integerWatcher;
private MyTextWatcher<Float> decimalWatcher;
public TestElementViewHolder(@NonNull View view) {
super(view);
integer = view.findViewById(R.id.integerNumber);
decimal = view.findViewById(R.id.decimalNumber);
}
public void initTextWatchers() {
MyTextWatcher<Integer> integerWatcher = new MyTextWatcher<>(this, TestElement::updateByInteger, Integer::parseInt);
integer.addTextChangedListener(integerWatcher);
this.integerWatcher = integerWatcher;
MyTextWatcher<Float> decimalWatcher = new MyTextWatcher<>(this, TestElement::updateByDecimal, Float::parseFloat);
decimal.addTextChangedListener(decimalWatcher);
this.decimalWatcher = decimalWatcher;
watchers.add(this.integerWatcher);
watchers.add(this.decimalWatcher);
}
public void disableWatchers() {
integer.removeTextChangedListener(integerWatcher);
decimal.removeTextChangedListener(decimalWatcher);
}
public void enableWatchers() {
integer.addTextChangedListener(integerWatcher);
decimal.addTextChangedListener(decimalWatcher);
}
}
}