# 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的绘制的学习。
- 介绍
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字体相关设置
- Material Design
- Toolbar
- 下拉刷新
- 可折叠式标题栏
- 悬浮按钮
- 滑动菜单DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定义View
- 简单封装单选
- RecyclerView
- xml设置点击样式
- adb
- 连接真机
- 小技巧
- 通过字符串ID获取资源
- 自定义View组件
- 使用系统控件重新组合
- 旋转菜单
- 轮播图
- 下拉输入框
- 自定义VIew
- 图片组合的开关按钮
- 自定义ViewPager
- 联系人快速索引案例
- 使用ListView定义侧滑菜单
- 下拉粘黏效果
- 滑动冲突
- 滑动冲突之非同向冲突
- onMeasure
- 绘制字体
- 设置画笔Paint
- 贝赛尔曲线
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer与阴影效果
- Shader
- ImageView的scaleType属性
- 渐变
- LinearGradient
- 图像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 离屏绘制
- Canvas和图层
- Canvas简介
- Canvas中常用操作总结
- Shape
- 圆角属性
- Android常见动画
- Android动画简介
- View动画
- 自定义View动画
- View动画的特殊使用场景
- LayoutAnimation
- Activity的切换转场效果
- 属性动画
- 帧动画
- 属性动画监听
- 插值器和估值器
- 工具
- dp和px的转换
- 获取屏幕宽高
- JNI
- javah命令
- C和Java相互调用
- WebView
- Android Studio快捷键
- Bitmap和Drawable图像
- Bitmap简要介绍
- 图片缩放和裁剪效果
- 创建指定颜色的Bitmap图像
- Gradle本地仓库
- Gradle小技巧
- RxJava+Okhttp+Retrofit构建网络模块
- 服务器相关配置
- node环境配置
- 3D特效