对于这一章来说,我们来学习搜索的功能
一.想要有搜索的功能,那么就需要使用使用SearchView来整合搜索功能,按住SearchView,用户可以输入关键字,提交查询请求来搜索Flicker,返回结果将显示在RecyclerView中,用户提交过的关键字会被保留下来
二.想要对一个网址进行搜索,那么就首先需要知道该网址的搜索方法
就像搜索Flickr网址就需要使用flickr.photos.search方法,下面的搜索cat文本的GET请求
http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=xxx&format=json&nojsoncallback=1&text=cat
虽然搜索URL和图片请求URL不同,但是网站返回来的JSON数据是一样的,这样我们就可以使用相同的JSON数据解析逻辑
(1)首先,重构FlickrFetchr代码以后面可以复用JSON,逻辑解析逻辑,先添加一些URL复用相关的常量,先把之前的fetchItems方法中剪切URL创建代码复制作为ENDPOINT的值,这个常量不应包含查询方法参数,也不应该使用toString()方法
private static final String FETCH_RECENTS_METHOD = "flickr.photos.getRecent";
private static final String SEARCH_METHOD = "flickr.photos.search";
private static final String Uri ENDPOINT = Uri.parse("http://api.flickr.com/services/rest/").buildUpon().appendQueryParameter("api_key",API_KEY).appendQueryParameter("format","json").appendQueryParameter("nojsoncallback","1").appendQueryParameter("extras","url_s").build();
(2)为了通用,我们需要将fetchItem方法改为downloadGalleryItem
private List<GalleryItem> downloadGalleryItem(String url)
这个方法使用了url的参数,也就不用再创建URL了
(3)在网络类里面新加一个方法用来基于用户功能和查询值创建URL,如下面的代码
private String buildUrl(String method,String query) {
Uri.Builder uriBuilder = ENDPOINT.buildUpon().appendQueryParameter("method",method);
if(methid.equals(SEARCH_METHOD)){
uriBuilder.appendQueryParameter("text",query);
}
return uriBuilder.build().toString();
}
buildUrl方法会自动拼接必要的参数,不过参数是固定的,如果发现是搜索,那么它就会附加一个text的值
(4)然后在网络类里面根据不同的需求建立不同的方法
//下载图片的方法
public List<GalleryItem> fetchRecentPhotos(){
String url = buildUri(FETCH_RECENTS_METHOD,null);
return downloadGalleryItem(url);
}
//搜索图片并下载的方法
public List<GalleryItem> searchPhotos(String query){
String url = buildUri(FETCH_RECENTS_METHOD,query);
return downloadGalleryItem(url);
}
(5)FlickrFetchr的重构应该体现在fragment的代码中,在PhotoGalleryFragment中,更新FetchItemsTask类的代码
在doInBackground方法中
String query = "robot";
if(query ==null){
return new FlickrFetchr().fetchRecentPhotos();
}else{
return new FlickrFetch().searchPhotos(query);
}
现在如果运行的话就会看到一两张机器人的图片
三.使用SearchView
(1)现在就来用SearchView创建搜索界面,让用户输入查询关键字并触发搜索
SearchView是一个操作视图,所谓的操作视图就是可以内置在工具栏中的视图,SearchView可以让整个搜索界面完全内置在应用的工具栏中,但是首先需要确认顶部有工具栏
然后在res/menu/fragment_photo_gallery.xml文件中,为PhotoGalleryFragment创建一个新的菜单XML文件,可以通过这个文件指定工具栏要显示什么,
//下面代码是用来告诉工具栏要显示SearchView的
<item android:id="@id/menu_item_search"
android:title="@string/search"
app:actionViewClass="android.support.v7.widgetSearchView"
app:showAsAction="ifRoom"/>
(2)在PhotoGalleryFragment文件中,在onCreate()方法中调用setHasOptionsMenu(true)方法来让fragment接受菜单回调方法,然后覆盖onCreateOptionsMenu()方法并实例化菜单XML文件
public void onCreateOptionsMenu(Menu menu,MenuInflater menuInflater) {
super.onCreateOptionsMenu(menu,menuInflater);
menuInflater.inflate(R.menu.fragment_photo_gallery,menu);
}
现在点击Search按钮,那么就会出现一个提供用户输入的文本框,SearchView展开后一个 x的按钮会出现在右边,点击它会删除用户输入的文字,再次点击它,就会回到只有一个搜索按钮的界面
(3)响应用户搜索
用户提交查询后,应用立刻搜索网站,然后刷新显示搜索结果,由开发文档可以知道SearchView.OnQueryTextListener接口已经提供了接受回调的方式,可以响应查询指令
在onCreateOptionsMenu方法中添加接受回调的方式
public void onCreateOptionsMenu(Menu menu,MenuInflater menuInflater) {
super.onCreateOptionsMenu(menu,menuInflater);
menuInflater.inflate(R.menu.fragment_photo_gallery,menu);
MenuItem searchView = menu.findItem(R.id.menu_item_search);//我们先从菜单中取出MenuItem并把它放在searchItem变量中,然后使用getActionView方法从这个变量去取出SearchView的对象
final SearchView searchView = (SearchView)searchItem.getActionView();
searchView.setQueryTextListener(new SearchView.OnQueryTextListener(){ //取出对象后就可以来设置接口
public boolean onQueryTextSubmit(String s) {//用户提交搜索查询后这个回调方法就会执行,用户提交的搜索字符串也会传给它,搜索请求受理后这个方法就会返回true
Log.d(TAG,"dd" + s);
updateItem();
return true;
}
public boolean onQueryTextChange(String s) {//当文本框里面的文字有变换时这个方法的回调方法就会执行,
Log.d(TAG,"dd" + s);
return false;
}
});
}
//下面是一个调用FetchItemsTask的封装方法
private void updateItem(){
new FetchItemsTask().execute();
}
现在运行应用后虽然图片重新加载了,但是搜索的结果仍然是基于代码清单中的硬编码搜索字符串
(4)用shared preferences实现轻量级的数据存储
现在删除硬编码搜索字符串,我们在实现用户在SearchView中输入并提交查询的指令,在这个应用中一次只有一个激活的查询,应用应该保存这个查询,即使应用或设备重启也不会丢,要实现这个目标就需要把查询字符串写入shared preferences中,只要用户提交查询,就把它写入shared preferences覆盖之前的记录字符串,实际搜索时就是在shared preferences中去取出查询字符串,把它作为text的参数值
shared preferences 本质上就是文件系统中的文件,可以使用SharedPreferences类去读写它,SharedPreference实例用起来更像一个键值对仓库,但是它可以通过持久化存储保存数据,键值对中的键就是字符串,而值是原子数据类型,进一步查看这个文件可以知道它实际上就是一种简单的XML文件,
在实际开发中我们只要这个对象可以共享于整个应用就可以了,这种情况下,最好使用PreferenceManager.getDefaultSharedPreferencrs(Context)方法,该方法会返回私有的权限和默认名称的实例
(5)新建立一个QueryPreferences的类,用于读取和写入查询
public class QueryPreferences {
private static final String PREF_SEARCH_QUERY = "searchQuery"//作为查询字符串的存储key
//取出字符串值
public static String getStoredQuery(Context context) {
return PreferenceManager.getDefaultSharedPreference(context).getString(PREF_SEARCH_QUERY,null);
}
//向指定的context的默认shared preferences写入查询值,
public static void setStoredQuery(Context context,String query) {
PreferenceManaget.getDefaultSharedPrefences(context).edit().putString(PREF_SEARCH_QUERY,query).apply();
}
}
在上面的代码中,取出什么值就调用什么方法,如取出字符串就使用getString()方法,如果找不到它的值,那么就返回null,
而对于写入时,调用SharedPreferences.edit()方法就可以获取一个SharedPreference.Editor实例,它是sharefreferences中保存查询信息要用到的类,完成数据变更后,调用SharedPreferences.Editor的apple()异步方法写入数据,这样Sharepreference文件的其他用户就可以看到写入的数据了,apple()方法首先在内存中执行数据变更然后在后台线程上真正把数据写入文件
(6)存储用户提交的查询信息
public void onCreateOptionsMenu(Menu menu,MenuInflater menuInflater) {
super.onCreateOptionsMenu(menu,menuInflater);
menuInflater.inflate(R.menu.fragment_photo_gallery,menu);
MenuItem searchView = menu.findItem(R.id.menu_item_search);//我们先从菜单中取出MenuItem并把它放在searchItem变量中,然后使用getActionView方法从这个变量去取出SearchView的对象
final SearchView searchView = (SearchView)searchItem.getActionView();
searchView.setQueryTextListener(new SearchView.OnQueryTextListener(){ //取出对象后就可以来设置接口
public boolean onQueryTextSubmit(String s) {//用户提交搜索查询后这个回调方法就会执行,用户提交的搜索字符串也会传给它,搜索请求受理后这个方法就会返回true
Log.d(TAG,"dd" + s);
QueryPreferences.setStoredQuery(getActivity,s);//这里就是保存用户提交的查询信息
updateItem();
return true;
}
public boolean onQueryTextChange(String s) {//当文本框里面的文字有变换时这个方法的回调方法就会执行,
Log.d(TAG,"dd" + s);
return false;
}
});
}
(7)用户在溢出菜单中清除存储的查询信息(也就是设置为null)
在onOptionsItemsItemSelected()中添加选项和代码
case R.id.menu_item_clear:
QueryPreferences.setStoredQuery(getActivity(),null);
updateItems();
return true;
更新完查询信息后,updateItem()方法就会被调用,这可以确保显示最新的搜索结果
(8)更新FetchItemsTask,以使用保存的查询字符串,在这个类中添加一个定制的构造方法,用于接收查询信息并保存在一个成员变量中备用,更新update()从shared Preferences中取出保存的查询信息,用它创建一个FetchItemTask的实例
private void updateItem(){
String query = QueryPreferences.getStoredQuery(getActivity());
new FetchItemsTask(query).execute();
}
下面的代码是在FetchItemsTask中添加的
private String mQuery;
public FetchItemsTasksTask(String query) {
mQuery = query;
}
现在运行应用尝试一些结果就可以看见返回的东西了
(9)而此时如果当用户点开搜索文本框能显示已经保存的查询字符串就不错了,其实当用户点击搜索按钮的时候,SearchView的View.OnClickListener.onClick()方法就会调用,那么此时利用这个回调方法来设置搜索文本框、
search.setOnSearchClickListener(new View.OnClickListener(){
public void onClick(View v) {
String query = QueryPreferences.getStoredQuery(getActiviy());
searchView.setQuery(query,false);
}
})