我在【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 }
这边有几点比较有意思,值得注意:
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 }
这个类继承了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 }
查询操作又交给了另外一个对象: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 }
更新完毕以后调用该方法刷新列表。
现在假设你要去使用这样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 }
就一个接口,所以我们需要在外面继承这个接口实现它,然后通过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 }
方法设置,之后整个机制完全串联起来,在外调用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 }
这里面实际上整个对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 }
来实现该功能,每一个操作都由一个Token进行标记,取消的时候也需要用这个标记进行。
四、总结
总体来说,除了暴力搜索方法,其余的方法大同小异,不论设计的多么复杂,无非是将搜索的过程放置在异步线程里面,搜索完毕之后更新界面,我们只要记住这一点即可。之所以写文章介绍后面几种方法,是因为既然Android已经帮我们设计好了使用异步线程查询的代码架构,我们直接用即可,肯定更加健壮,也省去重复造轮子的力气。