天天看點

Android造輪子---聯系人快速索引

聯系人快速索引,可以根據右邊索引導航來定位具體拼音首字母的資料,下面是效果圖:

Android造輪子---聯系人快速索引

實作主要使用三個View:

1、ListView:負責展示聯系人資料

2、右邊的索引IndexView(自定義View):負責處理使用者點選或滑動事件,根據事件坐标值定位相應的字母索引,并通知索引事件監聽者

3、正中間的目前索引拼音首字母提示View

IndexView實作原理:

1、根據控件寬度和高度計算出每個字母所占的區域大小

2、通過Paint.getTextBounds()計算出每個字母本身的寬高

3、根據計算出的控件寬高,計算出每個字母需要繪制的左下角坐标(用于文字繪制)

4、計算出每個字母有效的點選/觸摸區域坐标(用Rect儲存,後面可以通過Rect.contains()方法來判斷某個坐标是否在該區域内)

5、被點選/觸目到的字母區域用另外一個Paint來繪制通過不同畫筆顔色來區分

6、通過回調接口通知事件監聽者索引的變化

【注意】要注意擷取控件寬高的時機,這裡是在onMeasure()方法被調用時擷取。

LisetView的定位原理:

1、資料需根據拼音進行排序

2、當IndexView索引發生變化,需要在回調函數裡将所有資料的首字母拼音與回調的拼音進行比較,進而擷取到具體的資料索引

3、通過ListView.setSelection(int index)方法來定位到具體資料索引的位置

索引拼音首字母提示View:

1、當IndexView索引發生變化,在回調函數裡設定目前字母資料并控制View的顯示和隐藏

下面開始貼代碼,首先是布局檔案:

contacts.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/lv_contacts"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <TextView
        android:id="@+id/tv_abc"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerInParent="true"
        android:textSize="18sp"
        android:gravity="center"
        android:textColor="#FFFFFF"
        android:background="#88333333"
        android:visibility="gone"/>

    <com.log.anotherapp.customView.IndexView
        android:id="@+id/index_words"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:background="#88ff0000"
        android:layout_alignParentRight="true"/>

</RelativeLayout>
           

contacts_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#C3BCBB"
        android:gravity="center_vertical"
        android:paddingLeft="20dp"
        android:textStyle="bold"
        android:textSize="18sp" />
    
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical"
        android:paddingLeft="20dp"
        android:textSize="16sp" />

</LinearLayout>
           

IndexView.java

package com.log.anotherapp.customView;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import com.log.anotherapp.util.DensityUtil;

public class IndexView extends View {

    private String[] words = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    // 儲存需繪制的文字寬高
    private Pair<Integer, Integer>[] wordSizes = new Pair[words.length];
    // 每個文字的坐下角坐标
    private Pair<Float, Float>[] wordCoordinates = new Pair[words.length];
    // 每個文字的有效區域(用于判斷是否點選或滑動該文字區域,做高亮顯示)
    private Rect[] wordRects = new Rect[words.length];
    // 常态文字的paint
    private Paint paint;
    // 高亮文字的paint
    private Paint selectedPaint;
    // 每一個字母的寬度和高度
    private float itemWidth;
    private float itemHeight;
    // 目前字母索引
    private int currentIndex = -1;
    // 繪制的文本字型大小
    private int textSize;
    private OnIndexListener listener;
    // 是否已經計算過字型大小和坐标,防止重複計算
    private boolean isNotComputed = true;

    public IndexView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 系統調用這個方法後,本控件才測量出寬和高
        itemWidth = getMeasuredWidth();
        itemHeight = getMeasuredHeight() / words.length;
    }

    /**
     * 設定字型大小
     *
     * @param textSize
     */
    public void setTextSize(int textSize) {
        this.textSize = textSize;
    }

    private void init() {
        // 預設字型大小
        if (textSize <= 0) {
            textSize = DensityUtil.sp2px(getContext(), 18);
        }

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setTextSize(textSize);
        paint.setTypeface(Typeface.DEFAULT_BOLD); // 粗體字
        paint.setColor(Color.WHITE);

        selectedPaint = new Paint();
        selectedPaint.setAntiAlias(true);
        selectedPaint.setTextSize(textSize);
        selectedPaint.setTypeface(Typeface.DEFAULT_BOLD); // 粗體字
        selectedPaint.setColor(Color.GRAY);
    }

    private void compute() {
        Rect rect = new Rect();
        for (int i = 0; i < words.length; i++) {
            // 計算文字寬高
            paint.getTextBounds(words[i], 0, 1, rect);
            Pair pair = new Pair(rect.width(), rect.height());
            wordSizes[i] = pair;

            // 計算每個字的左下角x和y坐标
            float x = (itemWidth - wordSizes[i].first) / 2.f;
            float y = (itemHeight + wordSizes[i].second) / 2.f + itemHeight * i;
            wordCoordinates[i] = new Pair<>(x, y);

            // 計算文字點選/觸摸有效區域
            wordRects[i] = new Rect(0, (int) (i * itemHeight), (int) itemWidth, (int) ((i + 1) * itemHeight));
        }
        isNotComputed = false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (isNotComputed) {
            compute();
        }

        // 高亮文字的畫筆
        Paint currentPaint;
        for (int i = 0; i < words.length; i++) {
            if (i == currentIndex) {
                currentPaint = selectedPaint;
            } else {
                currentPaint = paint;
            }
            canvas.drawText(words[i], wordCoordinates[i].first, wordCoordinates[i].second, currentPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < wordRects.length; i++) {
                    // 判斷點選坐标落在哪個字母上
                    if (wordRects[i].contains((int) x, (int) y)) {
                        // 目前選中索引
                        currentIndex = i;

                        invalidate();

                        if (listener != null) {
                            listener.onIndexChange(words[i]);
                        }
                        break;
                    }
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                currentIndex = -1;
                invalidate();
                if (listener != null) {
                    listener.onIndexRelease();
                }
                break;
        }
        return true;
    }

    public void setOnIndexListener(OnIndexListener listener) {
        this.listener = listener;
    }

    public interface OnIndexListener {
        void onIndexChange(String text);

        void onIndexRelease();
    }
}
           

PinYinUtil.java

package com.log.anotherapp.util;

import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;

public class PinYinUtil {

    public static String getPinYin(String chineseWord) {
        String pinyin = "";

        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();//控制轉換是否大小寫,是否帶音标
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);//大寫
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

        //由于不能直接對多個漢字轉換,隻能對單個漢字轉換
        char[] arr = chineseWord.toCharArray();
        for (int i = 0; i < arr.length; i++) {
            if (Character.isWhitespace(arr[i])) continue;//如果是空格,則不處理,進行下次周遊

            //漢字是2個位元組存儲,肯定大于127,是以大于127就可以當為漢字轉換
            if (arr[i] > 127) {
                try {
                    //由于多音字的存在,單 dan shan
                    String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(arr[i], format);

                    if (pinyinArr != null) {
                        pinyin += pinyinArr[0];
                    } else {
                        pinyin += arr[i];
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                    //不是正确的漢字
                    pinyin += arr[i];
                }
            } else {
                //不是漢字,
                pinyin += arr[i];
            }
        }
        return pinyin;
    }
}
           

ContactsActivity.java

package com.log.anotherapp;

import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

import androidx.annotation.Nullable;

import com.log.anotherapp.customView.IndexView;
import com.log.anotherapp.model.Contacts;
import com.log.anotherapp.util.PinYinUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class ContactsActivity extends BaseActivity implements IndexView.OnIndexListener {

    private ListView listView;
    private IndexView indexView;
    private TextView textView;
    private Handler handler;
    private List<Contacts> contacts;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.contacts);
        initView();
    }

    private void initView() {
        handler = new Handler();
        textView = findViewById(R.id.tv_abc);
        listView = findViewById(R.id.lv_contacts);
        indexView = findViewById(R.id.index_words);
        indexView.setOnIndexListener(this);
        initData();
        ContactsAdapter adapter = new ContactsAdapter();
        listView.setAdapter(adapter);
    }

    private void initData() {
        contacts = new ArrayList<>();

        contacts.add(new Contacts("阿一", PinYinUtil.getPinYin("阿一")));
        contacts.add(new Contacts("奧利奧", PinYinUtil.getPinYin("奧利奧")));
        contacts.add(new Contacts("布爾", PinYinUtil.getPinYin("布爾")));
        contacts.add((new Contacts("曹操", PinYinUtil.getPinYin("曹操"))));
        contacts.add((new Contacts("曹尼瑪", PinYinUtil.getPinYin("曹尼瑪"))));
        contacts.add((new Contacts("董卿", PinYinUtil.getPinYin("董卿"))));
        contacts.add((new Contacts("厄尼", PinYinUtil.getPinYin("厄尼"))));
        contacts.add((new Contacts("凡妮莎", PinYinUtil.getPinYin("凡妮莎"))));
        contacts.add((new Contacts("高手", PinYinUtil.getPinYin("高手"))));
        contacts.add((new Contacts("韓紅", PinYinUtil.getPinYin("韓紅"))));
        contacts.add((new Contacts("花果", PinYinUtil.getPinYin("花果"))));
        contacts.add((new Contacts("InDon", PinYinUtil.getPinYin("InDon"))));
        contacts.add((new Contacts("節操", PinYinUtil.getPinYin("節操"))));
        contacts.add((new Contacts("康嘉", PinYinUtil.getPinYin("康嘉"))));
        contacts.add((new Contacts("岚岚", PinYinUtil.getPinYin("岚岚"))));
        contacts.add((new Contacts("瑪尼", PinYinUtil.getPinYin("瑪尼"))));
        contacts.add((new Contacts("能升", PinYinUtil.getPinYin("能升"))));
        contacts.add((new Contacts("歐洋", PinYinUtil.getPinYin("歐洋"))));
        contacts.add((new Contacts("盼盼", PinYinUtil.getPinYin("盼盼"))));
        contacts.add((new Contacts("錢途", PinYinUtil.getPinYin("錢途"))));
        contacts.add((new Contacts("讓龍", PinYinUtil.getPinYin("讓龍"))));
        contacts.add((new Contacts("詩人", PinYinUtil.getPinYin("詩人"))));
        contacts.add((new Contacts("唐僧", PinYinUtil.getPinYin("唐僧"))));
        contacts.add((new Contacts("ULove", PinYinUtil.getPinYin("ULove"))));
        contacts.add((new Contacts("VLog", PinYinUtil.getPinYin("VLog"))));
        contacts.add((new Contacts("王帝", PinYinUtil.getPinYin("王帝"))));
        contacts.add((new Contacts("現金", PinYinUtil.getPinYin("現金"))));
        contacts.add((new Contacts("媛媛", PinYinUtil.getPinYin("媛媛"))));
        contacts.add((new Contacts("正宗", PinYinUtil.getPinYin("正宗"))));

        // 按照拼音排序
        Collections.sort(contacts, new Comparator<Contacts>() {
            @Override
            public int compare(Contacts o1, Contacts o2) {
                return o1.getPinyin().compareTo(o2.getPinyin());
            }
        });
    }

    @Override
    public void onIndexChange(String text) {
        textView.setText(text);
        textView.setVisibility(View.VISIBLE);

        // 查找指定的拼音首字母在聯系人數組的哪個索引
        // 由于數組已經排序,隻要找到第一個比對的就可以了
        int index = -1;
        for (int i = 0; i < contacts.size(); i++) {
            if (contacts.get(i).getPinyin().substring(0,1).equalsIgnoreCase(text)) {
                index = i;
                break;
            }
        }

        // 跳轉到ListView指定位置
        if (index >= 0) {
            listView.setSelection(index);
        }
    }

    @Override
    public void onIndexRelease() {
        // 延遲一秒再隐藏
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                textView.setVisibility(View.GONE);
            }
        }, 1000);
    }

    class ContactsAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return contacts == null ? 0 : contacts.size();
        }

        @Override
        public Object getItem(int position) {
            return contacts == null ? null : contacts.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                holder = new ViewHolder();
                convertView = LayoutInflater.from(getContext()).inflate(R.layout.contacts_item, null);
                holder.title = convertView.findViewById(R.id.tv_title);
                holder.content = convertView.findViewById(R.id.tv_content);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            Contacts data = contacts.get(position);
            if (position == 0) {
                holder.title.setText(data.getPinyin().substring(0, 1));
                holder.title.setVisibility(View.VISIBLE);
            } else {
                // 比較上一個是否與目前的拼音首字母是否一樣
                if (data.getPinyin().charAt(0) == contacts.get(position - 1).getPinyin().charAt(0)) {
                    holder.title.setVisibility(View.GONE);
                } else {
                    holder.title.setText(data.getPinyin().substring(0, 1));
                    holder.title.setVisibility(View.VISIBLE);
                }
            }
            holder.content.setText(data.getName());

            return convertView;
        }

        class ViewHolder {
            TextView title;
            TextView content;
        }
    }

}
           

因為用到了Pinyin4j,需要在build.gradle裡添加依賴:

// https://mvnrepository.com/artifact/org.clojars.cbilson/pinyin4j
    implementation group: 'org.clojars.cbilson', name: 'pinyin4j', version: '2.5.0'