阅读前请下载ApiDemos工程代码。
LoaderCustom类中的自定义AppListLoader的写法其实和android类CursorLoader的写法基本一样,可见自定义Loader的实现可以说是有模板的,这也为轻松的实现自定义Loader提供了基础。
自定义Loader的实现步骤相对简单,只需要做如下几件事即可:
1) 自定义Loader类必须继承于AsyncTaskLoader类,AsyncTaskLoader类提供了大部分的Loader管理工作;当然了,如果你很牛X,你也可以不用这么做。
2) 需要实现如下基本的回调方法,以AppListLoader为例:
上面回调的细节描述后文会介绍。
3) 监听数据源的变化。
CursorLoader中使用cursor.registerContentObserver(mObserver)来监听数据源变化,当数据源发生变化的时候,Loader类的onContentChanged()方法会被调用。
而AppListLoader类的加载数据是从PackageManager中获取的所有已安装的应用程序信息,我们通过直接操作PackageManager来获取数据,因此对于数据源的变化,此处是通过一个BroadcastReceiver来监听应用程序包的安装情况,当收到onReceive()回调的时候,我们直接调用Loader类的onContentChanged()方法,这样就和CursorLoader的行为就保持一致了。
下面我们先看第一步AppListLoader的继承情况:
public static class AppListLoader extends AsyncTaskLoader<List<AppEntry>>
首先继承自AsyncTaskLoader,数据类型为List<AppEntry>,AppEntry也是自定义的内部类,用来保存应用程序的信息。
接着依次看看这几个回调方法的细节:
1) loadInBackground
/**
* 此方法是Loader在后台加载大量数据的地方。这个方法工作在后台线程中,而且你要做的就是加载新的数据,并且返回给调用者.
*/
@Override public List<AppEntry> loadInBackground() {
// 获得所有已安装的应用程序信息.
List<ApplicationInfo> apps = mPm.getInstalledApplications(
PackageManager.GET_UNINSTALLED_PACKAGES |
PackageManager.GET_DISABLED_COMPONENTS);
if (apps == null) {
apps = new ArrayList<ApplicationInfo>();
}
final Context context = getContext();
// Create corresponding array of entries and load their labels.
List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
for (int i=0; i<apps.size(); i++) {
AppEntry entry = new AppEntry(this, apps.get(i));
entry.loadLabel(context);
entries.add(entry);
}
// Sort the list.
Collections.sort(entries, ALPHA_COMPARATOR);
// Done!
return entries;
}
这个方法主要工作就是加载后台数据。因此,在这里你可以用自己的方法去加载想要获得的数据,但是切记,此方法工作于后台线程,因此不要和视图组件有任何交互。
2)deliverResult
/**
* 当需要把新的数据传递给用户的时候调用. 父类会处理具体传递细节,
*我们实现这个回调主要是为了添加额外的逻辑处理。
*/
@Override public void deliverResult(List<AppEntry>apps) {
if (isReset()) {
// 能够进到这个判断条件里,说明一个异步加载正在进行的时候,用户已经
// 停止了这个Loader的加载,所以返回的新数据不需要再传递给用户了,
//那么直接清理了,onReleaseResources()是自定义清理数据的方法,
//是个空方法,此处只是个示例。
if (apps != null) {
onReleaseResources(apps);
}
//原工程代码中没有return语句,但是从理论上以及参考CursorLoader
//类,应该写上return。因为毕竟新数据是无用的,那么当然要直接返回了。
return;
}
List<AppEntry> oldApps = mApps;
mApps = apps;
if (isStarted()) {
// 如果用户已经调用了startLoading,但是没有调用stopLoading
// 说明应该把数据传递给用户。
super.deliverResult(apps);
}
//到这里我们已经传递了新数据,那么旧数据自然无用了,所以直接清理掉.
if (oldApps != null) {
onReleaseResources(oldApps);
}
}
3) onStartLoading
/**
*这个回调通知我们Loader准备开始加载了。此方法必须在UI线程中调用。
*/
@Override protected void onStartLoading() {
if (mApps != null) {
// 如果当前已经有可用的数据,那么直接传递这些数据.
deliverResult(mApps);
}
// 开始监控数据源的变化,主要是注册一个BroadcastReceiver。
if (mPackageObserver == null) {
mPackageObserver = new PackageIntentReceiver(this);
}
// Has something interesting in the configuration changed since we
// last built the app list?
boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
if (takeContentChanged() || mApps == null || configChange) {
// 如果源数据从上次加载以来已经发生了变化,那么就强制重新加载一次。
// 这里有一个方法takeContentChanged(),值得一说,还记得前面
// 说得Loader的nContentChanged()方法吗,那个方法可能会设置相关
//标记,这样这个条件就为真了。
forceLoad();
}
}
4) onStopLoading
/**
* 处理停止加载的请求.
*/
@Override protected void onStopLoading() {
// 只需简单地调用下面方法即可,表示退出加载.
cancelLoad();
}
5) onCanceled
/**
*在加载完成前,处理一个退出加载的请求。
*/
@Override public void onCanceled(List<AppEntry> apps) {
super.onCanceled(apps);
// 这个时候需要清理从LoadInBackground返回的数据
onReleaseResources(apps);
}
6) onReset
/**
*处理重置Loader的请求
*/
@Override protected void onReset() {
super.onReset();
// 首先确保停止加载
onStopLoading();
//如果当前有数据,那么清理掉.
if (mApps != null) {
onReleaseResources(mApps);
mApps = null;
}
// 关闭对数据源的监听.
if (mPackageObserver != null) {
getContext().unregisterReceiver(mPackageObserver);
mPackageObserver = null;
}
}
接着再看一下对数据源的监听以及当发生变化时如何通知Loader。看下面的代码:
/**
* Helper class to look for interesting changes to the installed apps
* so that the loader can be updated.
*/
public static class PackageIntentReceiver extends BroadcastReceiver {
final AppListLoader mLoader;
public PackageIntentReceiver(AppListLoader loader) {
mLoader = loader;
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mLoader.getContext().registerReceiver(this, filter);
// Register for events related to sdcard installation.
IntentFilter sdFilter = new IntentFilter();
sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
// 1) 在此处注册,实现监听
mLoader.getContext().registerReceiver(this, sdFilter);
}
@Override public void onReceive(Context context, Intent intent) {
// 2)此处通知Loader,数据源有变化,这样在适当情况下,Loader会重新加载数据
mLoader.onContentChanged();
}
}
以上就是实现一个自定义Loader的基本步骤。和CursorLoader的实现过程基本一样。运行本例子,效果图如下:
点击右上角的搜索图标,并输入“帮”字,效果图如下:
但是如果输入“储”,结果图如下:
我第一次看到,觉得很奇怪,按理来说,如果输入“储”,应该显示“安全存储”,“拨号器存储”等结果啊。后来一查找到了原因,首先查看一下搜索图标的回调事件处理:
@Override public boolean onQueryTextChange(String newText) {
// Called when the action bar search text has changed. Since this
// is a simple array adapter, we can just have it do the filtering.
mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
mAdapter.getFilter().filter(mCurFilter);
return true;
}
原来调用了mAdatper的filter方法,mAdapter是一个自定义的Adapter:
public static class AppListAdapter extends ArrayAdapter<AppEntry> {
本质上讲mAdapter就是一个ArrayAdatper<AppEntry>,当调用filter()方法时,实际上调用了ArrayAdapter里的私有内部类ArrayFilter的performFiltering方法,这个方法就是把数据元素(此处是AppEntry)的toString()返回值与参数进行匹配,如果这个参数是那个toString()的前缀,那么就显示这个结果。
在本例中,AppEntry类的toString()方法如下:
@Override public String toString() {
return mLabel;
}
返回是mLable值,也就是应用的名字。当输入“储”的时候,并没有任何一个名字是以“储”为前缀的,所以自然没有结果显示了。而实际上,我觉得只要匹配名字中任何一个字符就应该显示出来,大部分用户应该都会希望是这样的,所以我把ArrayAdapter拿来修改了一番就实现了搜索的模糊查找:
1) 拷贝adt-bundle-windows-x86-20140321\sdk\sources\android-19\android\widget\ArrayAdapter.java到目录\adt-bundle-windows-x86-20140321\sdk\samples\android-19\legacy\ApiDemos\src\com\example\android\apis\widget下;
2) 修改ArrayAdapter.java的内容:、
将两处valueText.startWith更改valueText.contains;
3)修改LoaderCustom.java的包导入路径:
import com.example.android.apis.widget.ArrayAdapter;
//import android.widget.ArrayAdapter;
编译运行,然后再输入“储”,看到的结果图如下: