自定义View实现通讯录和索引联动,如丝般顺滑

    xiaoxiao2022-07-13  147

    1,右边索引导航我自定义一个View:WordsNavigator.java

    package com.txhl.testapp.cus; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.txhl.testapp.R; import com.txhl.testapp.act.MailListActivity; import com.txhl.testapp.listener.OnWordsChangeListener; import com.txhl.testapp.utils.ScreenUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Created by chen.yingjie on 2019/4/29 * description */ public class WordsNavigator extends View { private int mRealWidth; private int mRealHeight; private int mWidth;// 画布宽 private int mHeight;// 画布高 private int mEachHeight;// 每个字母平均分配到的占位高,宽和画布一样。 private int mTouchIndex = 0; private Paint wordsPaint;// 字母画笔 private Paint selectedPaint;// 背景圈画笔 private Rect mRect; private OnWordsChangeListener onShowLetterListener; private int colorTrans; private int colorNormal; private int colorChecked; private int colorCheckedBg; public void setOnShowLetterListener(OnWordsChangeListener onShowLetterListener) { this.onShowLetterListener = onShowLetterListener; } private List<String> letterLists; public WordsNavigator(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { letterLists = new ArrayList<>(); if (getContext() instanceof MailListActivity) { MailListActivity activity = (MailListActivity) getContext(); activity.setOnRecyclerViewScrollListener(new MailListActivity.OnRecyclerViewScrollListener() { @Override public void onScroll(String firstWords) { int wordsIndex = letterLists.indexOf(firstWords); mTouchIndex = wordsIndex; invalidate(); } }); } colorTrans = getContext().getResources().getColor(R.color.transparent); colorNormal = getContext().getResources().getColor(R.color.black); colorChecked = getContext().getResources().getColor(R.color.white); colorCheckedBg = getContext().getResources().getColor(R.color.red); mRect = new Rect(); wordsPaint = new Paint(); wordsPaint.setStrokeWidth(0); wordsPaint.setAntiAlias(true); wordsPaint.setTextSize(ScreenUtils.dip2px(getContext(), 10)); selectedPaint = new Paint(); selectedPaint.setStrokeWidth(0); wordsPaint.setAntiAlias(true); selectedPaint.setStyle(Paint.Style.FILL); } public void setLetters(List<String> letterLists) { this.letterLists = letterLists; } @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(colorTrans); mWidth = canvas.getWidth(); mHeight = canvas.getHeight(); mEachHeight = mHeight / letterLists.size(); for (int i = 0; i < letterLists.size(); i++) { final String _latter = letterLists.get(i); wordsPaint.getTextBounds(_latter, 0, 1, mRect); final int letterWidth = mRect.width(); final int letterHeight = mRect.height(); if (mTouchIndex == i) { wordsPaint.setColor(colorChecked); selectedPaint.setColor(colorCheckedBg); } else { wordsPaint.setColor(colorNormal); selectedPaint.setColor(colorTrans); } // 画选中背景圈,x:画笔宽的一半,即画布水平终点;y:每个字母高乘个数减去字母高的一半;radius:半径为画布宽的三分之一。 canvas.drawCircle(mWidth / 2, mEachHeight * (i + 1) - letterHeight / 2, mWidth / 3, selectedPaint); // 画字母,x,y分别为字母的左上角起点,背景圈的圆心取决于字母的起点。 canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight, wordsPaint); } } @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; } private void refreshLetterIndex(int y) { //y坐标 / 每个字母高度 = 当前字母下标 int index = y / mEachHeight; if (index > letterLists.size() || index < 0) return; if (index != mTouchIndex) { mTouchIndex = index; //回调选中的字母 if (onShowLetterListener != null) { onShowLetterListener.wordsChange(letterLists.get(mTouchIndex)); } invalidate(); } } }

    这里定义了一个接口为了在手指滑动索引的时候通知activity滚动item。

    public interface OnWordsChangeListener { void wordsChange(String words); }

    2,用RecyclerView来显示列表数据,给它设置滚动监听,当滚动列表的时候要让索引跟着重绘,这里通过LinearLayoutManager找到第一个可见item的position,查出对于的首字母,通过OnRecyclerViewScrollListener接口给到WordsNavigator,让其重绘。 3,让RecyclerView滚动到指定item,用的是它的LayoutManager的startSmoothScroll方法,需要一个Scroller:

    public class TopSmoothScroller extends LinearSmoothScroller { public TopSmoothScroller(Context context) { super(context); } @Override protected int getHorizontalSnapPreference() { return SNAP_TO_START;//具体见源码注释 } @Override protected int getVerticalSnapPreference() { return SNAP_TO_START;//具体见源码注释 } }

    4,activity

    package com.txhl.testapp.act; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.txhl.testapp.R; import com.txhl.testapp.cus.TopSmoothScroller; import com.txhl.testapp.cus.WordsNavigator; import com.txhl.testapp.cus.widget.SectionListView; import com.txhl.testapp.listener.OnWordsChangeListener; import com.txhl.testapp.utils.PinYinUtils; import com.txhl.testapp.utils.SortUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; public class MailListActivity extends AppCompatActivity { private final int CHANGE_WORDS = 1; @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.tvHintWord) TextView tvHintWord; @BindView(R.id.wordsNavigator) WordsNavigator wordsNavigator; private String[] names = new String[]{ "露娜", "李白", "韩信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "关羽", "张飞", "刘备", "貂蝉", "吕布", "王昭君", "武则天", "百里守约", "百里玄策", "司马懿", "孙策", "干将莫邪", "裴擒虎", "张良", "诸葛亮", "达摩", "蒙奇", "曹操", "钟馗", "钟无艳", "程咬金", "米莱狄", "狄仁杰", "后羿", "大乔", "小乔", "刘邦", "杨玉环", "马可波罗", "狂铁", "苏烈", "赵云", "公孙离", "鬼谷子", "成吉思汗", "哪吒", "杨戬", "嬴政", "女娲", "周瑜", "弈星", "扁鹊", "甄姬墨子", "高渐离", "亚瑟", "姜子牙", "宫本武藏", "牛魔", "庄周", "蔡文姬", "黄忠", "鲁班七号", "铠", "妲己", "白起", "安其拉", "不知火舞", "芈月", "项羽", "刘禅", "橘右京", "兰陵王", "典韦", "元歌", "明世隐", "雅典娜", "娜可露露", "东皇太一", "花木兰", "孙尚香", "孙膑", "虞姬", "孙悟空", "老夫子" }; private List<String> letterLists = new ArrayList<>(Arrays.asList("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 String[] newNames; private LinearLayoutManager linearLayoutManager; private TopSmoothScroller smoothScroller; private MailAdapter mailAdapter; private OnRecyclerViewScrollListener onRecyclerViewScrollListener; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case CHANGE_WORDS: tvHintWord.setVisibility(View.GONE); break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mail_list); ButterKnife.bind(this); wordsNavigator.setLetters(letterLists); wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() { @Override public void wordsChange(String words) { mHandler.removeMessages(CHANGE_WORDS); tvHintWord.setVisibility(View.VISIBLE); tvHintWord.setText(words); mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500); scrollToWords(words); } }); newNames = SortUtils.sortChar(names); linearLayoutManager = new LinearLayoutManager(this); smoothScroller = new TopSmoothScroller(this); mailAdapter = new MailAdapter(); recyclerView.setAdapter(mailAdapter); recyclerView.setLayoutManager(linearLayoutManager); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition(); String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]); onRecyclerViewScrollListener.onScroll(firstWords); } } }); } private void scrollToWords(String words) { for (int i = 0; i < newNames.length; i++) { String firstWrods = PinYinUtils.getFirstWords(newNames[i]); if (words.equals(firstWrods)) { // 找到列表中汉字首字母和按下的字母相同的汉字 smoothScroller.setTargetPosition(i); linearLayoutManager.startSmoothScroll(smoothScroller); return; } } } class MailAdapter extends RecyclerView.Adapter<MailAdapter.MailHolder> { @NonNull @Override public MailHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { View view = View.inflate(MailListActivity.this, R.layout.item_mail_list, null); return new MailHolder(view); } @Override public void onBindViewHolder(MailHolder viewHolder, int position) { String lastFirstChar = "";// 上一个首汉字 String lastFirstWords = "";// 上一个首汉字的首字母 String firstChar = "";// 当前首汉字 String firstWords = "";// 当前首汉字的首字母 firstChar = newNames[position].substring(0, 1); firstWords = PinYinUtils.getFirstWords(firstChar);// 多音字,只取第一个字母。 if (position > 0) { lastFirstChar = newNames[position - 1].substring(0, 1); lastFirstWords = PinYinUtils.getFirstWords(lastFirstChar); } if (firstWords.equals(lastFirstWords)) { viewHolder.tvWords.setVisibility(View.GONE); } else { viewHolder.tvWords.setVisibility(View.VISIBLE); viewHolder.tvWords.setText(firstWords); } viewHolder.tvName.setText(newNames[position]); } @Override public int getItemCount() { return newNames == null ? 0 : newNames.length; } class MailHolder extends RecyclerView.ViewHolder { @BindView(R.id.tvWords) TextView tvWords; @BindView(R.id.tvName) TextView tvName; @BindView(R.id.tvMsg) TextView tvMsg; @BindView(R.id.ivPhoto) ImageView ivPhoto; public MailHolder(@NonNull View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } } public interface OnRecyclerViewScrollListener { void onScroll(String firstWords); } public void setOnRecyclerViewScrollListener(OnRecyclerViewScrollListener onRecyclerViewScrollListener) { this.onRecyclerViewScrollListener = onRecyclerViewScrollListener; } }

    用pinyin4j.jar来取首字母,可能有多音字,pinyinName是一个类似(比如重 pinyinName=c,z),这里只取第一个:

    public static String getFirstWords(String chines) { StringBuffer pinyinName = new StringBuffer(); char[] nameChar = chines.toCharArray(); HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); for (int i = 0; i < nameChar.length; i++) { if (nameChar[i] > 128) { try { String[] strs = PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat); if (strs != null) { for (int j=0; j<strs.length; j++) { pinyinName.append(strs[j].substring(0,1)); if (j != strs.length - 1) { pinyinName.append(","); } } } } catch (BadHanyuPinyinOutputFormatCombination e) { e.printStackTrace(); } } else { pinyinName.append(nameChar[i]); } } return pinyinName.toString().substring(0, 1); }

    排序方式有很多:

    * 按首字母排序 * @param strs * @return */ public static String[] sortChar(String[] strs) { Comparator<Object> com = Collator.getInstance(Locale.CHINA); List<String> list = Arrays.asList(strs); Collections.sort(list, com); return list.toArray(new String[0]); }

    5,布局 activity_mail_list

    <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".act.MailListActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.txhl.testapp.cus.WordsNavigator android:id="@+id/wordsNavigator" android:layout_marginRight="12dp" android:layout_marginTop="100dp" android:layout_marginBottom="40dp" android:background="@color/transparent" android:layout_gravity="right" android:layout_width="30dp" android:layout_height="match_parent" /> <TextView android:id="@+id/tvHintWord" android:text="a" android:background="@color/gray" android:visibility="gone" android:layout_centerInParent="true" android:gravity="center" android:layout_gravity="center" android:textSize="30dp" android:textColor="@color/red" android:layout_width="60dp" android:layout_height="80dp" /> </FrameLayout>

    item_mail_list

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tvWords" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" android:textSize="16sp" android:background="@color/bg_focus" android:text="A" android:textColor="@color/blue" /> <LinearLayout android:padding="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/ivPhoto" android:layout_gravity="center_vertical" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher_round" /> <LinearLayout android:layout_gravity="center_vertical" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tvName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dp" android:text="" android:textColor="@color/black" android:textSize="18sp"/> <TextView android:id="@+id/tvMsg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dp" android:maxLines="2" android:text="我是一个小学生,你不要欺负我,否则我会哭的!" android:textColor="@color/gray" android:textSize="13sp" /> </LinearLayout> </LinearLayout> </LinearLayout>

    补充:测试发现字母导航器在滑动的时候不跟手,没有那种如丝般顺滑的手感,经过分析原因是,在手指滑动导航器的时候,会触发recyclerView的onScroll监听,这个监听里又不断的告诉导航器你该重绘了,但是如果我们滑动导航器,这时候是不应该再让RecyclerView告诉导航器重绘的,所以导致导航器不断的重绘导致滑动像大鼻涕一样粘稠。故我们在activity里定义一个boolean值来控制是否需要重绘导航器,而且这个值默认为true,否则第一次进页面不会绘制导航器,导致其看不见。

    MailListActivity.java

    private boolean needInvalidate = true;// 是否需要重绘字母导航 是手指滑动recyclerView的时候需要重绘导航器 recyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { needInvalidate = true; return false; } }); 在导航器滑动导致recyclerView滑动的时候不需要重绘导航器 wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() { @Override public void wordsChange(String words) { needInvalidate = false; mHandler.removeMessages(CHANGE_WORDS); tvHintWord.setVisibility(View.VISIBLE); tvHintWord.setText(words); mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500); scrollToWords(words); } }); 控制导航器是否重绘 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (!needInvalidate) return; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition(); String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]); onRecyclerViewScrollListener.onScroll(firstWords); } } });
    最新回复(0)