一. 前言

常用的应用场景:城市搜索的侧边栏,通讯录的侧边栏

Github地址:XLLetterView

二. 自定义View

1. 自定义用到的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//绘制文本的画笔
private Paint paint;
//字母正常状态画笔的颜色
private int text_color = Color.WHITE;
//字母选中状态画笔颜色
private int select_text_color = Color.MAGENTA;
//画笔的粗细
private int text_size = 60;

//字母与左右的间距
private int space_hor = 10;

//字母之间或者字母和上下的间距
private int space_ver = 10;

//字母的最大高度
private int max_letter_height = 0;

//监听者
private LetterChangeListener listener;

//上一次选择字母
private String lastLetter;

//当前选择的字母
private String currentLetter;

2. 在values文件夹下自定义属性

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="XLLetterView">
<attr name="text_color" format="color"/>
<attr name="select_text_color" format="color"/>
<attr name="text_size" format="integer"/>
<attr name="space_hor" format="integer"/>
<attr name="space_ver" format="integer"/>
</declare-styleable>
</resources>

3. 创建构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 构造方法:Xml代码初始化
* @param context
* @param attrs
*/
public XLLetterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);

if (attrs != null){
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XLLetterView);

text_color = typedArray.getColor(R.styleable.XLLetterView_text_color,text_color);
select_text_color = typedArray.getColor(R.styleable.XLLetterView_select_text_color,select_text_color);
text_size = typedArray.getInteger(R.styleable.XLLetterView_text_size,text_size);
space_hor = typedArray.getInteger(R.styleable.XLLetterView_space_hor,space_hor);
space_ver = typedArray.getInteger(R.styleable.XLLetterView_space_ver,space_ver);

text_size = PxUtil.spToPx(text_size,context);
space_hor = PxUtil.spToPx(space_hor,context);
space_ver = PxUtil.spToPx(space_ver,context);

typedArray.recycle();
}

//初始化
init();
}

/**
* 初始化方法
*/
private void init(){
//初始化画笔
paint = new Paint();
paint.setColor(text_color);
paint.setTextSize(text_size);
}

4. onMeasure和onDraw方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//获得测量模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

//存储wrap_content模式下的宽高
int wrap_width = 0;
int wrap_height = 0;

for (int i = 0; i < Constant.letter.length; i++) {
//获取字母绘制出来的宽度
Rect rect = new Rect();
paint.getTextBounds(Constant.letter[i],0,1,rect);

//测试
Log.d(Constant.TAG,Constant.letter[i]+":"+(rect.bottom - rect.top));

//最大宽度
wrap_width = Math.max(wrap_width,rect.right - rect.left);
max_letter_height = Math.max(max_letter_height,rect.bottom - rect.top);
}

//累加间距
wrap_width += space_hor * 2;
wrap_height += max_letter_height * Constant.letter.length + space_ver * (Constant.letter.length + 1);

//根据测量模式去保存相应的测量宽度
//即如果是MeasureSpec.EXACTLY直接使用父ViewGroup传入的宽和高
//否则设置为自己计算的宽和高,即为warp_content时
setMeasuredDimension(
(widthMode == MeasureSpec.EXACTLY) ? widthSize : wrap_width,
(heightMode == MeasureSpec.EXACTLY) ? heightSize : wrap_height
);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//获取宽高
int width = getWidth();
int height = getHeight();

//计算绘制的baseline往下的偏移距离 baseline的坐标是0
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
float base_space = fontMetrics.bottom;
Log.d(Constant.TAG,"top:"+fontMetrics.top);
Log.d(Constant.TAG,"ascent:"+fontMetrics.ascent);
Log.d(Constant.TAG,"descent:"+fontMetrics.descent);
Log.d(Constant.TAG,"bottom:"+fontMetrics.bottom);
Log.d(Constant.TAG,"leading:"+fontMetrics.leading);

//绘制每一个字母
for (int i = 0; i < Constant.letter.length; i++) {
//1.获取每一个字母的高度
Rect rect = new Rect();
paint.getTextBounds(Constant.letter[i],0,1,rect);
int letter_width = rect.right - rect.left;
int letter_height = rect.bottom - rect.top;

//2.存储绘制文本的起始坐标
float x;
float y;

//3.获取绘制的x起始点
x = (width - letter_width) >> 1;

//4.获取绘制的y起始点
float distance = ((fontMetrics.bottom - fontMetrics.top) / 2) - fontMetrics.bottom;
float location = ((max_letter_height - letter_height) >> 1) + (letter_height >> 1) + distance;
y = space_ver + location + (space_ver + max_letter_height) * i;

//5.绘制文本
if (TextUtils.equals(currentLetter,Constant.letter[i])){
paint.setColor(select_text_color);

canvas.drawText(
Constant.letter[i],
x,
y,
paint
);

paint.setColor(text_color);
}else {
canvas.drawText(
Constant.letter[i],
x,
y,
paint
);
}
}
}

5. 触摸事件,回调触摸到的字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取触摸点的坐标x,y
float x = event.getX();
float y = event.getY();

//获取选中的字母
float position = (y / (space_ver + max_letter_height));
Log.d(Constant.TAG,"position:"+position);
if (position >= 0 && position < Constant.letter.length) {
currentLetter = Constant.letter[(int) Math.ceil(position)-1];
}else {
currentLetter = "";
}

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (TextUtils.equals(currentLetter,lastLetter)){
return true;
}

break;

case MotionEvent.ACTION_UP:

//记录上一次的字母
lastLetter = currentLetter;
return true;
}

//回调
if (listener != null) {
listener.currentLetter(currentLetter);
}

//刷新
invalidate();

return true;
}

/**
* 回调当前的字母
*/
public interface LetterChangeListener {
void currentLetter(String letter);
}

//设置监听者
public void setLetterChangeListener(LetterChangeListener listener) {
this.listener = listener;
}

三. 具体的使用

首先需要添加依赖,添加依赖之后,在布局文件中这样调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">

<swu.xl.xlletterview.XLLetterView
android:id="@+id/letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:layout_centerInParent="true"
app:text_size="12"
/>

</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {

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

XLLetterView letterView = findViewById(R.id.letter);

letterView.setLetterChangeListener(new XLLetterView.LetterChangeListener() {
@Override
public void currentLetter(String letter) {
Toast.makeText(MainActivity.this, "当前的字母:"+letter, Toast.LENGTH_SHORT).show();
}
});

letterView.setCurrentLetter("S");
}
}