简单的带历史记录的搜索功能实现

这两天闲来无事,就把前短时间项目中的搜索功能抽取出来,重新写一下,搜索功能虽然简单,但是设计到得知识点也挺多的,就当做一个总结吧。
代码地址在最后面,话不多说,上效果图 _

searchdemo.gif

历史记录的存储

首先来说下关于历史记录的存储,历史记录的存储方式其实可以有很多方法,可以用sp,数据库等等,那么就直接开撸吧。说开撸你还真以为就直接开撸了,还是先想想吧,我们做历史记录存储的时候需要提供什么给调用者,其实很简单无非就是可以增删改查吗。为了遵守里氏替换原则,就先写了个抽象类BaseHistoryStorage,里面有几个抽象方法,至于是什么自己看吧。这样不管你是用SP还是数据库甚至其他更牛的技术,只需要集成Base类,实现这些抽象方法就行了,至于你是怎么实现的,我才不管呢。

/**
 * 历史信息存储基类
 * Created by Zellerpooh on 17/1/18.
 */

public abstract class BaseHistoryStorage {
    /**
     * 保存历史记录时调用
     *
     * @param value
     */
    public abstract void save(String value);

    /**
     * 移除单条历史记录
     *
     * @param key
     */
    public abstract void remove(String key);

    /**
     * 清空历史记录
     */
    public abstract void clear();

    /**
     * 生成key
     *
     * @return
     */
    public abstract String generateKey();

    /**
     * 返回排序好的历史记录
     *
     * @return
     */
    public abstract ArrayList<SearchHistoryModel> sortHistory();
}

上面的代码很好理解,SearchHistoryModel是我写的一个JavaBean,里面就放了两个String,一个是历史搜索的内容,一个是历史记录的Key,其实你直接返回一个String泛型的ArrayList的就行,但是我这里为了用SP实现的时候跟快速偷了个懒,好了自己去实现一个历史记录存储功能把。

通过SharedPreference实现数据存储

听到让你自己去实现是不是心凉了一半,当然是逗你的了,既然都来了怎么能不给你点福利呢,下面我就实现一个简单的通过SharedPreference实现的数据存储吧,来抛砖迎玉吧。


 private static SpHistoryStorage instance;

 private SpHistoryStorage(Context context, int historyMax) {
        this.context = context.getApplicationContext();
        this.HISTORY_MAX = historyMax;
    }

 public static synchronized SpHistoryStorage getInstance(Context context, int historyMax) {
        if (instance == null) {
            synchronized (SpHistoryStorage.class) {
                if (instance == null) {
                    instance = new SpHistoryStorage(context, historyMax);
                }
            }
        }
        return instance;
    }

作为一个励志成为高逼格的高级程序员的菜鸟,当然不会放过任何装逼的机会,对于这种比较耗资源的数据存储,将他设计为单例模式当然最合适不过了,上面就是一个简单的DCL的单例模式实现,在安卓中将Context传入到单例中是一个大忌,你试想一下你的activity永远得不到释放是一件多么恐怖的事情,所以我就换成了applicationContext,反正他是跟随程序一直在的。
然后来看看里面的方法实现吧

 private static SimpleDateFormat mFormat = new SimpleDateFormat("yyyyMMddHHmmss");
 @Override
    public String generateKey() {
        return mFormat.format(new Date());
    }

上面这个方法就是为了存储的时候根据当前时间生成的一个Key,用来判断先后顺序。

  
    @Override
    public ArrayList<SearchHistoryModel> sortHistory() {
        Map<String, ?> allHistory = getAll();
        ArrayList<SearchHistoryModel> mResults = new ArrayList<>();
        Map<String, String> hisAll = (Map<String, String>) getAll();
        //将key排序升序
        Object[] keys = hisAll.keySet().toArray();
        Arrays.sort(keys);
        int keyLeng = keys.length;
        //这里计算 如果历史记录条数是大于 可以显示的最大条数,则用最大条数做循环条件,防止历史记录条数-最大条数为负值,数组越界
        int hisLeng = keyLeng > HISTORY_MAX ? HISTORY_MAX : keyLeng;
        for (int i = 1; i <= hisLeng; i++) {
            mResults.add(new SearchHistoryModel((String) keys[keyLeng - i], hisAll.get(keys[keyLeng - i])));
        }
        return mResults;
    }

 public Map<String, ?> getAll() {
        SharedPreferences sp = context.getSharedPreferences(SEARCH_HISTORY,
                Context.MODE_PRIVATE);
        return sp.getAll();
    }

这个方法就是从SharedPreferences取出所有的值并且按照时间先后进行排序。


  @Override
    public void save(String value) {
        Map<String, String> historys = (Map<String, String>) getAll();
        for (Map.Entry<String, String> entry : historys.entrySet()) {
            if (value.equals(entry.getValue())) {
                remove(entry.getKey());
            }
        }
        put(generateKey(), value);
    }

保存的方法,需要先判断是否已经存在,存在的话就先删除然后根据最新的时间保存。剩下两个移除和清空的方法就自己看吧。

 @Override
    public void remove(String key) {
        SharedPreferences sp = context.getSharedPreferences(SEARCH_HISTORY,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.remove(key);
        editor.commit();
    }

  @Override
 public void clear() {
        SharedPreferences sp = context.getSharedPreferences(SEARCH_HISTORY,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.clear();
        editor.commit();
    }

好了,通过sharedPreferences存储历史记录的功能就这样完成了,但是心里一想好久没有写数据库相关的代码了,就简单的撸了一个通过数据库存储的方法,篇幅有限代码就不贴了,想看的点这里。其实这里的代码借鉴了remusic的搜索记录存储的实现,有什么问题找他去→_→。

界面的实现

界面的实现就比较朴素了,毕竟我们都是比较注重内在的人,代码就不贴了,上面一个EditText,下面一个ListView再带上一个清空的按钮。界面写好了之后呢,先给ListView撸个adapter吧,也很简单继承BaseAdapter,实现下方法,然后暴露个接口出来,用于单条历史记录被点击和删除的时候用。

public interface OnSearchHistoryListener {
    void onDelete(String key);

    void onSelect(String content);
}
 holder.layoutClose.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onSearchHistoryListener != null) {
                    onSearchHistoryListener.onDelete(mHistories.get(position).getTime());
                }
            }
        });
        holder.layout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onSearchHistoryListener != null) {
                    onSearchHistoryListener.onSelect(mHistories.get(position).getContent());
                }
            }
        });

数据绑定

界面撸完了,接着就需要将数据和界面联系起来了,这里就不得不再推荐一下MVP模式了,Model和View都由中间层Presenter来控制,使得逻辑看起来变得很清晰,所以这里就用MVP模式写了,其实我是怕自己把各位绕糊涂。

public interface SearchPresenter {

    void remove(String key);

    void clear();

    void sortHistory();

    void search(String value);
}

public interface SearchView {
    void showHistories(ArrayList<SearchHistoryModel> results);

    void searchSuccess(String value);
}

public interface SearchModel {

    void save(String value);

    void search(String value,OnSearchListener onSearchListener);

    void remove(String key);

    void clear();

    void sortHistory(OnSearchListener onSearchListener);
}

presenter要做的事情就是,当View被操作后,通知它去做数据操作,即增删改查,而Model只需要在presenter告诉它做什么的时候去做就行,成功之后再回调presenter去通知View,这里View就只需要两个操作,搜索成功后界面的切换以及历史记录的显示。那接下来就来实现一下model,presenter和View。

public class SearchPresenterImpl implements SearchPresenter, OnSearchListener             {
    private static final int historyMax = 10;
    private SearchView searchView;
    private SearchModel searchModel;

    public SearchPresenterImpl(SearchView searchView, Context context) {
        this.searchView = searchView;
        this.searchModel = new SearchModelImpl(context, historyMax);
    }

    //移除历史记录
    @Override
    public void remove(String key) {
        searchModel.remove(key);
        searchModel.sortHistory(this);
    }

    @Override
    public void clear() {
        searchModel.clear();
        searchModel.sortHistory(this);
    }

    //获取所有的历史记录
    @Override
    public void sortHistory() {
        searchModel.sortHistory(this);
    }

    @Override
    public void search(String value) {
        searchModel.save(value);
        searchModel.search(value, this);
    }

    @Override
    public void onSortSuccess(ArrayList<SearchHistoryModel> results) {
        searchView.showHistories(results);
    }

    @Override
    public void searchSuccess(String value) {
        searchView.searchSuccess(value);
    }
}

在初始化presenter的同时引用了View和Model,然后实现OnSearchListener当model完成操作是回调view中的方法。代码自己看吧,应该没有任何疑问。


public class SearchModelImpl implements SearchModel {

    private BaseHistoryStorage historyStorage;

    public SearchModelImpl(Context context, int historyMax) {
//        historyStorage = SpHistoryStorage.getInstance(context, historyMax);
        historyStorage = DbHistoryStorage.getInstance(context, historyMax);
    }

    @Override
    public void save(String value) {
        historyStorage.save(value);
    }

    @Override
    public void search(String value, OnSearchListener onSearchListener) {
        onSearchListener.searchSuccess(value);
    }

    @Override
    public void remove(String key) {
        historyStorage.remove(key);
    }

    @Override
    public void clear() {
        historyStorage.clear();
    }

    @Override
    public void sortHistory(OnSearchListener onSearchListener) {
        onSearchListener.onSortSuccess(historyStorage.sortHistory());
    }
}

model中的内容就简单了,创建一个前面实现的BaseStorage对象,对数据进行操作,这里没有对搜索做什么处理直接通过回调返回了传进来的字符串,在实际开发中应该是去请求接口,返回参数,所以在View中也没有做具体的处理,实际开发中可以打开一个新的页面后者,切换列表显示搜索到的内容。

 @Override
    public void showHistories(ArrayList<SearchHistoryModel> results) {
        llSearchEmpty.setVisibility(0 != results.size() ? View.VISIBLE : View.GONE);
        searchHistoryAdapter.refreshData(results);
    }

    @Override
    public void searchSuccess(String value) {
        Toast.makeText(this, value, Toast.LENGTH_SHORT).show();
    }

上面就是View中实现的方法,获得历史记录是,告诉adapter去刷新列表就行了。接下来就只剩下View中一些简单的点击事件的处理了,搜索的时候调用mSearchPresenter.search(value);,清空的时候调用mSearchPresenter.clear();是不是感觉so easy,妈妈再也不用担心我的学习了,当然别忘了presenter需要在activity的onCreate方法中进行实例化。
最后呢,再给大家介绍几个技巧:

  1. 不要忘记把搜索框EditText设置成Search模式 android:imeOptions="actionSearch"
    设置完以后不要忘记对键盘上的搜索按钮的监听
etSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    search(etSearch.getText().toString());
                    return true;
                }
                return false;
            }
        });

  1. 我们看到很多软件都会做模糊搜索的操作,你一输入列表就会弹出很多相关的词汇供你点击,顿时感觉好贴心哦,其实这个功能要实现也不能,通过给editText设置监听以及Handler延时发送消息就能够实现了。
    private TextWatcher textWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            //在500毫秒内改变时不发送
            if (mHandler.hasMessages(MSG_SEARCH)) {
                mHandler.removeMessages(MSG_SEARCH);
            }
            if (TextUtils.isEmpty(s)) {
                llSearchHistory.setVisibility(View.VISIBLE);
                mSearchPresenter.sortHistory();
            } else {
                llSearchHistory.setVisibility(View.GONE);
                //否则延迟500ms开始模糊搜索
                Message message = mHandler.obtainMessage();
                message.obj = s;
                message.what = MSG_SEARCH;
                mHandler.sendMessageDelayed(message, 500); //自动搜索功能 删除
            }
        } };//模糊搜索 private static final int MSG_SEARCH = 1;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
        search(etSearch.getText().toString().trim());
        }  
    };

啰嗦了这么半天,最后还是附上代码吧,searchBar
有什么Bug和可以优化的地方还是希望大家能够留言,你们都将是菜鸡成长路上的恩师。

最后附上菜鸡的主页,记得来逛逛哦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容