1、先来看下布局的效果
布局的代码如下,其中LetterIndexView为我们将要自定义的控件,使用相对布局置于界面的右侧;
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="com.itemp.letterindexview.MainActivity">
<ListView
android:id="@+id/lvFriends"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/tvCurrentLetter"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/shape_letterindexview_bg_pressed"
android:gravity="center"
android:textSize="50sp"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:textStyle="bold"
android:visibility="visible"
android:text="A" />
<com.itemp.letterindexview.LetterIndexView
android:id="@+id/liv"
android:layout_width="35dp"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:layout_alignParentRight="true"
/>
</RelativeLayout>
2、继承于View并使用绘图法在画布上绘制字母:
public class LetterIndexView extends View
3、实现构造方法,在其中初始化画笔,并为控件设置背景图(shape资源制作的圆角矩形)
public LetterIndexView(Context context) {
this(context, null);
}
public LetterIndexView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LetterIndexView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//setBackgroundResourece();
setBackgroundResource(R.drawable.shape_letterindexview_bg);
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
}
4、shape资源的定义代码:res/drawable/shape_letterindexview_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#fff" />
<stroke
android:width="1dp"
android:color="#ddd" />
<corners android:radius="10dp" />
</shape>
这是一个白色实心的圆角矩形,按下后将其变为黄色实心的圆角矩形,文件为res/drawable/shape_letterindexview_bg_pressed.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ff0" />
<stroke
android:width="1dp"
android:color="#ddd" />
<corners android:radius="10dp" />
</shape>
5、定义字符串数组作为索引的文本:
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", "#",
};
6、覆写onDraw()方法,将字母纵向排列均匀地绘制在画布上:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (width == 0) {
width = getWidth();
height = getHeight();
}
//把字母画在控件上,【选中字母】用红色画笔,否则黑色
for (int i = 0; i < letters.length; i++) {
//计算startX,控件宽度的一半减去字母宽度的一半
String letter = letters[i];
float letterSize = paint.measureText(letter);
float startX = (width - letterSize) / 2;
//startY,上方所有单元格的高度之和+(单元格高度的一半+字母高度的一半)
float unitHeight = (height - 40) / 27f;
float startY = 20 + i * unitHeight + (unitHeight + letterSize) / 2;
//高亮字母为红色,否则为黑色
if(i == currentPosition){
paint.setColor(Color.RED);
}else {
paint.setColor(Color.BLACK);
}
paint.setTextSize(35);
paint.setStyle(Paint.Style.FILL_AND_STROKE);//使用加粗效果
canvas.drawText(letter, startX, startY, paint);//绘制字母
}
}
7、接下来覆写onTouchEvent()定义手指在控件上的滑动响应,逻辑为:
·手指按下,整个控件的背景色变为黄色,并根据手指按下的y的位置,确认哪个字母为选中字母,并重绘以将该字母高亮显示,并通知外界响应按下事件(比如显示小窗口见本文末尾GIF)
·手指滑动,动态改变选中字母,并重绘以将该字母高亮显示
·手指抬起,控件背景恢复为默认的白色,并通知外界响应(比如隐藏小窗口见文章末尾GIF)
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//改变背景效果
setBackgroundResource(R.drawable.shape_letterindexview_bg_pressed);
//根据手指位置设置高亮字母并重绘
invalidateCurrentPosition(y);
//通知外界手指按下
if (callback != null) {
callback.onFingerDown(true);
}
break;
case MotionEvent.ACTION_MOVE:
//根据手指位置设置高亮字母并重绘
invalidateCurrentPosition(y);
//通知外界字母变化
if (callback != null) {
callback.onLetterChanged(letters[currentPosition]);
}
break;
case MotionEvent.ACTION_UP:
//恢复背景效果
setBackgroundResource(R.drawable.shape_letterindexview_bg);
//通知外界手指抬起
if (callback != null) {
callback.onFingerDown(false);
}
break;
}
return true;
}
/**
* 根据手指位置动态设置高亮字母并重绘
* @param y
*/
private void invalidateCurrentPosition(float y) {
currentPosition = (int) ((y / height) * letters.length);
if(currentPosition > 26){
currentPosition = 26;
}
invalidate();
}
11、以接口的方式通知外界:手指按下或抬起,高亮字母发生改变:
LetterIndexCallback callback;
public void setCallback(LetterIndexCallback callback) {
this.callback = callback;
}
public interface LetterIndexCallback {
void onFingerDown(boolean down);
void onLetterChanged(String letter);
}
12、最后当外界ListView主动滚动时,字母索引的选中字母也随之变化,我们为外界提供公共方法,用于更新选中字母的位置:
/**
* 供外界ListView滚动时通知到当前控件
* @param firstLetter
*/
public void setCurrentLetter(String firstLetter) {
for (int i = 0; i < letters.length; i++) {
if(letters[i].equals(firstLetter)){
setCurrentPosition(i);
return;
}
}
}
13、Activity实现【索引控件】的回调接口,并将自身设置给【索引控件】:
public class MainActivity extends AppCompatActivity implements LetterIndexView.LetterIndexCallback
@Override
public void onLetterChanged(String letter) {
tvCurrentLetter.setText(letter);
}
@Override
public void onFingerDown(boolean fingerDown) {
if(fingerDown){
tvCurrentLetter.setVisibility(View.VISIBLE);
}else {
tvCurrentLetter.setVisibility(View.GONE);
}
}
letterIndexView.setCallback(this);
效果如下: