Android應用系列:手把手教你做一個小米通訊錄(附圖附源碼)


前言

  最近心血來潮,突然想搞點仿制品玩玩,很不幸小米成為我苦逼的第一個試驗品。既然雷布斯的MIUI挺受歡迎的(本人就是其的屌絲用戶),所以就拿其中的一些小功能做一些小demo來玩玩。小米的通訊錄大家估計用過小米的都清楚是啥子樣的,沒用過小米的也別着急,瞧瞧我的demo,起碼也有七八分相似滴。先上圖看效果

我是圖:

 

PS:吐槽一下,博客園上個圖真難,所以搞了個短點的gif上才沒失敗。。。。唉。。。

在這里僅僅是實現了邏輯交互的效果,並沒有點擊打電話的功能,因為也不難就懶得加了。。。

分析

我們說說這個東西主要的實現功能點在哪些?

1、輸入數據按首字母排序

2、查詢框輸入時快速更改數據顯示

3、右端首字母跟隨數據列表位置高亮顯示

4、滑動時,出現數據第一個字提示

解決方案

1、輸入數據按首字母排序

  這個怎么辦呢?畢竟一個android小小的一個軟件不可用把整個漢字拼音轉換庫放進去對吧,如果真的放進去,你覺得這樣用戶會買你的帳,對不起你的APP太大啦;所以Stop這個想法!但是我們換一個思路,既然漢字在計算機中的是以編碼的存放的,我們是不是可以再編碼這里面做點文章。就以GB2312來說,里面的漢字是按字典序排列的,就是越前面其聲母越靠前,例如第一個漢字編碼代表的就是“啊”這個字,那好辦了,我們只要把各個首字母的編碼獲取區間,然后對於任何一個漢字,判斷其是否在相對應區間中即可獲取該漢字的首字母。但是但是,對於多音字這個東西,大家當我沒說好吧???

附核心源碼:

/**
 *    GetPYUntl工具類提供根據漢字獲取首字母的功能 
 *    僅支持GB2312簡體漢字
 */
public class GetPYUntl {
    
    //簡體中文的編碼范圍從B0A1(45217)一直到F7FE(63486)
    private static final int BEGIN = 45217;
    private static final int END = 63486;
    
    //按照聲 母表示,這個表是在GB2312中的出現的第一個漢字,也就是說“啊”是代表首字母a的第一個漢字
    //i, u, v都不做聲母, 自定規則跟隨前面的字母
    //最后保持空格
    private static final char[] charTable = new char[]{'啊', '芭', '擦', '搭', '蛾', '發', 
                                                    '噶', '哈', '哈', '擊', '喀', '垃', '媽',
                                                    '拿', '哦', '啪', '期', '然', '撒', '塌',
                                                    '塌', '塌', '挖', '昔', '壓', '匝'};
    
    //對應首字母區間表 
    private static final char[] initialtable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G',
                                                    'H', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                                                    'Q', 'R', 'S', 'T', 'T', 'T', 'W', 'X', 'Y',
                                                    'Z'};
    
    //二十六個字母區間對應二十七個端點  
    //GB2312碼漢字區間十進制表示  
    private static int[] table = new int[27];
    
    //工具類初始化代碼塊,進行初始化操作
    static{
        for(int i = 0; i < 26; i++){
            table[i] = getGBValue(charTable[i]);//得到GB2312碼的首字母區間端點表,十進制。
        }
        table[26] = END;//區間表的結尾數值
    }
    
    /**
     * 根據漢字c獲取其對應的編碼
     * 將一個漢字(GB2312)轉換為十進制表示
     */
    private static int getGBValue(char c) {
        String str = c + "";
        try{
            byte[] bytes = str.getBytes("GB2312");
            if (bytes.length < 2){
                return 0;
            }
            return (bytes[0] << 8 & 0xff00) + (bytes[1] & 0xff);  
        } catch (Exception e){  
            return 0;  
        }
    }
    
    
    
    //----------------------對外調用方法區---------------------------------------//
    /**
     * 根據輸入的單漢字,返回首字母
     * @param ch 所需要查詢的漢字
     * @return 返回對應輸入漢字的首字母
     */
    public static char getFristWord(char ch){
        //ch為英文字符,小寫轉為大寫,大寫直接返回
        if(ch >= 'a' && ch <= 'z'){
            return (char)(ch - 'a' + 'A');
        }
        if(ch >= 'A' && ch <= 'Z'){
            return ch;
        }
        //若為漢字,獲取其區間值,並判斷是否在碼表的范圍中
        //若不是,返回&
        //若是,在碼表對其進行判斷
        int gb = getGBValue(ch);
        
        if(gb < BEGIN || gb > END){
            return '&';
        }
        int i;
        for(i = 0; i < 26; i++){//判斷匹配碼表區間,匹配到就break,判斷區間形如“[,)”
            if(gb >= table[i] && gb < table[i+1]){
                break;
            }
        }
        if(gb == END){//補齊GB2312區間最右端,就是首字母編碼值為END
            i = 25;
        }
        return initialtable[i];//在碼表區間中,返回首字母
    }
    
    /**
     * 根據輸入的漢字字符串,返回首字母字符串,內部機制依賴調用 getFristWord(char ch)方法。
     * @param str 所需要查詢的漢字字符串
     * @return 返回對應輸入漢字字符串的首字母字符串
     */
    public static String getFristWord(String str){
        String reslut = "";
        if(str != null || str != ""){//容錯操作,校驗輸入的字符串
            int length = str.length();
            for(int i = 0; i < length; i++){//分別對str的各個字符進行取首字母操作
                reslut += getFristWord(str.charAt(i));
            }
        }
        return reslut;
    }
    
    /**
     * 提供兩個字符串,根據其首字母字符串的排列順序
     * @param str1 
     * @param str2
     * @return true:str1位於str2前面,false:str1位於str2后面
     */
    public static int Compare(String str1, String str2){
        String cstr1 = getFristWord(str1);
        String cstr2 = getFristWord(str2);
        return cstr1.compareTo(cstr2);
    }
}

2、查詢框輸入時快速更改數據顯示

  這個實現比較簡單,主要是用到TextWatcher這個類來監聽EditText的內容變化,如果我們在輸入查詢內容,TextWatcher會監聽EditText的狀態改變,調用onTextChanged()方法響應。

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
    dealData.clear();//清空上次的query查詢處理后的數據,用於加載新一次數據
    if(data != null && !"".equals(s.toString())){//query有內容,顯示根據query處理后的數據
        for(int i = 0; i < data.size(); i++){//編譯顯示內容,將符合的數據收集到dealData
            String str = data.get(i);
            //判斷s是否為str的子字符串,如果是,則是需要收集的數據,s為query當前輸入的內容
            boolean find = str.contains(s.toString());
            if(find){
                dealData.add(str);
            }
        }
        //更新listView的數據源
        adapter.setData(dealData, s.toString());
        //刷新listView
        list.invalidateViews();
    }
    else{//數據集合為空,或者不存在查詢的query請求
        adapter.setData(data, s.toString());
        list.invalidateViews();//刷新listView為初始狀態
    }
}

但是!這個東西實現的最重要邏輯並不是TextWatcher的代碼,而是我們重寫的BaseAdapter里面的getView方法。因為我們要根據查詢內容高亮顯示字體,所以我們的ListView的item對應的View需要着重處理,但是這樣會引入大量的View,估計教育我們要記得重用View,所以我在getView里面也大量用重用思想,減少資源占用率。

由於邏輯比較復雜,說以簡略說說實現思路吧:BaseAdapter會根據是否有查詢請求的接入,如果沒有查詢請求則是顯示全部數據,那么我們會在每一個convertView里面add一個TextView的實例,顯示全部內容。如果有查詢請求,BaseAdapter會整理要顯示的數據,然后在convertView里面add若干個TextView,每一個TextView將會只顯示一個字,在對應需要高亮的TextView會進行設置字體顏色。但是這樣會導致很多TextView的存在,所以我們在每一個convertView里面會設置緩存緩存這些TextView的引用,如果下次復用時直接從Tag中取得TextView進行處理后add到convertView里面顯示,然后清理掉多余的緩存。如果你看懵了不要緊,注釋非常詳細,可以過一下代碼。

核心代碼:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    List<TextView> cache = null;
    //進行convertView的復用,如果第一次使用,則實例化該View
    if(convertView == null){
        convertView = inflater.inflate(R.layout.mark_list_item, null, false);
        //申請一個緩沖cache用於保存其子TextView的引用
        cache = new ArrayList<TextView>();
        
        TextView tv = null;
        if(query == null || "".equals(query)){//如果query為null,或者為空字符串,則直接加載
            tv = new TextView(context);
            tv.setText(data.get(position));//將其整個字符串設置到TextView里面,而不單字加載
            setTextParmas(tv);
            ((LinearLayout)convertView).addView(tv);//加載到convertView節點下
            cache.add(tv);//緩存集合中保存tv的引用,便於下次復用該TextView
        }
        else{//query不為null,其有query內容,我們需要將該查詢內容對於的字符高亮顯示
            //將顯示內容的字符串拆分為字符數組,用於單字加載
            char[] words = data.get(position).toCharArray();
            for(int i = 0; i < words.length; i++){
                tv = new TextView(context);
                tv.setText(words[i]+"");//單字加載,即一個字用一個TextView顯示
                setTextParmas(tv);
                //查詢是否在query的內容之中,如果為是則將該TextView高亮顯示
                for(char c : query){
                    if(c == words[i])
                        tv.setTextColor(Color.RED);
                }
                ((LinearLayout)convertView).addView(tv);
                cache.add(tv);
            }
        }
    }
    else{//如果不是第一次使用,則該convertView可以被復用
        cache = (List<TextView>) convertView.getTag();//獲取緩存cache集合
        TextView tv;
        int usingSize = 0;//記錄當前convertView所有使用的TextView個數
        int length = cache.size();
        if(query == null || "".equals(query)){//如果query為null,或者為空字符串,則直接加載
            if(length > 0){//檢測cache是否存在可復用的View
                tv = cache.get(0);//取首個TextView復用
                tv.setTextColor(Color.GRAY);
            }
            else{//如果沒有可復用的TextView,實例化一個TextView並加入緩存中
                tv = new TextView(context);
                setTextParmas(tv);
                ((LinearLayout)convertView).addView(tv);
                cache.add(tv);
            }
            tv.setText(data.get(position));
            usingSize++;
        }
        else{//query不為null,其有query內容,我們需要將該查詢內容對於的字符高亮顯示
            //將顯示內容的字符串拆分為字符數組,用於單字加載
            char[] words = data.get(position).toCharArray();
            for(int i = 0; i < words.length; i++){
                if(i < length){//獲取第i個緩存的TextView
                    tv = cache.get(i);
                    tv.setTextColor(Color.GRAY);
                }
                else{//如果已經沒有緩存TextView可獲取了
                    tv = new TextView(context);
                    setTextParmas(tv);
                    ((LinearLayout)convertView).addView(tv);
                    cache.add(tv);
                }
                //查詢是否在query的內容之中,如果為是則將該TextView高亮顯示
                for(char c : query){
                    if(c == words[i])
                        tv.setTextColor(Color.RED);
                }
                tv.setText(words[i]+"");//單字加載,即一個字用一個TextView顯示
                usingSize++;
            }
        }
        clearCache(convertView, cache, usingSize);
    }
    convertView.setTag(cache);//保存緩存cache集合
    firstWord.add(position, cache.get(0).getText().toString().charAt(0));
    return convertView;
}

3、右端首字母跟隨數據列表位置高亮顯示

這個實現的思路主要用到OnScrollListener這個監聽接口,其中onScroll()這個方法會在滾動時調用,那么我們在滾動listView的時候就可以在這里操作首字母的高亮顯示了。

@Override
public void onScroll(AbsListView view, int firstVisibleItem,
        int visibleItemCount, int totalItemCount) {
    ListViewAdapter adapter = (ListViewAdapter)(view.getAdapter());
    if(adapter != null){//在首次加載listview時會調用onScroll(),那時候的adapter為null,容錯處理
        char c1 = adapter.getFristWord(firstVisibleItem + 4);
        char c2 = GetPYUntl.getFristWord(c1);
        TextView tv = tvMaps.get(c2+"");
        if(tv != null){
            if(oldTv != null)//如果前次已經有TextView高亮顯示,取消高亮效果
                oldTv.setTextColor(Color.GRAY);
            tv.setTextColor(Color.RED);//設置高亮效果
            centerTextView.setText(c1+"");
            oldTv = tv;
        }
    }
}

4、滑動時,出現數據第一個字提示

這個做的不是那么好,如果快速多次滑動還是會出現一些問題。如果大家有興趣就整改一下哈,還是用到OnScrollListener這個監聽接口里面的onScrollStateChanged方法,邏輯也簡單,在滾動時設置顯示的TextView可見,在停止滾動時啟動定時器2秒后取消TextView可見。但是對於多次快速滾動來說,這個方法還需要微調一下。

核心代碼

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    
    //構造一個定時器,用於延時2000毫秒取消顯示
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            centerTextView.setVisibility(View.INVISIBLE);
        }
    };
    Handler handler = new Handler();
    
    switch (scrollState){
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
        //如果手指還接觸界面,則取消定時,主要用於第二次后觸發事件使用
        if(centerTextView.getVisibility() == View.VISIBLE)
            handler.removeCallbacks(runnable);
        break;
    case OnScrollListener.SCROLL_STATE_FLING:
        //如果手指離開屏幕,屏幕滑動,設置centerTextView可見
        centerTextView.setVisibility(View.VISIBLE);
        //如果第二次手指離開屏幕,屏幕滑動,則取消定時,主要用於第二次后觸發事件使用
        if(centerTextView.getVisibility() == View.VISIBLE)
            handler.removeCallbacks(runnable);
        break;
    case OnScrollListener.SCROLL_STATE_IDLE:
        //啟動定時任務,於2秒后取消顯示
        handler.postDelayed(runnable, 1000);
        break;
    }
    
}

后記

由於代碼量太多,也不好仔細講實現的邏輯,那么一篇博客肯定寫不完,我又比較懶,不喜歡折騰好幾個博客來寫,所以我在代碼里面進行了詳盡的注釋,希望你參考demo能看懂這些功能的實現思路。

源碼請戳(這里

 

作者:enjoy風鈴
出處:http://www.cnblogs.com/net168/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則下次不給你轉載了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM