自定義 View 之聯(lián)系人字母索引及定位效果

博主聲明:

轉(zhuǎn)載請?jiān)陂_頭附加本文鏈接及作者信息屋摔,并標(biāo)記為轉(zhuǎn)載芹彬。本文由博主 威威喵 原創(chuàng),請多支持與指教贡未。

本文首發(fā)于此 博主威威喵 | 博客主頁https://blog.csdn.net/smile_running

我們來實(shí)現(xiàn)這樣的一個小Demo熊榛,聯(lián)系人的快速檢索功能锚国,左側(cè)帶有字母快速定位,滑動列表時(shí)字母相繼切換的效果玄坦。那么我們來看看實(shí)現(xiàn)的Demo血筑,顯而易見,這是使用列表控件煎楣,而我使用的是ListView控件豺总。至于ListView的一些用法,您可以看這博主的這篇文章ListView使用技巧择懂、優(yōu)化和用法拓展喻喳,掌握ListView,這篇文章我們就不說明它的使用姿勢了

效果圖

image
聯(lián)系人界面

想來想去沸枯,我就搞了個王者榮耀的英雄名稱來作為我們的聯(lián)系人。因?yàn)槟M器上也沒有聯(lián)系人赂弓,也懶得搞绑榴,手機(jī)的話這個真實(shí)的聯(lián)系人也就不放出來,這也是個人隱私嘛盈魁。搞這么多英雄的名字還挺累的翔怎,大家將就著看,其實(shí)原理都一樣杨耙。所以赤套,這不是我們的關(guān)鍵,重要的是如何實(shí)現(xiàn)這種效果呢珊膜?

  • 思路及實(shí)現(xiàn)

一容握、左側(cè)字母檢索器

左邊是一個自定義View,通過獲得屏幕的高度车柠,將26個字母逐一從上往下繪制剔氏。通過點(diǎn)擊事件繪制不同的顏色,再通過監(jiān)聽右邊列表的滑動檢索字母選擇器中的相對應(yīng)字母下標(biāo)竹祷,然后繪制改變字母的顏色谈跛。

二、列表顯示字母分類

列表添加數(shù)據(jù)塑陵,主要說一下頭部的字母分類實(shí)現(xiàn)感憾。通過獲取漢字的原拼音,再檢索出首字母令花。在適配器中判斷當(dāng)前字母是否與前一個字母相同阻桅。相同則歸類,不相同則分類兼都。

三鳍刷、中間的字母提示

這個應(yīng)該是最簡單的了,在我們的自定義View里通過接口回調(diào)獲取點(diǎn)擊或滑動切換時(shí)所顯示的字母俯抖,然后在設(shè)置到一個TextView上输瓜,通過定時(shí)器將它延遲隱藏。

大致思路就是這樣了芬萍,其實(shí)自己使勁的想一想尤揣,還是可以實(shí)現(xiàn)出這樣的效果的,也不是特別難柬祠。思路講完了北戏,那么我們就上代碼吧,由于代碼比較多漫蛔,所以這里只貼比較核心的地方來看看嗜愈,若有需要請到我的Github上查看完整的項(xiàng)目旧蛾。

一、左側(cè)字母檢索器

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class ContactIndexView extends View {
    private int mRealWidth;
    private int mRealHeight;
    private int mWidth;
    private int mHeight;
    private int mEachHeight;

    private int mTouchIndex = 0;

    private Paint mPaint;
    private Rect mRect;
    private onShowLetterListener onShowLetterListener = null;

    private int colorBackground;
    private int colorNormal;
    private int colorChecked;

    public interface onShowLetterListener {
        void showLatter(String letter);
    }

    public void setOnShowLetter(onShowLetterListener showLetterListener) {
        this.onShowLetterListener = showLetterListener;
    }

    private String[] letters = new String[]{"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"};

    @TargetApi(Build.VERSION_CODES.M)
    private void init() {
        EventBus.getDefault().register(this);
        colorBackground = getContext().getResources().getColor(R.color.colorDivider, null);
        colorNormal = getContext().getResources().getColor(R.color.colorWhite, null);
        colorChecked = getContext().getResources().getColor(R.color.colorPrimaryDark, null);

        mRect = new Rect();
        mPaint = new Paint();
        mPaint.setStrokeWidth(3f);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(38f);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (widthMode) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.EXACTLY:
                mRealWidth = widthSize;
                break;
        }
        switch (heightMode) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.EXACTLY:
                mRealHeight = heightSize;
                break;
        }
        setMeasuredDimension(mRealWidth, mRealHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(colorBackground);
        mWidth = canvas.getWidth();
        mHeight = canvas.getHeight() - getStatusbarHeight();
        mEachHeight = mHeight / letters.length;

        for (int i = 0; i < letters.length; i++) {
            final String _latter = letters[i];
            mPaint.getTextBounds(_latter, 0, 1, mRect);
            final int letterWidth = mRect.width();
            final int letterHeight = mRect.height();

            if (mTouchIndex == i) {
                mPaint.setColor(colorChecked);
            } else {
                mPaint.setColor(colorNormal);
            }
            canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight - letterHeight / 2, mPaint);
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                refreshLetterIndex(y);
                break;
            case MotionEvent.ACTION_MOVE:
                refreshLetterIndex(y);
                break;
        }
        return true;
    }

    @Subscribe
    public void onListScrollEvent(ScrollEvent event) {
        if (event.isLast()) {
            return;
        }
        for (int i = 0; i < letters.length; i++) {
            if (event.getLetter().equals(letters[i])) {
                mTouchIndex = i;
                invalidate();
                return;
            }
        }
    }

    private void refreshLetterIndex(int y) {
        //y坐標(biāo) / 每個字母高度 = 當(dāng)前字母下標(biāo)
        int index = y / mEachHeight;
        if (index != mTouchIndex) {
            mTouchIndex = index;
            //回調(diào)選中的字母
            if (onShowLetterListener != null) {
                onShowLetterListener.showLatter(letters[mTouchIndex]);
            }
            invalidate();
        }
    }

    private int getStatusbarHeight() {
        int resId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
        return resId > 0 ? getContext().getResources().getDimensionPixelSize(resId) : 0;
    }
}

代碼挺長的蠕嫁,最重要的是計(jì)算字母所在的x锨天,y坐標(biāo),坐標(biāo)計(jì)算準(zhǔn)了剃毒,才可以繪制到控件的中心去病袄,否則你會發(fā)現(xiàn)字母將會偏右或偏左等,顯示效果比較難看赘阀。

            final String _latter = letters[i];
            mPaint.getTextBounds(_latter, 0, 1, mRect);
            final int letterWidth = mRect.width();
            final int letterHeight = mRect.height();

這段代碼片段益缠,它主要是給文本繪制了一個邊框,然后我們從邊框的寬基公、高間接得到文本的寬和高幅慌。

二、列表顯示字母分類

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class MainActivity extends AppCompatActivity {

    private ListView mListView;
    private ContactIndexView mIndexView;
    private TextView mShowTextView;

    private CountDownTimer mCountDownTimer;
    private ContactAdapter mAdapter;
    private ArrayList<Contact> mContacts;
    private String mLetter;

    private static final String[] NAME = new String[]{
            "露娜", "李白", "韓信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "關(guān)羽", "張飛", "劉備", "貂蟬", "呂布", "王昭君", "武則天",
            "百里守約", "百里玄策", "司馬懿", "孫策", "干將莫邪", "裴擒虎", "張良", "諸葛亮", "達(dá)摩", "蒙奇", "曹操", "鐘馗", "鐘無艷",
            "程咬金", "米萊狄", "狄仁杰", "后羿", "大喬", "小喬", "劉邦", "楊玉環(huán)", "馬可波羅", "狂鐵", "蘇烈", "趙云", "公孫離", "鬼谷子",
            "成吉思汗", "哪吒", "楊戩", "嬴政", "女媧", "周瑜", "弈星", "扁鵲", "甄姬墨子", "高漸離", "亞瑟", "姜子牙", "宮本武藏",
            "牛魔", "莊周", "蔡文姬", "黃忠", "魯班七號", "鎧", "妲己", "白起", "安其拉", "不知火舞", "羋月", "項(xiàng)羽", "劉禪", "橘右京",
            "蘭陵王", "典韋", "元歌", "明世隱", "雅典娜", "娜可露露", "東皇太一", "花木蘭", "孫尚香", "孫臏", "虞姬", "孫悟空", "老夫子"
    };

    @SuppressWarnings("unchecked")
    private <T extends View> T $(int id) {
        View view = findViewById(id);
        return (T) view;
    }

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

    private void initView() {
        mListView = $(R.id.list_contact);
        mIndexView = $(R.id.view_contact);
        mShowTextView = $(R.id.tv_show_letter);

        initData();
        initListener();
    }

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

        for (String name : NAME) {
            mContacts.add(new Contact(name));
        }

        Collections.sort(mContacts, (o1, o2) -> o1.getPinyin().compareTo(o2.getPinyin()));
        mAdapter = new ContactAdapter(mContacts);
        mListView.setAdapter(mAdapter);
    }

    @SuppressWarnings("UnusedAssignment")
    private void initListener() {
        mIndexView.setOnShowLetter(letter -> {

            mShowTextView.setText(letter);
            mLetter = letter;

            if (mCountDownTimer == null) {
                mCountDownTimer = new CountDownTimer(1500, 500) {
                    @Override
                    public void onTick(long millisUntilFinished) {
                        mShowTextView.setVisibility(View.VISIBLE);
                    }

                    @Override
                    public void onFinish() {
                        mShowTextView.setVisibility(View.INVISIBLE);
                        if (mCountDownTimer != null) {
                            mCountDownTimer.cancel();
                            mCountDownTimer = null;
                        }
                    }
                }.start();
            } else {
                mCountDownTimer.start();
            }

            for (int i = 0; i < mContacts.size(); i++) {
                final String letterName = mContacts.get(i).getPinyin().substring(0, 1);
                if (letterName.equals(mLetter)) {
                    mListView.setSelection(i);
                    return;
                }
            }
        });

        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {}

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                String letter = mContacts.get(firstVisibleItem).getPinyin().substring(0, 1);
                boolean isLast = false;
                if (firstVisibleItem + visibleItemCount == totalItemCount) {
                    isLast = true;
                }
                EventBus.getDefault().post(new ScrollEvent(letter, isLast));
            }
        });
    }
}

我們的Activity沒什么好說的轰豆,就是添加數(shù)據(jù)和一些監(jiān)聽事件欠痴。主要注意列表的滑動監(jiān)聽,我們要判斷是否已經(jīng)滑動到了最底部秒咨,如果在列表最底部喇辽,我們不再處理滑動監(jiān)聽事件。其實(shí)雨席,我之前用的是自己隨機(jī)組的一些名字菩咨,發(fā)現(xiàn)有點(diǎn)影響美觀,所以就換了陡厘。

三抽米、列表適配器的邏輯代碼

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class ContactAdapter extends BaseAdapter {

    private ArrayList<Contact> mContacts;

    ContactAdapter(ArrayList<Contact> contacts) {
        this.mContacts = contacts;
    }

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

    @Override
    public Object getItem(int position) {
        return mContacts.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) {
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_contact, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        final String name = mContacts.get(position).getName();
        final String pinyin = mContacts.get(position).getPinyin().substring(0, 1);
        if (position == 0) {
            holder.tvPinYin.setVisibility(View.VISIBLE);
        } else {
            final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
            if (pinyin.equals(prePinyin)) {
                holder.tvPinYin.setVisibility(View.GONE);
            } else {
                holder.tvPinYin.setVisibility(View.VISIBLE);
            }
        }

        holder.tvName.setText(name);
        holder.tvPinYin.setText(pinyin);
        return convertView;
    }

    private static class ViewHolder {
        private TextView tvName;
        private TextView tvPinYin;

        ViewHolder(View itemView) {
            tvName = itemView.findViewById(R.id.tv_name);
            tvPinYin = itemView.findViewById(R.id.tv_pinyin);
        }
    }
}

我們看關(guān)鍵代碼:

        if (position == 0) {
            holder.tvPinYin.setVisibility(View.VISIBLE);
        } else {
            final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
            if (pinyin.equals(prePinyin)) {
                holder.tvPinYin.setVisibility(View.GONE);
            } else {
                holder.tvPinYin.setVisibility(View.VISIBLE);
            }
        }

這段是實(shí)現(xiàn)列表頭部的字母分類的,我們知道第一個字母永遠(yuǎn)不會重復(fù)糙置,而從第二個字母開始云茸,我們判斷是否與前一個字母一樣來決定字母的顯示與否。

四谤饭、漢字轉(zhuǎn)拼音的一個工具類

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

final class PinYinUtils {
    public static String getPinYin(String hanzi) {
        StringBuilder pinyin = new StringBuilder();

        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

        char[] arr = hanzi.toCharArray();
        for (char anArr : arr) {
            if (Character.isWhitespace(anArr)) continue;

            if (anArr > 127) {
                try {
                    String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(anArr, format);

                    if (pinyinArr != null) {
                        pinyin.append(pinyinArr[0]);
                    } else {
                        pinyin.append(anArr);
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                    pinyin.append(anArr);
                }
            } else {
                pinyin.append(anArr);
            }
        }
        return pinyin.toString();
    }
}

這個工具欄比較固定 标捺,那么所有的關(guān)鍵是的代碼基本就這些了。這是我個人寫的一個聯(lián)系人的Demo揉抵,個人認(rèn)為界面還不錯亡容,哈哈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冤今,一起剝皮案震驚了整個濱河市闺兢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌戏罢,老刑警劉巖屋谭,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脚囊,死亡現(xiàn)場離奇詭異,居然都是意外死亡桐磁,警方通過查閱死者的電腦和手機(jī)悔耘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來所意,“玉大人,你說我怎么就攤上這事催首》鲇唬” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵郎任,是天一觀的道長秧耗。 經(jīng)常有香客問我,道長舶治,這世上最難降的妖魔是什么分井? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮霉猛,結(jié)果婚禮上尺锚,老公的妹妹穿的比我還像新娘。我一直安慰自己惜浅,他們只是感情好瘫辩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坛悉,像睡著了一般伐厌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裸影,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天挣轨,我揣著相機(jī)與錄音,去河邊找鬼轩猩。 笑死卷扮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的均践。 我是一名探鬼主播画饥,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浊猾!你這毒婦竟也來了抖甘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葫慎,失蹤者是張志新(化名)和其女友劉穎衔彻,沒想到半個月后薇宠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡艰额,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年澄港,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柄沮。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡回梧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祖搓,到底是詐尸還是另有隱情狱意,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布拯欧,位于F島的核電站详囤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镐作。R本人自食惡果不足惜藏姐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望该贾。 院中可真熱鬧羔杨,春花似錦、人聲如沸杨蛋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽六荒。三九已至护姆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掏击,已是汗流浹背卵皂。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砚亭,地道東北人灯变。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像捅膘,于是被迫代替她去往敵國和親添祸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程寻仗,因...
    小菜c閱讀 6,419評論 0 17
  • 前言 :每個人都有屬于自己的一片森林刃泌,也許我們從來不曾去過,但它一直在那里,總會在那里耙替。迷失的人迷失了亚侠,相逢的人一...
    阿瑞921閱讀 725評論 0 0
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,101評論 1 32
  • 一、簡歷準(zhǔn)備 1俗扇、個人技能 (1)自定義控件硝烂、UI設(shè)計(jì)、常用動畫特效 自定義控件 ①為什么要自定義控件铜幽? Andr...
    lucas777閱讀 5,206評論 2 54
  • 所有的色彩都來自太陽 它沒有任何特別的色彩 沒有鮮花爭奇斗艷 沒有風(fēng)在翱翔 什么也看不見 什么也感覺不到 但它包含...
    二零一久閱讀 437評論 1 1