【Android】Android中的搜索过滤操作实现

我在【Android】搜索框架中转载了一篇文章,这篇文章讲述了如何使用Android平台自带的搜索框架从而实现搜索的原生展现。这篇文章则要叙述如何实现搜索功能。

搜索在很多应用中都非常常见,比如联系人应用,Google Play中,能够有让我们快速找到需要寻找的内容。

在手机端,搜索需要注意搜索结果的展示,我们希望用户可以尽可能看到更多的搜索结果,所以我们一般使用ListView展现搜索结果,信息简单而且有价值。下面以联系人应用、ListView展示搜索结果为例,讲述如何搜索功能。

一、简介

一般来说,我们可以使用以下几种方式实现搜索:

1)暴力搜索——直接使用数据库提供的功能,每次都从数据库中读取搜索的结果,存进一个数据结构用于Adapter显示,调用notifyDataSetChanged()刷新数据;

2)利用filter进行搜索。这块涉及到Filterable接口。推荐文章:Android实现Filterable通过输入文本框实现联系人自动筛选。有两点值得注意:a)Android原生组件AutoCompleteTextview就是使用该方法实现的;b)这个方法本质上还是调用notifyDataSetChanged()方法,并且还是要自己去实现搜索部分,只是整个方法看上去比较优雅,而且不用再去搜索数据库,最重要的一点是这个时候搜索过程被自动移到另外一个线程之中,搜索完毕之后才会刷新UI;

二、CursorAdapter

以上两种方法都比较简单,接下来要讲的方法并非有什么新奇之处,只是利用Android已有的API去实现该功能,避免自己去实现已有的实现。方法没有好坏之分,能实现功能的情况下越简单越好,暴力搜索也很OK。首先,我们认识一下一个类:CursorAdapter(一看就知道是干嘛的了吧),上源码:

  1 /*
  2  * Copyright (C) 2011 The Android Open Source Project
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 package android.support.v4.widget;
 18 
 19 import android.content.Context;
 20 import android.database.ContentObserver;
 21 import android.database.Cursor;
 22 import android.database.DataSetObserver;
 23 import android.os.Handler;
 24 import android.util.Log;
 25 import android.view.View;
 26 import android.view.ViewGroup;
 27 import android.widget.BaseAdapter;
 28 import android.widget.Filter;
 29 import android.widget.FilterQueryProvider;
 30 import android.widget.Filterable;
 31 
 32 /**
 33  * Static library support version of the framework's {@link android.widget.CursorAdapter}.
 34  * Used to write apps that run on platforms prior to Android 3.0.  When running
 35  * on Android 3.0 or above, this implementation is still used; it does not try
 36  * to switch to the framework's implementation.  See the framework SDK
 37  * documentation for a class overview.
 38  */
 39 public abstract class CursorAdapter extends BaseAdapter implements Filterable,
 40         CursorFilter.CursorFilterClient {
 41     /**
 42      * This field should be made private, so it is hidden from the SDK.
 43      * {@hide}
 44      */
 45     protected boolean mDataValid;
 46     /**
 47      * This field should be made private, so it is hidden from the SDK.
 48      * {@hide}
 49      */
 50     protected boolean mAutoRequery;
 51     /**
 52      * This field should be made private, so it is hidden from the SDK.
 53      * {@hide}
 54      */
 55     protected Cursor mCursor;
 56     /**
 57      * This field should be made private, so it is hidden from the SDK.
 58      * {@hide}
 59      */
 60     protected Context mContext;
 61     /**
 62      * This field should be made private, so it is hidden from the SDK.
 63      * {@hide}
 64      */
 65     protected int mRowIDColumn;
 66     /**
 67      * This field should be made private, so it is hidden from the SDK.
 68      * {@hide}
 69      */
 70     protected ChangeObserver mChangeObserver;
 71     /**
 72      * This field should be made private, so it is hidden from the SDK.
 73      * {@hide}
 74      */
 75     protected DataSetObserver mDataSetObserver;
 76     /**
 77      * This field should be made private, so it is hidden from the SDK.
 78      * {@hide}
 79      */
 80     protected CursorFilter mCursorFilter;
 81     /**
 82      * This field should be made private, so it is hidden from the SDK.
 83      * {@hide}
 84      */
 85     protected FilterQueryProvider mFilterQueryProvider;
 86 
 87     /**
 88      * If set the adapter will call requery() on the cursor whenever a content change
 89      * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
 90      *
 91      * @deprecated This option is discouraged, as it results in Cursor queries
 92      * being performed on the application's UI thread and thus can cause poor
 93      * responsiveness or even Application Not Responding errors.  As an alternative,
 94      * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
 95      */
 96     @Deprecated
 97     public static final int FLAG_AUTO_REQUERY = 0x01;
 98 
 99     /**
100      * If set the adapter will register a content observer on the cursor and will call
101      * {@link #onContentChanged()} when a notification comes in.  Be careful when
102      * using this flag: you will need to unset the current Cursor from the adapter
103      * to avoid leaks due to its registered observers.  This flag is not needed
104      * when using a CursorAdapter with a
105      * {@link android.content.CursorLoader}.
106      */
107     public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
108 
109     /**
110      * Constructor that always enables auto-requery.
111      *
112      * @deprecated This option is discouraged, as it results in Cursor queries
113      * being performed on the application's UI thread and thus can cause poor
114      * responsiveness or even Application Not Responding errors.  As an alternative,
115      * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
116      *
117      * @param c The cursor from which to get the data.
118      * @param context The context
119      */
120     @Deprecated
121     public CursorAdapter(Context context, Cursor c) {
122         init(context, c, FLAG_AUTO_REQUERY);
123     }
124 
125     /**
126      * Constructor that allows control over auto-requery.  It is recommended
127      * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
128      * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
129      * will always be set.
130      *
131      * @param c The cursor from which to get the data.
132      * @param context The context
133      * @param autoRequery If true the adapter will call requery() on the
134      *                    cursor whenever it changes so the most recent
135      *                    data is always displayed.  Using true here is discouraged.
136      */
137     public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
138         init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
139     }
140 
141     /**
142      * Recommended constructor.
143      *
144      * @param c The cursor from which to get the data.
145      * @param context The context
146      * @param flags Flags used to determine the behavior of the adapter; may
147      * be any combination of {@link #FLAG_AUTO_REQUERY} and
148      * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
149      */
150     public CursorAdapter(Context context, Cursor c, int flags) {
151         init(context, c, flags);
152     }
153 
154     /**
155      * @deprecated Don't use this, use the normal constructor.  This will
156      * be removed in the future.
157      */
158     @Deprecated
159     protected void init(Context context, Cursor c, boolean autoRequery) {
160         init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
161     }
162 
163     void init(Context context, Cursor c, int flags) {
164         if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
165             flags |= FLAG_REGISTER_CONTENT_OBSERVER;
166             mAutoRequery = true;
167         } else {
168             mAutoRequery = false;
169         }
170         boolean cursorPresent = c != null;
171         mCursor = c;
172         mDataValid = cursorPresent;
173         mContext = context;
174         mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
175         if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
176             mChangeObserver = new ChangeObserver();
177             mDataSetObserver = new MyDataSetObserver();
178         } else {
179             mChangeObserver = null;
180             mDataSetObserver = null;
181         }
182 
183         if (cursorPresent) {
184             if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
185             if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
186         }
187     }
188 
189     /**
190      * Returns the cursor.
191      * @return the cursor.
192      */
193     public Cursor getCursor() {
194         return mCursor;
195     }
196 
197     /**
198      * @see android.widget.ListAdapter#getCount()
199      */
200     public int getCount() {
201         if (mDataValid && mCursor != null) {
202             return mCursor.getCount();
203         } else {
204             return 0;
205         }
206     }
207     
208     /**
209      * @see android.widget.ListAdapter#getItem(int)
210      */
211     public Object getItem(int position) {
212         if (mDataValid && mCursor != null) {
213             mCursor.moveToPosition(position);
214             return mCursor;
215         } else {
216             return null;
217         }
218     }
219 
220     /**
221      * @see android.widget.ListAdapter#getItemId(int)
222      */
223     public long getItemId(int position) {
224         if (mDataValid && mCursor != null) {
225             if (mCursor.moveToPosition(position)) {
226                 return mCursor.getLong(mRowIDColumn);
227             } else {
228                 return 0;
229             }
230         } else {
231             return 0;
232         }
233     }
234     
235     @Override
236     public boolean hasStableIds() {
237         return true;
238     }
239 
240     /**
241      * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
242      */
243     public View getView(int position, View convertView, ViewGroup parent) {
244         if (!mDataValid) {
245             throw new IllegalStateException("this should only be called when the cursor is valid");
246         }
247         if (!mCursor.moveToPosition(position)) {
248             throw new IllegalStateException("couldn't move cursor to position " + position);
249         }
250         View v;
251         if (convertView == null) {
252             v = newView(mContext, mCursor, parent);
253         } else {
254             v = convertView;
255         }
256         bindView(v, mContext, mCursor);
257         return v;
258     }
259 
260     @Override
261     public View getDropDownView(int position, View convertView, ViewGroup parent) {
262         if (mDataValid) {
263             mCursor.moveToPosition(position);
264             View v;
265             if (convertView == null) {
266                 v = newDropDownView(mContext, mCursor, parent);
267             } else {
268                 v = convertView;
269             }
270             bindView(v, mContext, mCursor);
271             return v;
272         } else {
273             return null;
274         }
275     }
276     
277     /**
278      * Makes a new view to hold the data pointed to by cursor.
279      * @param context Interface to application's global information
280      * @param cursor The cursor from which to get the data. The cursor is already
281      * moved to the correct position.
282      * @param parent The parent to which the new view is attached to
283      * @return the newly created view.
284      */
285     public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
286 
287     /**
288      * Makes a new drop down view to hold the data pointed to by cursor.
289      * @param context Interface to application's global information
290      * @param cursor The cursor from which to get the data. The cursor is already
291      * moved to the correct position.
292      * @param parent The parent to which the new view is attached to
293      * @return the newly created view.
294      */
295     public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
296         return newView(context, cursor, parent);
297     }
298 
299     /**
300      * Bind an existing view to the data pointed to by cursor
301      * @param view Existing view, returned earlier by newView
302      * @param context Interface to application's global information
303      * @param cursor The cursor from which to get the data. The cursor is already
304      * moved to the correct position.
305      */
306     public abstract void bindView(View view, Context context, Cursor cursor);
307     
308     /**
309      * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
310      * closed.
311      * 
312      * @param cursor The new cursor to be used
313      */
314     public void changeCursor(Cursor cursor) {
315         Cursor old = swapCursor(cursor);
316         if (old != null) {
317             old.close();
318         }
319     }
320 
321     /**
322      * Swap in a new Cursor, returning the old Cursor.  Unlike
323      * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
324      * closed.
325      *
326      * @param newCursor The new cursor to be used.
327      * @return Returns the previously set Cursor, or null if there wasa not one.
328      * If the given new Cursor is the same instance is the previously set
329      * Cursor, null is also returned.
330      */
331     public Cursor swapCursor(Cursor newCursor) {
332         if (newCursor == mCursor) {
333             return null;
334         }
335         Cursor oldCursor = mCursor;
336         if (oldCursor != null) {
337             if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
338             if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
339         }
340         mCursor = newCursor;
341         if (newCursor != null) {
342             if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
343             if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
344             mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
345             mDataValid = true;
346             // notify the observers about the new cursor
347             notifyDataSetChanged();
348         } else {
349             mRowIDColumn = -1;
350             mDataValid = false;
351             // notify the observers about the lack of a data set
352             notifyDataSetInvalidated();
353         }
354         return oldCursor;
355     }
356 
357     /**
358      * <p>Converts the cursor into a CharSequence. Subclasses should override this
359      * method to convert their results. The default implementation returns an
360      * empty String for null values or the default String representation of
361      * the value.</p>
362      *
363      * @param cursor the cursor to convert to a CharSequence
364      * @return a CharSequence representing the value
365      */
366     public CharSequence convertToString(Cursor cursor) {
367         return cursor == null ? "" : cursor.toString();
368     }
369 
370     /**
371      * Runs a query with the specified constraint. This query is requested
372      * by the filter attached to this adapter.
373      *
374      * The query is provided by a
375      * {@link android.widget.FilterQueryProvider}.
376      * If no provider is specified, the current cursor is not filtered and returned.
377      *
378      * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
379      * and the previous cursor is closed.
380      *
381      * This method is always executed on a background thread, not on the
382      * application's main thread (or UI thread.)
383      * 
384      * Contract: when constraint is null or empty, the original results,
385      * prior to any filtering, must be returned.
386      *
387      * @param constraint the constraint with which the query must be filtered
388      *
389      * @return a Cursor representing the results of the new query
390      *
391      * @see #getFilter()
392      * @see #getFilterQueryProvider()
393      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
394      */
395     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
396         if (mFilterQueryProvider != null) {
397             return mFilterQueryProvider.runQuery(constraint);
398         }
399 
400         return mCursor;
401     }
402 
403     public Filter getFilter() {
404         if (mCursorFilter == null) {
405             mCursorFilter = new CursorFilter(this);
406         }
407         return mCursorFilter;
408     }
409 
410     /**
411      * Returns the query filter provider used for filtering. When the
412      * provider is null, no filtering occurs.
413      *
414      * @return the current filter query provider or null if it does not exist
415      *
416      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
417      * @see #runQueryOnBackgroundThread(CharSequence)
418      */
419     public FilterQueryProvider getFilterQueryProvider() {
420         return mFilterQueryProvider;
421     }
422 
423     /**
424      * Sets the query filter provider used to filter the current Cursor.
425      * The provider's
426      * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
427      * method is invoked when filtering is requested by a client of
428      * this adapter.
429      *
430      * @param filterQueryProvider the filter query provider or null to remove it
431      *
432      * @see #getFilterQueryProvider()
433      * @see #runQueryOnBackgroundThread(CharSequence)
434      */
435     public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
436         mFilterQueryProvider = filterQueryProvider;
437     }
438 
439     /**
440      * Called when the {@link ContentObserver} on the cursor receives a change notification.
441      * The default implementation provides the auto-requery logic, but may be overridden by
442      * sub classes.
443      * 
444      * @see ContentObserver#onChange(boolean)
445      */
446     protected void onContentChanged() {
447         if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
448             if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
449             mDataValid = mCursor.requery();
450         }
451     }
452 
453     private class ChangeObserver extends ContentObserver {
454         public ChangeObserver() {
455             super(new Handler());
456         }
457 
458         @Override
459         public boolean deliverSelfNotifications() {
460             return true;
461         }
462 
463         @Override
464         public void onChange(boolean selfChange) {
465             onContentChanged();
466         }
467     }
468 
469     private class MyDataSetObserver extends DataSetObserver {
470         @Override
471         public void onChanged() {
472             mDataValid = true;
473             notifyDataSetChanged();
474         }
475 
476         @Override
477         public void onInvalidated() {
478             mDataValid = false;
479             notifyDataSetInvalidated();
480         }
481     }
482 
483 }
View Code

这边有几点比较有意思,值得注意:

1)这个类实现了Filterable接口和CursorFilter.CursorFilterClient接口,第一个接口已经有所认识,第二个接口暂时不提,后面论述;

2)下面这个方法:

 1 /**
 2      * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
 3      */
 4     public View getView(int position, View convertView, ViewGroup parent) {
 5         if (!mDataValid) {
 6             throw new IllegalStateException("this should only be called when the cursor is valid");
 7         }
 8         if (!mCursor.moveToPosition(position)) {
 9             throw new IllegalStateException("couldn't move cursor to position " + position);
10         }
11         View v;
12         if (convertView == null) {
13             v = newView(mContext, mCursor, parent);
14         } else {
15             v = convertView;
16         }
17         bindView(v, mContext, mCursor);
18         return v;
19     }

我们知道ListView,GridView之类的ViewGroup子类都自带组件复用机制,View可以重复显示在界面上而不需要重新去实例化它,而通常这个都是需要我们在getView()方法里面自己实现,而实现机制就非常类似上面这段代码,这里,CursorAdapter已经帮我们实现了,如果View为空则执行newView操作,最后执行bindView()方法,最后返回v。而在CurosrAdapter类里面,bindView和newView方法都是抽象方法,需要开发者自己实现的,而根据这边的源码,我们很容易知道这两个方法应该如何实现:1)newView中应该重新生成一个View;2)bindView中应该重新对View中的组件进行“赋值”操作;完全不需要去考虑复用组件。

下面回到第1)点,第1)点中还有半点不明白的地方:CursorFilter.CursorFilterClient。首先了解一下CursorFilter这个类,上源码:

 1 /*
 2  * Copyright (C) 2011 The Android Open Source Project
 3  *
 4  * Licensed under the Apache License, Version 2.0 (the "License");
 5  * you may not use this file except in compliance with the License.
 6  * You may obtain a copy of the License at
 7  *
 8  *      http://www.apache.org/licenses/LICENSE-2.0
 9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v4.widget;
18 
19 import android.database.Cursor;
20 import android.widget.Filter;
21 
22 /**
23  * <p>The CursorFilter delegates most of the work to the CursorAdapter.
24  * Subclasses should override these delegate methods to run the queries
25  * and convert the results into String that can be used by auto-completion
26  * widgets.</p>
27  */
28 class CursorFilter extends Filter {
29     
30     CursorFilterClient mClient;
31     
32     interface CursorFilterClient {
33         CharSequence convertToString(Cursor cursor);
34         Cursor runQueryOnBackgroundThread(CharSequence constraint);
35         Cursor getCursor();
36         void changeCursor(Cursor cursor);
37     }
38 
39     CursorFilter(CursorFilterClient client) {
40         mClient = client;
41     }
42     
43     @Override
44     public CharSequence convertResultToString(Object resultValue) {
45         return mClient.convertToString((Cursor) resultValue);
46     }
47 
48     @Override
49     protected FilterResults performFiltering(CharSequence constraint) {
50         Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);
51 
52         FilterResults results = new FilterResults();
53         if (cursor != null) {
54             results.count = cursor.getCount();
55             results.values = cursor;
56         } else {
57             results.count = 0;
58             results.values = null;
59         }
60         return results;
61     }
62 
63     @Override
64     protected void publishResults(CharSequence constraint, FilterResults results) {
65         Cursor oldCursor = mClient.getCursor();
66         
67         if (results.values != null && results.values != oldCursor) {
68             mClient.changeCursor((Cursor) results.values);
69         }
70     }
71 }
View Code

这个类继承了Filter,如果你仔细看过前面推荐了解Filterable的那篇文章(Android实现Filterable通过输入文本框实现联系人自动筛选),对这个一定看上去非常眼熟,OK,这边几乎没什么不同,唯一的不同就是将查询操作交给了一个叫做CursorFilterClient的接口对象,这货长的是这副样子的:

1 interface CursorFilterClient {
2         CharSequence convertToString(Cursor cursor);
3         Cursor runQueryOnBackgroundThread(CharSequence constraint);
4         Cursor getCursor();
5         void changeCursor(Cursor cursor);
6     }

OKOK,CursorAdapter是实现了这个接口的,我们来看看最重要的两个接口具体在这里面是怎么实现的吧:

 1 /**
 2      * Runs a query with the specified constraint. This query is requested
 3      * by the filter attached to this adapter.
 4      *
 5      * The query is provided by a
 6      * {@link android.widget.FilterQueryProvider}.
 7      * If no provider is specified, the current cursor is not filtered and returned.
 8      *
 9      * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
10      * and the previous cursor is closed.
11      *
12      * This method is always executed on a background thread, not on the
13      * application's main thread (or UI thread.)
14      * 
15      * Contract: when constraint is null or empty, the original results,
16      * prior to any filtering, must be returned.
17      *
18      * @param constraint the constraint with which the query must be filtered
19      *
20      * @return a Cursor representing the results of the new query
21      *
22      * @see #getFilter()
23      * @see #getFilterQueryProvider()
24      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
25      */
26     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
27         if (mFilterQueryProvider != null) {
28             return mFilterQueryProvider.runQuery(constraint);
29         }
30 
31         return mCursor;
32     }
View Code

查询操作又交给了另外一个对象:FilterQueryProvider。

 1 /**
 2      * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
 3      * closed.
 4      * 
 5      * @param cursor The new cursor to be used
 6      */
 7     public void changeCursor(Cursor cursor) {
 8         Cursor old = swapCursor(cursor);
 9         if (old != null) {
10             old.close();
11         }
12     }
13 
14     /**
15      * Swap in a new Cursor, returning the old Cursor.  Unlike
16      * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
17      * closed.
18      *
19      * @param newCursor The new cursor to be used.
20      * @return Returns the previously set Cursor, or null if there wasa not one.
21      * If the given new Cursor is the same instance is the previously set
22      * Cursor, null is also returned.
23      */
24     public Cursor swapCursor(Cursor newCursor) {
25         if (newCursor == mCursor) {
26             return null;
27         }
28         Cursor oldCursor = mCursor;
29         if (oldCursor != null) {
30             if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
31             if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
32         }
33         mCursor = newCursor;
34         if (newCursor != null) {
35             if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
36             if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
37             mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
38             mDataValid = true;
39             // notify the observers about the new cursor
40             notifyDataSetChanged();
41         } else {
42             mRowIDColumn = -1;
43             mDataValid = false;
44             // notify the observers about the lack of a data set
45             notifyDataSetInvalidated();
46         }
47         return oldCursor;
48     }
View Code

更新完毕以后调用该方法刷新列表。

现在假设你要去使用这样CursorAdapter,我们来看看如何进行:

第一步,首先根据上面的说明去实现bindView和newView两个方法,然后传递一个Cursor进来实例化CursorAdapter,这样子首先实现了一个基本的Adapter的功能;

第二步,我们要实现搜索过滤功能。我们在使用Filterable接口的时候,调用方法如下:

1 adapter.getFilter().filter(et_filter.getText().toString());  

在这里一样的。这里CursorAdapter的getFileter()方法实现如下:

1 public Filter getFilter() {
2         if (mCursorFilter == null) {
3             mCursorFilter = new CursorFilter(this);
4         }
5         return mCursorFilter;
6     }

它返回的是一个CursorFilter,实例化的时候需要一个CursorFilter.CursorFilterClient参数,这里正好CursorAdapter就实现了这个接口。所以直接用this即可,这里还缺一点,我们缺少一个最最核心的对象:FilterQueryProvider,最后的查询操作就交给了它。它很简单:

 1 /*
 2  * Copyright (C) 2007 The Android Open Source Project
 3  *
 4  * Licensed under the Apache License, Version 2.0 (the "License");
 5  * you may not use this file except in compliance with the License.
 6  * You may obtain a copy of the License at
 7  *
 8  *      http://www.apache.org/licenses/LICENSE-2.0
 9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.database.Cursor;
20 
21 /**
22  * This class can be used by external clients of CursorAdapter and
23  * CursorTreeAdapter to define how the content of the adapter should be
24  * filtered.
25  * 
26  * @see #runQuery(CharSequence)
27  */
28 public interface FilterQueryProvider {
29     /**
30      * Runs a query with the specified constraint. This query is requested
31      * by the filter attached to this adapter.
32      *
33      * Contract: when constraint is null or empty, the original results,
34      * prior to any filtering, must be returned.
35      *
36      * @param constraint the constraint with which the query must
37      *        be filtered
38      *
39      * @return a Cursor representing the results of the new query
40      */
41     Cursor runQuery(CharSequence constraint);
42 }
View Code

就一个接口,所以我们需要在外面继承这个接口实现它,然后通过CursorAdapter的

 1 /**
 2      * Sets the query filter provider used to filter the current Cursor.
 3      * The provider's
 4      * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
 5      * method is invoked when filtering is requested by a client of
 6      * this adapter.
 7      *
 8      * @param filterQueryProvider the filter query provider or null to remove it
 9      *
10      * @see #getFilterQueryProvider()
11      * @see #runQueryOnBackgroundThread(CharSequence)
12      */
13     public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
14         mFilterQueryProvider = filterQueryProvider;
15     }
View Code

方法设置,之后整个机制完全串联起来,在外调用adapter.getFilter().filter(et_filter.getText().toString());  即可启动整个过滤机制。

其实上面说的和第2)种方法差不多,只不过,假如你已经决定使用CursorAdapter了,建议你使用以上方法实现更新,因为,它已经被实现好了,浑然天成。

三、AsyncQueryHandler

从单词表义来看,就是异步查询处理器。在Android中,异步线程似乎是天生的,基本上所有写Android的都会碰到Handler这个类,它就是异步的,用来通知主线程做事情。这个类和AsyncTask很像,都封装了一个操作的几个部分,操作完成之后,只不过,它局限于对ContentProvider进行操作。仔细研究一下这个类,上源码:

  1 /*
  2  * Copyright (C) 2007 The Android Open Source Project
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 package android.content;
 18 
 19 import android.database.Cursor;
 20 import android.net.Uri;
 21 import android.os.Handler;
 22 import android.os.HandlerThread;
 23 import android.os.Looper;
 24 import android.os.Message;
 25 import android.util.Log;
 26 
 27 import java.lang.ref.WeakReference;
 28 
 29 /**
 30  * A helper class to help make handling asynchronous {@link ContentResolver}
 31  * queries easier.
 32  */
 33 public abstract class AsyncQueryHandler extends Handler {
 34     private static final String TAG = "AsyncQuery";
 35     private static final boolean localLOGV = false;
 36 
 37     private static final int EVENT_ARG_QUERY = 1;
 38     private static final int EVENT_ARG_INSERT = 2;
 39     private static final int EVENT_ARG_UPDATE = 3;
 40     private static final int EVENT_ARG_DELETE = 4;
 41 
 42     /* package */ final WeakReference<ContentResolver> mResolver;
 43 
 44     private static Looper sLooper = null;
 45 
 46     private Handler mWorkerThreadHandler;
 47 
 48     protected static final class WorkerArgs {
 49         public Uri uri;
 50         public Handler handler;
 51         public String[] projection;
 52         public String selection;
 53         public String[] selectionArgs;
 54         public String orderBy;
 55         public Object result;
 56         public Object cookie;
 57         public ContentValues values;
 58     }
 59 
 60     protected class WorkerHandler extends Handler {
 61         public WorkerHandler(Looper looper) {
 62             super(looper);
 63         }
 64 
 65         @Override
 66         public void handleMessage(Message msg) {
 67             final ContentResolver resolver = mResolver.get();
 68             if (resolver == null) return;
 69 
 70             WorkerArgs args = (WorkerArgs) msg.obj;
 71 
 72             int token = msg.what;
 73             int event = msg.arg1;
 74 
 75             switch (event) {
 76                 case EVENT_ARG_QUERY:
 77                     Cursor cursor;
 78                     try {
 79                         cursor = resolver.query(args.uri, args.projection,
 80                                 args.selection, args.selectionArgs,
 81                                 args.orderBy);
 82                         // Calling getCount() causes the cursor window to be filled,
 83                         // which will make the first access on the main thread a lot faster.
 84                         if (cursor != null) {
 85                             cursor.getCount();
 86                         }
 87                     } catch (Exception e) {
 88                         Log.w(TAG, e.toString());
 89                         cursor = null;
 90                     }
 91 
 92                     args.result = cursor;
 93                     break;
 94 
 95                 case EVENT_ARG_INSERT:
 96                     args.result = resolver.insert(args.uri, args.values);
 97                     break;
 98 
 99                 case EVENT_ARG_UPDATE:
100                     args.result = resolver.update(args.uri, args.values, args.selection,
101                             args.selectionArgs);
102                     break;
103 
104                 case EVENT_ARG_DELETE:
105                     args.result = resolver.delete(args.uri, args.selection, args.selectionArgs);
106                     break;
107             }
108 
109             // passing the original token value back to the caller
110             // on top of the event values in arg1.
111             Message reply = args.handler.obtainMessage(token);
112             reply.obj = args;
113             reply.arg1 = msg.arg1;
114 
115             if (localLOGV) {
116                 Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1
117                         + ", reply.what=" + reply.what);
118             }
119 
120             reply.sendToTarget();
121         }
122     }
123 
124     public AsyncQueryHandler(ContentResolver cr) {
125         super();
126         mResolver = new WeakReference<ContentResolver>(cr);
127         synchronized (AsyncQueryHandler.class) {
128             if (sLooper == null) {
129                 HandlerThread thread = new HandlerThread("AsyncQueryWorker");
130                 thread.start();
131 
132                 sLooper = thread.getLooper();
133             }
134         }
135         mWorkerThreadHandler = createHandler(sLooper);
136     }
137 
138     protected Handler createHandler(Looper looper) {
139         return new WorkerHandler(looper);
140     }
141 
142     /**
143      * This method begins an asynchronous query. When the query is done
144      * {@link #onQueryComplete} is called.
145      *
146      * @param token A token passed into {@link #onQueryComplete} to identify
147      *  the query.
148      * @param cookie An object that gets passed into {@link #onQueryComplete}
149      * @param uri The URI, using the content:// scheme, for the content to
150      *         retrieve.
151      * @param projection A list of which columns to return. Passing null will
152      *         return all columns, which is discouraged to prevent reading data
153      *         from storage that isn't going to be used.
154      * @param selection A filter declaring which rows to return, formatted as an
155      *         SQL WHERE clause (excluding the WHERE itself). Passing null will
156      *         return all rows for the given URI.
157      * @param selectionArgs You may include ?s in selection, which will be
158      *         replaced by the values from selectionArgs, in the order that they
159      *         appear in the selection. The values will be bound as Strings.
160      * @param orderBy How to order the rows, formatted as an SQL ORDER BY
161      *         clause (excluding the ORDER BY itself). Passing null will use the
162      *         default sort order, which may be unordered.
163      */
164     public void startQuery(int token, Object cookie, Uri uri,
165             String[] projection, String selection, String[] selectionArgs,
166             String orderBy) {
167         // Use the token as what so cancelOperations works properly
168         Message msg = mWorkerThreadHandler.obtainMessage(token);
169         msg.arg1 = EVENT_ARG_QUERY;
170 
171         WorkerArgs args = new WorkerArgs();
172         args.handler = this;
173         args.uri = uri;
174         args.projection = projection;
175         args.selection = selection;
176         args.selectionArgs = selectionArgs;
177         args.orderBy = orderBy;
178         args.cookie = cookie;
179         msg.obj = args;
180 
181         mWorkerThreadHandler.sendMessage(msg);
182     }
183 
184     /**
185      * Attempts to cancel operation that has not already started. Note that
186      * there is no guarantee that the operation will be canceled. They still may
187      * result in a call to on[Query/Insert/Update/Delete]Complete after this
188      * call has completed.
189      *
190      * @param token The token representing the operation to be canceled.
191      *  If multiple operations have the same token they will all be canceled.
192      */
193     public final void cancelOperation(int token) {
194         mWorkerThreadHandler.removeMessages(token);
195     }
196 
197     /**
198      * This method begins an asynchronous insert. When the insert operation is
199      * done {@link #onInsertComplete} is called.
200      *
201      * @param token A token passed into {@link #onInsertComplete} to identify
202      *  the insert operation.
203      * @param cookie An object that gets passed into {@link #onInsertComplete}
204      * @param uri the Uri passed to the insert operation.
205      * @param initialValues the ContentValues parameter passed to the insert operation.
206      */
207     public final void startInsert(int token, Object cookie, Uri uri,
208             ContentValues initialValues) {
209         // Use the token as what so cancelOperations works properly
210         Message msg = mWorkerThreadHandler.obtainMessage(token);
211         msg.arg1 = EVENT_ARG_INSERT;
212 
213         WorkerArgs args = new WorkerArgs();
214         args.handler = this;
215         args.uri = uri;
216         args.cookie = cookie;
217         args.values = initialValues;
218         msg.obj = args;
219 
220         mWorkerThreadHandler.sendMessage(msg);
221     }
222 
223     /**
224      * This method begins an asynchronous update. When the update operation is
225      * done {@link #onUpdateComplete} is called.
226      *
227      * @param token A token passed into {@link #onUpdateComplete} to identify
228      *  the update operation.
229      * @param cookie An object that gets passed into {@link #onUpdateComplete}
230      * @param uri the Uri passed to the update operation.
231      * @param values the ContentValues parameter passed to the update operation.
232      */
233     public final void startUpdate(int token, Object cookie, Uri uri,
234             ContentValues values, String selection, String[] selectionArgs) {
235         // Use the token as what so cancelOperations works properly
236         Message msg = mWorkerThreadHandler.obtainMessage(token);
237         msg.arg1 = EVENT_ARG_UPDATE;
238 
239         WorkerArgs args = new WorkerArgs();
240         args.handler = this;
241         args.uri = uri;
242         args.cookie = cookie;
243         args.values = values;
244         args.selection = selection;
245         args.selectionArgs = selectionArgs;
246         msg.obj = args;
247 
248         mWorkerThreadHandler.sendMessage(msg);
249     }
250 
251     /**
252      * This method begins an asynchronous delete. When the delete operation is
253      * done {@link #onDeleteComplete} is called.
254      *
255      * @param token A token passed into {@link #onDeleteComplete} to identify
256      *  the delete operation.
257      * @param cookie An object that gets passed into {@link #onDeleteComplete}
258      * @param uri the Uri passed to the delete operation.
259      * @param selection the where clause.
260      */
261     public final void startDelete(int token, Object cookie, Uri uri,
262             String selection, String[] selectionArgs) {
263         // Use the token as what so cancelOperations works properly
264         Message msg = mWorkerThreadHandler.obtainMessage(token);
265         msg.arg1 = EVENT_ARG_DELETE;
266 
267         WorkerArgs args = new WorkerArgs();
268         args.handler = this;
269         args.uri = uri;
270         args.cookie = cookie;
271         args.selection = selection;
272         args.selectionArgs = selectionArgs;
273         msg.obj = args;
274 
275         mWorkerThreadHandler.sendMessage(msg);
276     }
277 
278     /**
279      * Called when an asynchronous query is completed.
280      *
281      * @param token the token to identify the query, passed in from
282      *            {@link #startQuery}.
283      * @param cookie the cookie object passed in from {@link #startQuery}.
284      * @param cursor The cursor holding the results from the query.
285      */
286     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
287         // Empty
288     }
289 
290     /**
291      * Called when an asynchronous insert is completed.
292      *
293      * @param token the token to identify the query, passed in from
294      *        {@link #startInsert}.
295      * @param cookie the cookie object that's passed in from
296      *        {@link #startInsert}.
297      * @param uri the uri returned from the insert operation.
298      */
299     protected void onInsertComplete(int token, Object cookie, Uri uri) {
300         // Empty
301     }
302 
303     /**
304      * Called when an asynchronous update is completed.
305      *
306      * @param token the token to identify the query, passed in from
307      *        {@link #startUpdate}.
308      * @param cookie the cookie object that's passed in from
309      *        {@link #startUpdate}.
310      * @param result the result returned from the update operation
311      */
312     protected void onUpdateComplete(int token, Object cookie, int result) {
313         // Empty
314     }
315 
316     /**
317      * Called when an asynchronous delete is completed.
318      *
319      * @param token the token to identify the query, passed in from
320      *        {@link #startDelete}.
321      * @param cookie the cookie object that's passed in from
322      *        {@link #startDelete}.
323      * @param result the result returned from the delete operation
324      */
325     protected void onDeleteComplete(int token, Object cookie, int result) {
326         // Empty
327     }
328 
329     @Override
330     public void handleMessage(Message msg) {
331         WorkerArgs args = (WorkerArgs) msg.obj;
332 
333         if (localLOGV) {
334             Log.d(TAG, "AsyncQueryHandler.handleMessage: msg.what=" + msg.what
335                     + ", msg.arg1=" + msg.arg1);
336         }
337 
338         int token = msg.what;
339         int event = msg.arg1;
340 
341         // pass token back to caller on each callback.
342         switch (event) {
343             case EVENT_ARG_QUERY:
344                 onQueryComplete(token, args.cookie, (Cursor) args.result);
345                 break;
346 
347             case EVENT_ARG_INSERT:
348                 onInsertComplete(token, args.cookie, (Uri) args.result);
349                 break;
350 
351             case EVENT_ARG_UPDATE:
352                 onUpdateComplete(token, args.cookie, (Integer) args.result);
353                 break;
354 
355             case EVENT_ARG_DELETE:
356                 onDeleteComplete(token, args.cookie, (Integer) args.result);
357                 break;
358         }
359     }
360 }
View Code

这里面实际上整个对ContentProvider的增删改查都是交给Handler来实现的,你只需要实现四个接口,告诉AsyncQueryHandler操作完毕之后需要干什么,然后调用相关的接口即可。所以查询的时候也可以通过它实现。

注意:根据项目经验,如果用户向搜索框中输入文字进行搜索,假设连续快速输入sz两个字母,那么你会先搜索s,然后搜索sz,我们假设一种极端情况:因为含有s的搜索结果很大概率上比sz多,假设搜索s的时间为10s,而搜索sz的时间只需要3s,你去更新界面,其实是3s后搜索sz的结果出现,再过7s搜索s的结果出现,而此时搜索框中的输入内容是sz,出现不正常的情况。所以我们在搜索sz之前要做的就是取消搜索s的任务,AsyncQueryHandler提供了该接口:

 1 /**
 2      * Attempts to cancel operation that has not already started. Note that
 3      * there is no guarantee that the operation will be canceled. They still may
 4      * result in a call to on[Query/Insert/Update/Delete]Complete after this
 5      * call has completed.
 6      *
 7      * @param token The token representing the operation to be canceled.
 8      *  If multiple operations have the same token they will all be canceled.
 9      */
10     public final void cancelOperation(int token) {
11         mWorkerThreadHandler.removeMessages(token);
12     }
View Code

来实现该功能,每一个操作都由一个Token进行标记,取消的时候也需要用这个标记进行。

四、总结

总体来说,除了暴力搜索方法,其余的方法大同小异,不论设计的多么复杂,无非是将搜索的过程放置在异步线程里面,搜索完毕之后更新界面,我们只要记住这一点即可。之所以写文章介绍后面几种方法,是因为既然Android已经帮我们设计好了使用异步线程查询的代码架构,我们直接用即可,肯定更加健壮,也省去重复造轮子的力气。

原文地址:https://www.cnblogs.com/lqminn/p/3093012.html