转载请表明出处:
一、概述
最近学习了一些关于自定义View控件的博客,积累了一些理论知识(自我感觉良好ing,哈哈)。现在想通过一个自定义验证码
控件来检验下自己的学习成果。先来讲下自定一view步骤:
1、自定义view属性;
2、在构造方法中获得我们自定义的属性;
3、重写onMeasure方法,设置控件大小;
4、重写ondraw方法。
步骤3不一定是必须的,当layout_widht和layout_height值为wrap_content时,则需要重写。
二、自定义验证码控件
2、1 自定view属性
首先在res/values下新建文件attrs.xml,在文件中定义属性以及声明我们的属性样式。
<?xml version=1.0 encoding="utf-8"?>
<resources>
<attr name="codeLength" format="integer"/>
<attr name="codeSize" format="dimension"/>
<declare-styleable name="valideCode>
<attr name="codeLength"/>
<attr name="codeSize"/>
</ resources>
这个我们定义了验证码字符串长度、字体大小两个属性,format类型是该属性的的取值类型,
string,color,integer,enum,float,boolean,flag,fraction,dimension,reference,具体含义大家可以谷歌下,然后我们在布局文
件中声明:
<?html version=1.0 encoding="uft-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com.apk/res_auto
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<com.vann.validecodetextview.ValideCodeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingleft="10dp"
app:codeLength="4"
app:codeSize="16sp"/>
</LinearLayout>
注意布局文件中要引入xmlns:app=”http://schemas.android.com/apk/res-auto”,不然我们是无法获取到我们的自定属性的。
2、2构造函数初始化
新建类ValideCodeTextView继承View,重写ValideCodeTextView的构造函数,并使最后运行含有三个参数的构造函数,在含有
是三个参数的构造函数中进行自定义属性获取以及部分变量初始化
//最大随机字符间隔
private static final int MAX_PADDING = 16;
//字体大小
private float mCodeSize;
// 验证码长度
private int mCodeLength;
//验证码
private String mCode;
//控件文本画笔、范围
private Paint mPaint;
private Rect mBound;
//每个字符左边距距离
private int paddingleft;
private int paddingTop;
private int mWidthMode;
// 噪点画笔
private Paint mPointPaint;
// 干扰线画笔
private Paint mPathPaint;
private Context mContext;
private Random mRandom = new Random();
public ValideCodeTextView(Context context) {
this(context, null);
}
public ValideCodeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ValideCodeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
//获得自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.valideCode);
mCodeLength = ta.getInt(R.styleable.valideCode_codeLength, 1);
mCodeSize = ta.getDimension(R.styleable.valideCode_codeSize, 0);
ta.recycle();
mCode = getRandomText();
//文本画笔初始化
mPaint = new Paint();
mPaint.setTextSize(mCodeSize);
mBound = new Rect();
mPaint.getTextBounds(mCode, 0, mCodeLength, mBound);
mPathPaint = new Paint();
mPointPaint = new Paint();
}
在三个参数的构造函数中获取到我们前面自定义的属性codeLength,codeSize,并对验证码文本画笔进行初始化。根据获取到的
自定义验证码长度获取随机验证码,验证码随机获取方法
如下:
/**
* 获取随机位数的数字与符号的组合字符串
*
* @return
*/
private String getRandomText() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mCodeLength; i++) {
String ch = mRandom.nextInt(2) % 2 == 0 ? "char" : "num";
if ("char".equals(ch)) {
int lowerOrUp = mRandom.nextInt(2) % 2 == 0 ? 65 : 97;
char val = (char) (lowerOrUp + mRandom.nextInt(26));
sb.append(val);
} else if ("num".equals(ch)) {
sb.append(String.valueOf(mRandom.nextInt(10)));
}
}
return sb.toString();
}
2、3 重写onMeasure方法
当验证码控件定义的不是一个精确值时,我们需要重写该方法获得他的测量值、测量模式来确定控件的大小。重写之前先了解
MeasureSpec的
specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面是我们重写onMeasure代码:
/**
* 计算测量值、模式,设置控件大小
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
mWidthMode = widthMode;
int width, height;
if (MeasureSpec.EXACTLY == widthMode) {
width = widthSize;
} else {
mPaint.setTextSize(mCodeSize);
randomPaintStyle(mPaint);
int tWidth = 0;
Rect child;
for (int i = 0; i < mCodeLength; i++) {
child = new Rect();
char c = mCode.charAt(i);
mPaint.getTextBounds(c + "", 0, 1, child);
float w = child.width();
int dw = (int) (w + MAX_PADDING);
tWidth += dw;
}
width = tWidth + getPaddingLeft() + getPaddingRight();
}
if (MeasureSpec.EXACTLY == heightMode) {
height = heightSize;
} else {
mPaint.setTextSize(mCodeSize);
randomPaintStyle(mPaint);
mPaint.getTextBounds(mCode, 0, mCodeLength, mBound);
float tHeight = mBound.height();
int h = (int) (tHeight + getPaddingTop() + getPaddingBottom());
height = h + MAX_PADDING;
}
setMeasuredDimension(width, height);
}
在该方法中我们先获得了验证码控件的测量值和测量模式,当测量模式为MeasureSpec.EXACTLY 时,父控件用width,height,
进行setMeasureDimension大小设置,否则,对画笔样式进行随机设置,此时width等于左边距+右边距+每个字符的宽+字符
间距的最大随机数,取最大随机数是为了保证验证码文本不超出控件范围之外,文本画笔样式随机设置如下:
/**
* 设置文本画笔的随机样式
*
* @param paint
*/
private void randomPaintStyle(Paint paint) {
paint.setTextSize(mCodeSize);
paint.setColor(getRandomColor(1));
paint.setFakeBoldText(mRandom.nextBoolean());//是否粗体
float skewx = mRandom.nextInt(11) / 10;
skewx = mRandom.nextBoolean() ? skewx : -skewx;
paint.setTextSkewX(skewx);// float类型参数,负数右倾,正数左倾
}
2、4 重写onDraw方法
通过onMeasure方法我们已经确定了控件的大小,现在通过onDraw方法来画出验证码文本,先定义画板的默认背景色为白色,
然后获取每个随机验证码的字符宽度以及左边距,并记录下左边距,在画板上通过drawText方法分别画出字符。代码如下:
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.WHITE);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
Rect rec;
int lastWidth = 0;
paddingleft = getPaddingLeft();
for (int i = 0; i < mCodeLength; i++) {
paddingTop = getHeight() / 2 + mRandom.nextInt(MAX_PADDING);
randomPaintStyle(mPaint);
char ch = mCode.charAt(i);
rec = new Rect();
mPaint.getTextBounds(ch + "", 0, 1, rec);
lastWidth = rec.width();
paddingleft += lastWidth + mRandom.nextInt(MAX_PADDING);
canvas.drawText(mCode.charAt(i) + "", paddingleft, paddingTop, mPaint);
}
int dw = getMeasuredWidth();
int w = paddingleft + lastWidth;
// Toast.makeText(mContext,"控件长度:"+dw+"验证码长度:"+w,Toast.LENGTH_SHORT).show();
// 当模式不为EXACTLY且验证码长度大于控件长度时,重新获取验证码并绘制
if (MeasureSpec.EXACTLY != mWidthMode && paddingleft + lastWidth > dw) {
mCode = getRandomText();
postInvalidate();
return;
}
for (int i = 0; i < 2; i++) {
randomPathPaintStyle(mPathPaint);
drawLine(canvas, mPathPaint);
}
for (int i = 0; i < 50; i++) {
randomPointPaintStyle(mPointPaint);
drawPoint(canvas, mPointPaint);
}
canvas.save(Canvas.ALL_SAVE_FLAG);
canvas.restore();
}
画出文本后,判断后一个字符的左边距是否大于控件的实际距离,如果大于则重新获取随机验证码字符串,如果没有,则进行噪
点和干扰线的生成操作,噪点我们固定生成150各,干扰线为2条,噪点生成的代码如下:
/**
* 绘制噪点
*
* @param canvas
* @param mPointPaint
*/
private void drawPoint(Canvas canvas, Paint mPointPaint) {
int x = mRandom.nextInt(getWidth());
int y = mRandom.nextInt(getHeight());
canvas.drawPoint(x, y, mPointPaint);
}
每次画噪点前先随机获取噪点画笔的样式,以便获取到不同颜色的噪点。画干扰线的方法如下:
/**
* 绘制干扰线
*
* @param canvas
* @param mPaint
*/
private void drawLine(Canvas canvas, Paint mPaint) {
int startX = mRandom.nextInt(getWidth());
int startY = mRandom.nextInt(getHeight());
int endX = mRandom.nextInt(getWidth());
int endY = mRandom.nextInt(getHeight());
canvas.drawLine(startX, startY, endX, endY, mPaint);
}
至此验证码已画出,当我们点击控件时需要刷新验证码文本,因此,需要给控件添加一个监听事件,在构造函数中添加添加事
件,并在添加事件中重新获取随机验证码,重绘图像,监听代码如下:
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mCode = getRandomText();
postInvalidate();
}
});
好了,现在看下运行结果:
参考博客: