重学 Android 自定义 View 系列(九):侧边字母选择器
前言
本文来实现一个侧边字母选择器,很常见的一个控件,和上篇文章星星评分用到的关键技术点类似,难度不大,本篇再来温故知新一下。
最终效果如下:
1. 效果分析
- 每个字母被均匀分布在整个控件区域中;
- 触摸某个字母时,该字母的颜色会改变,表示该字母被选中;
- 支持字母的触摸反馈,提供字母选中时的提示。
2. 技术实现
AlphabetTouchView 的实现主要包括这几个方面:属性配置、字母绘制、触摸事件、回调接口
2.1 属性配置
在res/values/attrs.xml
可配置字符大小、颜色、选中颜色,alphabet
为侧边字符默认为 "ABCDEFGHIJKLMNOPQRSTUVWXYZ#"
可自由配置。
<declare-styleable name="AlphabetTouchView">
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="selectedTextColor" format="color" />
<attr name="alphabet" format="string" />
</declare-styleable>
通过 TypedArray 解析 XML 中配置的属性值
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.AlphabetTouchView,
0, 0);
try {
mTextSize = a.getDimensionPixelSize(R.styleable.AlphabetTouchView_textSize, 14);
mTextColor = a.getColor(R.styleable.AlphabetTouchView_textColor, Color.BLACK);
mSelectedTextColor = a.getColor(R.styleable.AlphabetTouchView_selectedTextColor, Color.RED);
mAlphabet = a.getString(R.styleable.AlphabetTouchView_alphabet);
if (mAlphabet == null || mAlphabet.isEmpty()) {
mAlphabet = DEFAULT_ALPHABET;
}
} finally {
a.recycle();
}
2.2 字母绘制
根据控件的高度和字母的个数,均匀分布字母,并确保字母在每个格子中垂直居中。
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float fontHeight = fontMetrics.descent - fontMetrics.ascent;
for (int i = 0; i < mAlphabet.length(); i++) {
float x = mWidth / 2 - mTextPaint.measureText(String.valueOf(mAlphabet.charAt(i))) / 2;
float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent;
if (i == mSelectedIndex) {
mTextPaint.setColor(mSelectedTextColor);
} else {
mTextPaint.setColor(mTextColor);
}
canvas.drawText(String.valueOf(mAlphabet.charAt(i)), x, y, mTextPaint);
}
看过前几篇文章的应该会发现,这次确定文字基线的方式和之前都不一样了,之前确定基线的两种方式分别是:
方式一:
float dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseLine = getHeight() / 2 + dy;
方式二:
float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
而float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent;
这种方式也能确定文字在格子中的基线并垂直居中显示,并不是那么好理解,在这里解释下:
i * mLetterHeight
: 当前字母所在的格子的顶部位置(mLetterHeight + fontHeight) / 2
: 计算字母在格子中应该偏移多少才能垂直居中- fontMetrics.descent
: 确保基线(baseline)正确对齐,fontMetrics.descent是从基线到字母底部的距离,所以减去这一部分能使得字母真正的中心对齐到格子的中心。
仔细观察下就理解了,到这里已经有三种方法确定基线的方式了,记住一个就行了。
2.3 触摸事件处理
在 onTouchEvent
方法中,根据触摸位置(event.getY()
)计算触摸的字母索引,并更新选中的字母索引 mSelectedIndex
。
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float touchY = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
int index = (int) (touchY / (mHeight / mAlphabet.length()));
if (index >= 0 && index < mAlphabet.length()) {
mSelectedIndex = index;
if (mListener != null) {
mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)), true);
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (mListener != null) {
mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)), false);
}
mSelectedIndex = -1;
invalidate();
break;
}
return true;
}
2.4 回调接口
通过 OnLetterSelectedListener
接口,向外部组件传递选中的字母,外部可以自由处理事件,如放大显示选中字符,滚动列表等。
public interface OnLetterSelectedListener {
void onLetterSelected(String letter, boolean isTouch);
}
3. 完整代码
public class AlphabetTouchView extends View {
private static final String DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
private String mAlphabet;
private Paint mTextPaint;
private Paint mBubblePaint;
private int mTextSize;
private int mTextColor;
private int mSelectedTextColor = Color.RED;
private int mWidth, mHeight;
private float mLetterHeight;
private int mSelectedIndex = -1;
private OnLetterSelectedListener mListener;
public void setOnLetterSelectedListener(OnLetterSelectedListener listener) {
mListener = listener;
}
public AlphabetTouchView(Context context) {
this(context, null);
}
public AlphabetTouchView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AlphabetTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.AlphabetTouchView,
0, 0);
try {
mTextSize = a.getDimensionPixelSize(R.styleable.AlphabetTouchView_textSize, 14);
mTextColor = a.getColor(R.styleable.AlphabetTouchView_textColor, Color.BLACK);
mSelectedTextColor = a.getColor(R.styleable.AlphabetTouchView_selectedTextColor, Color.RED);
mAlphabet = a.getString(R.styleable.AlphabetTouchView_alphabet);
if (mAlphabet == null || mAlphabet.isEmpty()) {
mAlphabet = DEFAULT_ALPHABET;
}
} finally {
a.recycle();
}
init();
}
private float sp2px(int sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
private void init() {
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(sp2px(mTextSize));
mTextPaint.setAntiAlias(true);
mBubblePaint = new Paint();
mBubblePaint.setColor(Color.parseColor("#AA0000"));
mBubblePaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mLetterHeight = (float) mHeight / mAlphabet.length();
}
@Override
protected void onDraw(Canvas canvas) {
// 绘制26个字母
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); // 获取字体的度量信息
float fontHeight = fontMetrics.descent - fontMetrics.ascent; // 字体高度
for (int i = 0; i < mAlphabet.length(); i++) {
// 计算字母的x坐标,使字母居中 ,x = mWidth / 2 - 文字的宽度 / 2
float x = mWidth / 2 - mTextPaint.measureText(String.valueOf(mAlphabet.charAt(i))) / 2;
// 计算字母的y坐标,使用均匀分布的间距
float y = i * mLetterHeight + (mLetterHeight + fontHeight) / 2 - fontMetrics.descent;
if (i == mSelectedIndex) {
// 选中的字母文字颜色设置为选中颜色
mTextPaint.setColor(mSelectedTextColor);
} else {
// 否则,使用普通的文字颜色
mTextPaint.setColor(mTextColor);
}
// 绘制字母
canvas.drawText(String.valueOf(mAlphabet.charAt(i)), x, y, mTextPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float touchY = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 根据触摸的Y坐标来计算字母的索引
// 获取触摸点所在的字母
int index = (int) (touchY / (mHeight / mAlphabet.length()));
if (index >= 0 && index < mAlphabet.length()) {
mSelectedIndex = index;
if (mListener != null) {
mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)),true);
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (mListener != null) {
mListener.onLetterSelected(String.valueOf(mAlphabet.charAt(mSelectedIndex)),false);
}
mSelectedIndex = -1;
invalidate();
break;
}
return true;
}
public interface OnLetterSelectedListener {
void onLetterSelected(String letter,boolean isTouch);
}
}
4. 最后
难度不大,前面文章和接下来几篇文章都是打基础的,只有基础打牢了,遇到高级View才能不发怵,🤭。再会!
源码及更多自定义View已上传Github:DiyView
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai