在 TextWatcher 中过滤适配器时出现 IllegalStateException

发布于 2024-12-04 16:52:29 字数 8180 浏览 0 评论 0原文

我有一个由自定义适配器(扩展 BaseAdapter)支持的自定义 ListActivity受此启发,该布局还包含一个 EditText 和一个 TextWatcher。适配器实现了Filterable。正如 Hamy 在另一个问题中建议的那样,TextWatcher.onTextChanged(...) 只需调用 adapter.getFilter().filter(string); 即可。

问题:当设备方向改变时,Activity 会被销毁,并与所有对象一起重新创建。发生这种情况时,会引发 IllegalStateException。研究揭示了这个问题:适配器内容不应该从后台线程修改,而只能从 UI 线程修改。 另请参阅此处

在我看来,TextWatcher 是在后台线程,同时重新创建布局,过滤适配器内容并导致异常。

我该如何解决这个问题?

编辑:

我正在针对 Android 1.6 (Donut) - API Level 4 进行构建,并使用 Samsung Galaxy S2、Android 2.3.3 进行测试。

活动:

public class MyActivity extends ListActivity {

  MyAdapter adapter;
  EditText searchBox;

  public boolean activeSearch = false;
  public ArrayList<MyItem> filteredItems = new ArrayList<MyItem>();

  /*
   * Activity overrides
   */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mylayout);

    getListView().setTextFilterEnabled(true);

    adapter = new MyAdapter(this, R.layout.myitemlayout);
    setListAdapter(adapter);

    searchBox = (EditText)findViewById(R.id.mySearchBox);
    searchBox.addTextChangedListener(searchBoxTextWatcher); // defined below
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();

    searchBox.removeTextChangedListener(searchBoxTextWatcher);
  }

  /*
   * Search box text watcher
   */
  TextWatcher searchBoxTextWatcher = new TextWatcher() {
    @Override
    public void onTextChanged(CharSequence searchText, int start, int before, int count) {
      adapter.getFilter().filter(searchText);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void afterTextChanged(Editable s) {
    }
  };
}

适配器:

public class MyAdapter extends BaseAdapter implements Filterable {

  MyActivity activity;
  int layoutID;

  Filter filter;

  /*
   * Constructor
   */
  public MyAdapter(MyActivity activity, int layoutID) {
    super();

    this.activity = activity;
    this.layoutID = layoutID;
  }

  /*
   * BaseAdapter overrides
   */
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    View view = convertView;

    MyItem item = activity.activeSearch ? activity.filteredItems.get(position) : allItems.get(position);

    // Create/prepare view

    // ...

    return view;
  }

  @Override
  public int getCount() {
    return activity.activeSearch ? activity.filteredItems.size() : allItems.size();
  }

  @Override
  public MyItem getItem(int position) {
    return activity.activeSearch ? activity.filteredItems.get(position) : allItems.get(position);
  }

  @Override
  public long getItemId(int position) {
    return getItem(position).itemID;
  }

  /*
   * Filterable overrides
   */
  @Override
  public Filter getFilter() {
    if (filter == null) {
      filter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
          String searchText = constraint.toString();
          FilterResults results = new FilterResults();

          if (searchText == null || searchText.length() == 0) {
            synchronized (this) {
              activity.activeSearch = false;

              results.values  = allItems;
              results.count   = allItems.size();
            }
          } else {
            synchronized (this) {
              activity.activeSearch = true;

              ArrayList<MyItem> filteredItems = new ArrayList<MyItem>();
              ArrayList<MyItem> tmpItems = new ArrayList<MyItem>();
              tmpItems.addAll(allItems);
              for (MyItem item : tmpItems) {
                boolean matched = false;

                // Compare various properties of item against searchText
                // and set matched = true if it matched.

                // ...

                if (matched) {
                  filteredItems.add(item);
                }
              }
              results.values  = filteredItems;
              results.count   = filteredItems.size();
            }
          }

          return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
          activity.filteredItems = (ArrayList<MyItem>)results.values;
          notifyDataSetChanged();
        }
      };
    }
    return filter;
  }
}

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical"
>
  <EditText
    android:id="@+id/mySearchBox"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:hint="Enter something :)"
    android:inputType="number"
    android:maxLines="1"
  ></EditText>
  <ListView
    android:id="@android:id/list"
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:background="@android:color/white"
  ></ListView>
</LinearLayout>

Stacktrace:

Thread [<1> main] (Suspended (exception IllegalStateException)) 
    ListView.layoutChildren() line: 1558    
    ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 1384  
    ListView(View).layout(int, int, int, int) line: 7228    
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7228    
    FrameLayout.onLayout(boolean, int, int, int, int) line: 338 
    FrameLayout(View).layout(int, int, int, int) line: 7228 
    PhoneWindow$DecorView(FrameLayout).onLayout(boolean, int, int, int, int) line: 338  
    PhoneWindow$DecorView(View).layout(int, int, int, int) line: 7228   
    ViewRoot.performTraversals() line: 1148 
    ViewRoot.handleMessage(Message) line: 1868  
    ViewRoot(Handler).dispatchMessage(Message) line: 99 
    Looper.loop() line: 123 
    ActivityThread.main(String[]) line: 3691    
    Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
    Method.invoke(Object, Object...) line: 507  
    ZygoteInit$MethodAndArgsCaller.run() line: 847  
    ZygoteInit.main(String[]) line: 605 
    NativeStart.main(String[]) line: not available [native method]  

LogCat 显示没有 Stacktrace,只有这个(不知道是否相关):

09-15 09:23:59.575: WARN/WindowManager(2696): Window freeze timeout expired.
09-15 09:23:59.575: WARN/WindowManager(2696): Force clearing orientation change: Window{40a82ba8 InputMethod paused=false}
09-15 09:23:59.575: WARN/WindowManager(2696): Force clearing orientation change: Window{40bff2f8 my.package/my.package.Activities.MyActivity paused=false}
09-15 09:23:59.875: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback

09-15 09:24:20.330: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback
09-15 09:24:20.330: ERROR/InputMethodService(2820): Unexpected null in startExtractingText : mExtractedText = null, input connection = com.android.internal.view.InputConnectionWrapper@406a82c0
09-15 09:24:22.340: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback
09-15 09:24:22.340: ERROR/InputMethodService(2820): Unexpected null in startExtractingText : mExtractedText = null, input connection = com.android.internal.view.InputConnectionWrapper@406a82c0

I have a custom ListActivity backed by a custom adapter (extending BaseAdapter). Inspired by this, the layout also contains an EditText with a TextWatcher. The Adapter implements Filterable. The TextWatcher.onTextChanged(...) simply calls adapter.getFilter().filter(string); just as Hamy suggested in the other question.

The problem: When device orientation changes, the Activity is destroyed and recreated along with all the objects. When that happens, an IllegalStateException is thrown. Research revealed the problem: the adapter content should not be modified from a background thread, but only from the UI thread. see also here

Seems to me the TextWatcher is called on a background thread while recreating the layout, filtering the adapter contents and causing the exception.

How can I work around that?

EDIT:

I'm building against Android 1.6 (Donut) - API Level 4 and testing with a Samsung Galaxy S2, Android 2.3.3.

The activity:

public class MyActivity extends ListActivity {

  MyAdapter adapter;
  EditText searchBox;

  public boolean activeSearch = false;
  public ArrayList<MyItem> filteredItems = new ArrayList<MyItem>();

  /*
   * Activity overrides
   */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mylayout);

    getListView().setTextFilterEnabled(true);

    adapter = new MyAdapter(this, R.layout.myitemlayout);
    setListAdapter(adapter);

    searchBox = (EditText)findViewById(R.id.mySearchBox);
    searchBox.addTextChangedListener(searchBoxTextWatcher); // defined below
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();

    searchBox.removeTextChangedListener(searchBoxTextWatcher);
  }

  /*
   * Search box text watcher
   */
  TextWatcher searchBoxTextWatcher = new TextWatcher() {
    @Override
    public void onTextChanged(CharSequence searchText, int start, int before, int count) {
      adapter.getFilter().filter(searchText);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void afterTextChanged(Editable s) {
    }
  };
}

The adapter:

public class MyAdapter extends BaseAdapter implements Filterable {

  MyActivity activity;
  int layoutID;

  Filter filter;

  /*
   * Constructor
   */
  public MyAdapter(MyActivity activity, int layoutID) {
    super();

    this.activity = activity;
    this.layoutID = layoutID;
  }

  /*
   * BaseAdapter overrides
   */
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    View view = convertView;

    MyItem item = activity.activeSearch ? activity.filteredItems.get(position) : allItems.get(position);

    // Create/prepare view

    // ...

    return view;
  }

  @Override
  public int getCount() {
    return activity.activeSearch ? activity.filteredItems.size() : allItems.size();
  }

  @Override
  public MyItem getItem(int position) {
    return activity.activeSearch ? activity.filteredItems.get(position) : allItems.get(position);
  }

  @Override
  public long getItemId(int position) {
    return getItem(position).itemID;
  }

  /*
   * Filterable overrides
   */
  @Override
  public Filter getFilter() {
    if (filter == null) {
      filter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
          String searchText = constraint.toString();
          FilterResults results = new FilterResults();

          if (searchText == null || searchText.length() == 0) {
            synchronized (this) {
              activity.activeSearch = false;

              results.values  = allItems;
              results.count   = allItems.size();
            }
          } else {
            synchronized (this) {
              activity.activeSearch = true;

              ArrayList<MyItem> filteredItems = new ArrayList<MyItem>();
              ArrayList<MyItem> tmpItems = new ArrayList<MyItem>();
              tmpItems.addAll(allItems);
              for (MyItem item : tmpItems) {
                boolean matched = false;

                // Compare various properties of item against searchText
                // and set matched = true if it matched.

                // ...

                if (matched) {
                  filteredItems.add(item);
                }
              }
              results.values  = filteredItems;
              results.count   = filteredItems.size();
            }
          }

          return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
          activity.filteredItems = (ArrayList<MyItem>)results.values;
          notifyDataSetChanged();
        }
      };
    }
    return filter;
  }
}

The layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical"
>
  <EditText
    android:id="@+id/mySearchBox"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:hint="Enter something :)"
    android:inputType="number"
    android:maxLines="1"
  ></EditText>
  <ListView
    android:id="@android:id/list"
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:background="@android:color/white"
  ></ListView>
</LinearLayout>

Stacktrace:

Thread [<1> main] (Suspended (exception IllegalStateException)) 
    ListView.layoutChildren() line: 1558    
    ListView(AbsListView).onLayout(boolean, int, int, int, int) line: 1384  
    ListView(View).layout(int, int, int, int) line: 7228    
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7228    
    FrameLayout.onLayout(boolean, int, int, int, int) line: 338 
    FrameLayout(View).layout(int, int, int, int) line: 7228 
    PhoneWindow$DecorView(FrameLayout).onLayout(boolean, int, int, int, int) line: 338  
    PhoneWindow$DecorView(View).layout(int, int, int, int) line: 7228   
    ViewRoot.performTraversals() line: 1148 
    ViewRoot.handleMessage(Message) line: 1868  
    ViewRoot(Handler).dispatchMessage(Message) line: 99 
    Looper.loop() line: 123 
    ActivityThread.main(String[]) line: 3691    
    Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
    Method.invoke(Object, Object...) line: 507  
    ZygoteInit$MethodAndArgsCaller.run() line: 847  
    ZygoteInit.main(String[]) line: 605 
    NativeStart.main(String[]) line: not available [native method]  

LogCat shows no Stacktrace, only this (don't know if it's related):

09-15 09:23:59.575: WARN/WindowManager(2696): Window freeze timeout expired.
09-15 09:23:59.575: WARN/WindowManager(2696): Force clearing orientation change: Window{40a82ba8 InputMethod paused=false}
09-15 09:23:59.575: WARN/WindowManager(2696): Force clearing orientation change: Window{40bff2f8 my.package/my.package.Activities.MyActivity paused=false}
09-15 09:23:59.875: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback

09-15 09:24:20.330: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback
09-15 09:24:20.330: ERROR/InputMethodService(2820): Unexpected null in startExtractingText : mExtractedText = null, input connection = com.android.internal.view.InputConnectionWrapper@406a82c0
09-15 09:24:22.340: WARN/InputConnectionWrapper.ICC(2820): Timed out waiting on IInputContextCallback
09-15 09:24:22.340: ERROR/InputMethodService(2820): Unexpected null in startExtractingText : mExtractedText = null, input connection = com.android.internal.view.InputConnectionWrapper@406a82c0

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文