簡單的帶歷史記錄的搜索功能實現

這兩天閑來無事,就把前短時間項目中的搜索功能抽取出來,重新寫一下,搜索功能雖然簡單,但是設計到得知識點也挺多的,就當做一個總結吧。
代碼地址在最后面,話不多說,上效果圖 _

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和可以優化的地方還是希望大家能夠留言,你們都將是菜雞成長路上的恩師。

最后附上菜雞的主頁,記得來逛逛哦。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容