看了很多学习自定义的文章和各位大神的例子,综合起来写了一个验证码控件来练手。
如果你对自定义控件还不够了解那么你可以先看鸿洋的这篇文章来学习基本的知识。
先来一张效果图
- 1.先自定义两个属性用来设置view的字体大小和验证码的个数。
<declare-styleable name="VerificationCodeView">
<attr name="textSize" format="dimension" />
<attr name="textCount" format="integer" />
</declare-styleable>
- 2.在代码中获取属性值
public VerificationCodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VerificationCodeView);
mTextSize = a.getDimensionPixelSize(R.styleable.VerificationCodeView_textSize, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
mTextCount = a.getInt(R.styleable.VerificationCodeView_textCount, 4);
a.recycle();
}
在这里着重说明一点:用完TypedArray一定要记得 调用recycle()
方法释放资源
- 3.测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量宽度
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
mWidth = specSize;
} else {
if (specMode == MeasureSpec.AT_MOST) {
mWidth = Math.min((int) (mTextWidth * 1.8f), specSize);
}
}
//测量高度
specMode = MeasureSpec.getMode(heightMeasureSpec);
specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
mHeight = specSize;
} else {
if (specMode == MeasureSpec.AT_MOST) {
mHeight = Math.min((int) (mTextWidth * 16f), specSize);
}
}
setMeasuredDimension(mWidth, mHeight);
}
- 4.初始化画笔
private void init() {
mText = getCharAndNum(mTextCount);
//初始化验证码画笔
mTextPaint = new Paint();
mTextPaint.setStrokeWidth(3);
mTextPaint.setTextSize(mTextSize);
//初始化干扰点画笔
mPointPaint = new Paint();
mPointPaint.setStrokeWidth(6);
mPointPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
//初始化干扰线画笔
mPathPaint = new Paint();
mPathPaint.setStrokeWidth(5);
mPathPaint.setColor(Color.GRAY);
mPathPaint.setStyle(Paint.Style.STROKE); // 设置画笔为空心
mPathPaint.setStrokeCap(Paint.Cap.ROUND); // 设置断点处为圆形
// 取得验证码字符串显示的宽度值
mTextWidth = mTextPaint.measureText(mText);
}
初始化最好不用放在onDraw()
方法里,在实际过程中onDraw()
方法可能会被多次调用。
- 5.初始化数据
private void initData() {
mPoints.clear();
// 生成干扰点坐标
for (int i = 0; i < 150; i++) {
PointF pointF = new PointF(mRandom.nextInt(mWidth) + 10, mRandom.nextInt(mHeight) + 10);
mPoints.add(pointF);
}
mPaths.clear();
// 生成干扰线坐标
for (int i = 0; i < 2; i++) {
Path path = new Path();
int startX = mRandom.nextInt(mWidth / 3) + 10;
int startY = mRandom.nextInt(mHeight / 3) + 10;
int endX = mRandom.nextInt(mWidth / 2) + mWidth / 2 - 10;
int endY = mRandom.nextInt(mHeight / 2) + mHeight / 2 - 10;
path.moveTo(startX, startY);
path.quadTo(Math.abs(endX - startX) / 2, Math.abs(endY - startY) / 2, endX, endY);
mPaths.add(path);
}
}
- 6.最后一步,绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setBackgroundColor(Color.GRAY);
initData();
float charLength = mTextWidth / mTextCount;
//绘制验证码
for (int i = 1; i <= mTextCount; i++) {
int offsetDegree = mRandom.nextInt(15);
// 这里只会产生0和1,如果是1那么正旋转正角度,否则旋转负角度
offsetDegree = mRandom.nextInt(2) == 1 ? offsetDegree : -offsetDegree;
canvas.save();
//旋转画布
canvas.rotate(offsetDegree, mWidth / 2, mHeight / 2);
// 给画笔设置随机颜色
mTextPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawText(String.valueOf(mText.charAt(i - 1)), (i - 1) * charLength * 1.6f + 30, mHeight * 2 / 3f, mTextPaint);
//画完一定要把画布转回原来的位置,不然会影响后面的绘制
canvas.restore();
}
// 绘制干扰点
for (PointF pointF : mPoints) {
mPointPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPoint(pointF.x, pointF.y, mPointPaint);
}
//绘制干扰线
for (Path path : mPaths) {
mPathPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPath(path, mPathPaint);
}
}
- 7.随机生成字符串和数值
/**
* 随机生成字符串和数值
*
* @param length length
* @return String
*/
public static String getCharAndNum(int length) {
String val = "";
Random random = new Random();
for (int i = 0; i < length; i++) {
// 输出字母还是数字
String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
// 字符串
if ("char".equalsIgnoreCase(charOrNum)) {
// 取得大写字母还是小写字母
int choice = random.nextInt(2) % 2 == 0 ? 65 : 97;
val += (char) (choice + random.nextInt(26));
} else if ("num".equalsIgnoreCase(charOrNum)) { // 数字
val += String.valueOf(random.nextInt(10));
}
}
return val;
}
后记,Android自定义View并没有想象中的那么可怕和恐怖,只要能静下心来去思考多在纸上写写画画就能发现其实并没有多难,网上各大论坛都有很优秀的博客和教程,我们并不是独自战斗,而是站在巨人的肩膀上前行,共勉之。附一张我学习自定义View的手稿,难堪的字体可以忽略。