第二十七章 搜索
有关SearchView
SearchView是一个内置在工具栏的操作视图,其添加方式和第十三章中的方式一样,在resource中创建menu下的资源文件,只不过要特别加上一句app:actionViewClass以告诉工具栏要显示SearchView:
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_item_search" android:title="@string/search" </menu>
showAsAction和actionVIewClass属性都需要app的命名空间。
SearchView有有关OnQueryTextListener的监听器,在提交搜索时调用onQueryTextSubmit接口,在搜索框中文字发生了改变时调用onQueryTextChange接口:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
Log.i(TAG,"QueryTextSumbit: "+s);
QueryPreferences.setStoredQuery(getActivity(),s);
updateItems();
return true;
}
@Override
public boolean onQueryTextChange(String s) {
Log.i(TAG,"QueryTextChange: "+s);
return false;
}
});
有关shared preferences
PreferenceManager getDefaultSharedPreferences在Android Q中已弃用,需要使用AndroidX支持库版本,即androidx.preference.PreferenceManager而不是android.preference.PreferenceManager 。
因为没有特殊的需求去定制,直接使用默认的preferences:
public class QueryPreferences {
private static final String PREF_SEARCH_QUERY="searchQuery";
public static String getStoredQuery(Context context)
{
return PreferenceManager.getDefaultSharedPreferences(context)
.getString(PREF_SEARCH_QUERY,null);
}
public static void setStoredQuery(Context context,String query)
{
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(PREF_SEARCH_QUERY,query)
.apply();
}
}
preferences中维护SearchView的文本,就可以实现长期储存搜索栏的文字了。
有关挑战练习 深度优化PhotoGallery应用
先解决一些遗留的Bug
首先是在提交搜索结果之后,应用出现的前若干项依旧是第一次搜索时的结果,原因在于FetchItemsTask中下载图片的基本消息(不是图片本身)时,onPostExecute将所下载的结果合并到原先的结果中去了,现在要根据查询的结果来调整是合并结果还是直接覆盖:
private class FetchItemsTask extends AsyncTask<Void,Void,List<GalleryItem>>
{
private String mQuery;
public FetchItemsTask() {
}
public FetchItemsTask(String query) {
mQuery = query;
}
@Override
protected List<GalleryItem> doInBackground(Void ...integer) {
if (mQuery==null)
{
return new FlickrFetchr().fetchRecentPhotos(nowPage++);
}
else
{
return new FlickrFetchr().searchPhotos(mQuery);
}
}
@Override
protected void onPostExecute(List<GalleryItem> galleryItems) {
if (mQuery==null) mItems.addAll(galleryItems);
else mItems=galleryItems;
setupAdapter();
mTextView.setText(text1);
}
}
其次是上一章节的挑战练习中预下载一些图片,可能会出现多次滑动后导致一张图片被多次下载,原因是没有将下载的图片存到map中去,需要调整预下载的方法以存储正在下载的图片的信息,这时候需要每一张预下载图片的ViewHolder的实例,通过recyclerView 的findViewHolderForAdapterPosition方法来获取:
PhotoHolder photoHolder=(PhotoHolder)recyclerView.findViewHolderForAdapterPosition(position);
预下载方法的调整:
@Override protected void onLooperPrepared() { mRequestHandler=new Handler() { @Override public void handleMessage(@NonNull Message msg) { if (msg.what==MESSAGE_DOWNLOAD) { T target=(T)msg.obj; handleRequest(target); } else if (msg.what==PRE_DOWNLOAD) { T target=(T)msg.obj; handlePreDownload(target); } } }; } public void queuePreDownload(T target,String url) { if (url==null) return; else if (mRequestMap.get(target)==url||mHasQuit) return; else { mRequestMap.put(target,url); mRequestHandler.obtainMessage(PRE_DOWNLOAD,target).sendToTarget(); } } private void handlePreDownload(final T target) { try { final String url=mRequestMap.get(target); if (url==null) { return; } byte[] bitmapByte=new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap= BitmapFactory.decodeByteArray(bitmapByte,0,bitmapByte.length); mResponseHandler.post(new Runnable() { @Override public void run() { if (mRequestMap.get(target)!=url||mHasQuit) { return; } mRequestMap.remove(target); mThumbnailDownloaderListener.onPreDownloaded(bitmap,url); } }); } catch (IOException ioe) { } }
在recyclerView的滑动监听器中调用这个方法:
@Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); ... for (int i=1;i<=10;i++) { int position=lastCompletelyVisibleItemPosition+i; if (position<=totalItemCount) { GalleryItem galleryItem=mItems.get(position); Bitmap bitmap = getBitmapFromCache(galleryItem.getUrl()); if (bitmap != null) {//有缓存 } else {//没有缓存 PhotoHolder photoHolder=(PhotoHolder)recyclerView.findViewHolderForAdapterPosition(position); if (photoHolder!=null) { mThumbnailDownloader.queuePreDownload(photoHolder,galleryItem.getUrl()); } } } } }
还有一个Bug,是我在下载底下的页面时,迅速滑动到上面已经下载好了的页面,此时下载好的底下的图片会加载到上面去。这并不是缓存出现了问题,因为上面的页面用缓存重新加载之后图片资源也是对应的,我认为是下面的图片下载完之后绑定到视图上的过程(target.bindGalleryItem(drawable);)出现了错误,我的猜测是RecyclerView更新视图资源只能更新可见的ViewHolder,对于并没有显示出来的ViewHolder,要想让其绑定图片资源,是不可行的(做不到),而绑定视图资源的命令已经发出,即便不对应,也只能绑定到错误的ViewHolder上。当然这只是我的猜测,由于AVD根本没办法实现图片的下载,我也无从获取错误的原因,所以这个bug就只能暂时先放在这里。
本章的挑战练习
收起键盘、收起SearchView视图其实就在SearchVIew的onQueryTextSubmit中加一行代码就行了,参考资料:
searchView.onActionViewCollapsed();
onActionViewExpanded方法:
初始SearchView是否已经是展开的状态
写上此句后searchView初始展开的,也就是是可以点击输入的状态,如果不写,那么就需要点击下放大镜,才能展开出现输入框。
同理,onActionViewCollapsed正好相反。
添加加载状态,由于我没找到什么事状态指示器,所以就用一个dialog来代替了,借鉴的是别人做的模板,不过这个人做的还是小有问题,根据评论区进行了一番修改,
bulid.gradle:
dependencies {
compile 'com.wang.avi:library:2.1.3'
}
dialog_loading.xml:
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/avi"
style="@style/AVLoadingIndicatorView.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:indicatorColor="#FF0000"
app:indicatorName="LineSpinFadeLoaderIndicator" />
</RelativeLayout>
style.xml:
<style name="TransparentDialog" parent="@android:style/Theme.Holo.Light.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
</style>
LoadingDialog.java:
public class LoadingDialog extends AlertDialog {
private AVLoadingIndicatorView avi;
public LoadingDialog(Context context, int themeResId) {
super(context,themeResId);
}
// @Override
// protected void onCreate(Bundle savedInstanceState) {
// super.onCreate(savedInstanceState);
// this.setContentView(R.layout.dialog_loading);
// avi = (AVLoadingIndicatorView)this.findViewById(R.id.avi);
// }
@Override
protected void onStart() {
super.onStart();
this.setContentView(R.layout.dialog_loading);
avi = this.findViewById(R.id.avi);
}
@Override
public void show() {
super.show();
avi.show();
}
@Override
public void dismiss() {
super.dismiss();
avi.hide();
}
}
使用时,在每一个开启下载JSON数据的地方调用show方法,即在private void updateItems()中:
private void updateItems()
{
String query=QueryPreferences.getStoredQuery(getActivity());
new FetchItemsTask(query).execute();
mLoadingDialog.show();
}
在处理完下载之后调用dismiss方法,即在onPostExecute中:
@Override
protected void onPostExecute(List<GalleryItem> galleryItems) {
if (mQuery==null) mItems.addAll(galleryItems);
else mItems=galleryItems;
mLoadingDialog.dismiss();
setupAdapter();
mTextView.setText(text1);
}
效果图: