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);
}
}
}