ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 1. 联系人快速索引 ![](https://img.kancloud.cn/9f/a8/9fa82bd1ccf97f00c76e6aea75103faa_297x476.png) * 拖拽右边索引,ListView联动; * 显示中间标识,表示当前选择为多少; # 2. 实现 也就是: * ListView的setSelection方法来设置定位; * 自定义View; * 处理onTouchEvent事件; 布局文件: ~~~ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello World!" /> <com.weizu.contactindex.Index android:id="@+id/myIndex" android:layout_alignParentEnd="true" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="@color/teal_700" /> <TextView android:id="@+id/tag" android:gravity="center" android:layout_centerInParent="true" android:layout_width="80dp" android:layout_height="80dp" android:textSize="30sp" android:textColor="@color/black" android:background="#55000000" android:padding="20dp" android:visibility="gone" /> </RelativeLayout> ~~~ ListView对应的item文件: ~~~ <?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/top_char" android:gravity="center_vertical|left" android:layout_width="match_parent" android:layout_height="50dp" android:textSize="30sp" android:text="A" android:paddingLeft="5dp" android:background="#88000000" /> <TextView android:id="@+id/name" android:gravity="center_vertical|left" android:text="李思" android:layout_width="match_parent" android:layout_height="50dp" android:paddingLeft="8dp" /> </LinearLayout> ~~~ 即: ![](https://img.kancloud.cn/e0/bb/e0bbad2befe5c9cadb3d1997448c31d7_271x76.png) 然后自定义右边竖直View,: ~~~ class Index: View{ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) private var paint: Paint = Paint() var defaultTextSize = dp2px(20) // 默认字体大小 var defaultWidth = dp2px(40).toInt() // 默认宽度 var itemHeight = defaultTextSize // 每个词的高度 var itemWidth = defaultTextSize // 每个词的宽度 val defaultPaddingBottom = 10 // 默认底部距离 var touchIndex = -1 // 手指触摸的下标 var mListener: OnIndexTouchListener? = null init { paint.isAntiAlias = true paint.color = Color.WHITE paint.textSize = defaultTextSize paint.setTypeface(Typeface.DEFAULT_BOLD) paint.setTextAlign(Paint.Align.CENTER) } val words = listOf<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") override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) var width = defaultWidth if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY){ width = MeasureSpec.getSize(widthMeasureSpec) } val height = MeasureSpec.getSize(heightMeasureSpec) setMeasuredDimension(width, height) // 计算每一项的高度 itemHeight = ((height * 1.0f - defaultPaddingBottom) / words.size) Log.e("TAG", "itemHeight: ${itemHeight}", ) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) for( i in 0 until words.size){ val word = words[i] if(touchIndex == i){ paint.color = resources.getColor(R.color.purple_700) } else{ paint.color = Color.WHITE } // 绘制一个矩形 var rect = Rect(0, (i * itemHeight).toInt(), itemWidth.toInt(), ((i + 1) * itemHeight).toInt()) val x = (itemWidth + paint.measureText(word)) / 2 val y = rect.centerY() + itemHeight / 2; canvas?.drawText(word, x, y, paint) } } override fun onTouchEvent(event: MotionEvent?): Boolean { super.onTouchEvent(event) when(event?.action){ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { var currentIndex = (event.y / itemHeight).toInt() if(currentIndex != touchIndex){ touchIndex = currentIndex invalidate() // 设置监听 mListener?.onIndexTouch(touchIndex, words[touchIndex]) } } MotionEvent.ACTION_UP -> { touchIndex = -1 invalidate() } } return true } fun dp2px(dp: Int): Float{ return resources.displayMetrics.density * dp } fun setOnIndexTouchListener(l: OnIndexTouchListener){ mListener = l } interface OnIndexTouchListener{ fun onIndexTouch(index: Int, word: String) } } ~~~ 在其中完成测量和绘制,在测量中规定了默认的宽度,高度默认为设置的填充父控件,这里也就是全屏。然后根据这个高度来平均分配26个字母,也就是itemHeight。然后在onDraw方法中绘制一个小矩形,大小也就是默认宽度和itemHeight,即均匀分配的26个矩形填充整个View。然后计算字体应该放置的位置,使用: ~~~ val x = (itemWidth + paint.measureText(word)) / 2 val y = rect.centerY() + itemHeight / 2; ~~~ 进行简单计算,当然这里字体从效果上来看并没有居中显示。后续再继续深入学习。 然后就是对触摸事件的处理,也就是复写onTouchEvent方法,使用当前手指在屏幕上的坐标y值除以itemHeight得到逻辑上的每个字母的下标。然后判断是否和当前下标相等,如果不等,就表示手指移动了,故而需要更新该下标值以及对应的刷新屏幕,也就是调用invalidate方法。 然后就是在MainActivity中TextView的显示和隐藏,以及对ListView的setSelection()来设置移动,由于比较简单,这里就直接贴出代码: ~~~ class MainActivity : AppCompatActivity() { val myIndex by lazy { findViewById<Index>(R.id.myIndex) } val listView by lazy { findViewById<ListView>(R.id.listView) } val tag by lazy { findViewById<TextView>(R.id.tag) } var datas = mutableListOf<String>("阿雷", "李四", "李思", "张晓飞", "胡继群", "刘畅", "尹革新", "温松", "李凤秋", "娄全超", "王英杰", "孙仁政", "姜宇航", "张洪瑞", "侯亚帅", "徐雨健", "阿三") lateinit var handler: Handler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initDatas() listView.adapter = MyAdapter() myIndex.setOnIndexTouchListener(object : Index.OnIndexTouchListener { override fun onIndexTouch(index: Int, word: String) { // 定位到listView的下标位置 listView.setSelection(getIndex(word)) // 显示一下标签Tag showTag(word) } }) } fun showTag(word: String){ tag.text = word tag.visibility = View.VISIBLE handler.removeCallbacksAndMessages(null) handler.postDelayed({ tag.visibility = View.GONE }, 2000) } fun getIndex(word: String): Int{ // 该下标为距离word最近的位置 var itemIndex = 0 for (i in 0 until datas.size){ val current = PinYinUtils.getPinYin(datas.get(i)).subSequence(0, 1) if(word > current as String){ itemIndex = i } if(current == word){ itemIndex = i break } } return itemIndex } fun initDatas(){ Collections.sort(datas, object : java.util.Comparator<String> { override fun compare(o1: String?, o2: String?): Int { return PinYinUtils.getPinYin(o1).compareTo(PinYinUtils.getPinYin(o2)) } }) handler = Handler(Looper.getMainLooper()) } inner class MyAdapter: BaseAdapter() { override fun getCount() = datas.size override fun getItem(position: Int) = position override fun getItemId(position: Int) = position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { var myViewHolder: MyViewHolder? = null var view: View? = convertView if(convertView == null){ myViewHolder = MyViewHolder() view = View.inflate(this@MainActivity, R.layout.list_item, null) myViewHolder.top_char = view.findViewById<TextView>(R.id.top_char) myViewHolder.name = view.findViewById<TextView>(R.id.name) } else{ myViewHolder = convertView.getTag() as MyViewHolder? } myViewHolder?.apply { val current = PinYinUtils.getPinYin(datas.get(position)).subSequence(0, 1) myViewHolder.top_char?.text = current myViewHolder.name?.text = datas.get(position) view?.setTag(myViewHolder) // 设置出现过的不再显示top_char if(position != 0 && PinYinUtils.getPinYin(datas.get(position - 1)).subSequence(0, 1).equals(current)){ myViewHolder.top_char?.visibility = View.GONE } else{ myViewHolder.top_char?.visibility = View.VISIBLE } } return view!! } } inner class MyViewHolder{ var top_char: TextView? = null var name: TextView? = null } } ~~~ 当然,这里还需要处理的一个最大的问题就在于字体在自定义View中并没有居中。打算拟定新开一个目录在专门学习对onDraw的绘制的学习。