【问题标题】:Infinite loop of TextWatchers methods on RecyclerView AdapterRecyclerView Adapter 上 TextWatchers 方法的无限循环
【发布时间】:2021-01-05 21:13:46
【问题描述】:

我正在尝试实现将整数转换为十进制的简单功能,反之亦然(它简化了我更高级的问题),在 RecyclerView 的单个元素上,每个用户输入都存在。问题是当我在TextWatcher.afterTextChanged() 内部调用RecyclerView.Adapter.notifyItemChanged() 时,它最终会进入TextWatcher 方法的无限循环。有没有办法实现这种行为?我想更新 TextWatcher.afterTextChanged() 中的原始项目。

代码如下:

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>

更新:

我在ViewHolder 中添加了禁用和启用TextWatchers 的方法。我在notifyItemChanged 之前禁用它们,然后在onBindViewHolder 中启用它们。

public void disableWatchers() {
            integer.removeTextChangedListener(integerWatcher);
            decimal.removeTextChangedListener(decimalWatcher);
        }

        public void enableWatchers() {
            integer.addTextChangedListener(integerWatcher);
            decimal.addTextChangedListener(decimalWatcher);
        }

它对我有用。

我还把电话改为notifyItemChanged from

TestElementAdapter.this.recyclerView.post(() -&gt; notifyItemChanged(position))TestElementAdapter.this.notifyItemChanged(position).

从 UI 线程调用此方法使 EditText 响应不足。它也可以正常工作,但仅适用于我的RecyclerView 上的所有元素数量,如果有更多元素我得到异常:java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling...。有什么办法可以在TextWatcher.afterTextChanged 中留下电话TestElementAdapter.this.notifyItemChanged(position) 并避免此错误?

更新的适配器代码:

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

【问题讨论】:

  • 您可以在调用notifyItemChanged之前尝试停止观察者监听器并重新启用它们。
  • 感谢您的提示,我尝试了此解决方案,它需要对我的代码进行一些更改,但它确实有效。我在调用notifyItemChanged 之前禁用所有TextWatchers,然后在onBindViewHolder 中启用它们。另外,我将电话recyclerView.post(() -&gt; notifyItemChanged) 更改为TestElementAdapter.this.notifyItemChanged。它适用于所有可以放入 RecyclerView 的元素数量,但是当有更多元素时,我得到IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling,。有什么办法可以避免吗?
  • 你还在recyclerView.post消息中打电话给notifyItemChanged吗?
  • 不,我将其替换为RecyclerView.Adapter.notifyItemChanged,否则EditText 无法正常工作。在recyclerView.post 之间完成的所有更改以及消息的实际执行都被丢弃了,因为ViewHolder 得到了更新。
  • 可以分享这部分的更新代码

标签: android android-recyclerview android-edittext


【解决方案1】:

listernes 重复的原因是您调用notifyItemChanged() 添加了一个新的侦听器,同时在调用notifyItemChanged() 之后保持当前项的当前侦听器处于活动状态。

要解决此问题,您需要在使用editText.removeTextChangedListener() 调用notifyItemChanged() 之前禁用侦听器,然后使用editText.addTextChangedListener() 再次启用它们。

那么你得到了

IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling

这是一个common issue,您可以通过发布 recyclerView 更改主线程循环使用来解决。

但请确保您在这篇文章中添加了监听器,以便在 recyclerView 更改后同步,这里是更改:

@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()));
    
    // remove the listeners
    viewHolder.disableWatchers();

    recyclerView.post(() -> {
        TestElementAdapter.this.notifyItemChanged(position)
        // add the listeners
        viewHolder.enableWatchers();
    });

    System.out.println("afterTextChanged");
}

这是整个班级的变化


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();
            
            recyclerView.post(() -> {
                TestElementAdapter.this.notifyItemChanged(position)
                viewHolder.enableWatchers();
            });

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-25
    • 2014-03-21
    相关资源
    最近更新 更多