Java安卓学习总结(三十一)

第二十七章 搜索

有关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);

}

效果图:

发表评论