前言
本文链接:http://blog.csdn.net/dreamsever/article/details/53467708
前一段时间看到干货集中营 推荐的一个开源项目验证码CaptchaImageView,可用于动态生成验证码,项目地址:https://github.com/jineshfrancs/CaptchaImageView。我就忽然联想到陆金所App的动态验证码效果挺赞的,因为它不仅有文字倾斜,文字上下错位间距,中间黑曲线遮挡,还有文字背景阴影和文字变形。
下面是陆金所验证码效果,奈何这个app禁止了系统截屏,我只有手动拍照了。。
当时就很想实现一下这样的效果,经过网上的查找我找到两篇博客:
android自定义view(一),打造绚丽的验证码
Android仿斗鱼领取鱼丸文字验证(三)
其实看效果第一篇博客的效果就挺好的,如果不需要那么花哨,干货集中营推荐的CaptchaImageView也是可以用的,但是从效果和体验上我感觉陆金所的更好,更像网页版的动态验证码而且又不难看清楚文字,点击一下刷新验证码,最主要的还是这里有前面几位都没有的效果:文字变形。我就苦思冥想自定义View绘制文字的时候,哪个属性可以让文字变形,没有想出来,我就查了半天试了n次,最后找到了一篇爱哥的博客:http://blog.csdn.net/aigestudio/article/details/41960507,里面大胸美女的那段代码你会发现一个方法:
// 绘制网格位图
canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, matrixMoved, 0, null, 0, null);
这个方法能够实现图片变形,我们可以先生成文字的图片,然后对这个图片进行变形,这样就可以实现文字变形的动态验证码
最终效果:
实现
一、思路
关于思路我就大致的说说,其实除了文字变形基本都是参考的android自定义view(一),打造绚丽的验证码 这篇博客。感兴趣可以具体去看看。
首先,你要会基本的自定义View,市面上关于自定义View的博客太多了,如果对自定义View不了解的可以先去补补课。看到效果我们首先想到的是在onDraw()方法里面画一个背景,生成一个验证码,然后生成不同方位的文字,绘制上去。现在有了文字变形,所以大致步骤需要调节一下,现在是:
1、根据:
mbitmap = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
Canvas myCanvas=new Canvas(mbitmap);
得到一个画布,在这个画布上绘制上下倾斜错位不同颜色的文字,同时也得到了bitmap
2、对上面的到的bitmap进行变形,使用canvas.drawBitmapMesh(。。)方法,可以对图片进行变形扭曲等等,非常强大。用爱哥的话是:它可以对Bitmap做几乎任何改变。前提只要你算法够强
3、使用path绘制干扰线,要有一定的随机性
二、代码
先生成两个空画布,其中一个画布得到变形前的形状的bitmap,后一个画布的到最终的bitmap,最终显示出来
mbitmap = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
codebitmap = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
Canvas myCanvas=new Canvas(mbitmap);
Canvas canvas=new Canvas(codebitmap);
生成随机验证码:
/**
* java生成随机数字和字母组合
* @return 随机验证码
*/
public String getCharAndNumr() {
String val = "";
Random random = new Random();
for (int i = 0; i < codeNum; 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));
}
}
vCode=val;
return val;
}
已经画了背景,得到了验证码,现在把验证码绘制成上下倾斜错位且不同颜色的文字效果
for (int i=0;i<codeNum;i++){
int offsetDegree=mRandom.nextInt(15);
// 这里只会产生0和1,如果是1那么正旋转正角度,否则旋转负角度
offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;
myCanvas.save();
myCanvas.rotate(offsetDegree,mWidth/2,mHeight/2);
// 给画笔设置随机颜色,+20是为了去除一些边界值
textPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
myCanvas.drawText(String.valueOf(tempCode.charAt(i)), i * charLength * 1.6f+30, mHeight * 2 / 3f, textPaint);
myCanvas.restore();
}
关于图像扭曲变形这块建议先去看看爱哥的那一篇博客,先看看drawBitmapMesh到底是怎么工作的,我推荐这两篇博客:
自定义控件其实很简单5/12
Android简单实现界面扭曲与简单的物理效果实现
这两篇博客应该就可以看懂drawBitmapMesh是怎么通过网格对图像进行扭曲的,如果还没有看懂还可以再找找其他的博客看看,对这个方法的原理理解透彻,并且你的算法能力比较强,也许你能够实现更加炫酷的效果。由于我的算法能力不是很强,这里我的实现扭曲方法是将验证码bitmap分成4*3个网格,共(4+1)*(3+1)=20个点
不要笑我的这太简单了,主要是这样实现的效果基本上也挺好的,如果你想要更好的效果可以去按照这个套路去优化,反正原理你已经知道了
代码:
int index=0;
float bitmapwidth= mbitmap.getWidth();
float bitmapheight= mbitmap.getHeight();
for(int i=0;i<HEIGHT+1;i++){
float fy=bitmapheight/HEIGHT*i;
for(int j=0;j<WIDTH+1;j++){
float fx=bitmapwidth/WIDTH*j;
//偶数位记录x坐标 奇数位记录Y坐标
origs[index*2+0]=verts[index*2+0]=fx;
origs[index*2+1]=verts[index*2+1]=fy;
index++;
}
}
//设置变形点,这些点将会影响变形的效果
offset=bitmapheight/HEIGHT/3;
verts[12]=verts[12]-offset;
verts[13]=verts[13]+offset;
verts[16]=verts[16]+offset;
verts[17]=verts[17]-offset;
verts[24]=verts[24]+offset;
verts[25]=verts[25]+offset;
最后扭曲,加干扰线
// 对验证码图片进行扭曲变形
canvas.drawBitmapMesh(mbitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
// 产生干扰效果2 -- 干扰线
for(Path path : mPaths){
linePaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPath(path, linePaint);
}
三、完整代码
package sgffsg.com.verifycodeview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.Random;
/**
* Verification Code View
* Created by sgffsg on 16/11/30.
*/
public class VerificationCodeView extends View {
//将图片划分成4*3个小格
private static final int WIDTH=4;
private static final int HEIGHT=3;
//小格相交的总的点数
private int COUNT=(WIDTH+1)*(HEIGHT+1);
private float[] verts=new float[COUNT*2];
private float[] origs=new float[COUNT*2];
//黄背景颜色
private int YELLOW_BG_COLOR = 0xfff9dec1;
//蓝背景颜色
private int BLUE_BG_COLOR = 0xffdcdef8;
private RectF mBounds;//用于获取控件宽高
private Rect textBound;//用于计算文本的宽高
private Paint bgPaint;//背景画笔
private Paint textPaint;
private Paint linePaint;
private String tempCode;//当前生成的验证码
private int codeNum = 4;//验证码位数 4或6。。
private Random mRandom;
//控件总宽度
private int mWidth;
//控件高度
private int mHeight;
private Bitmap mbitmap;
private Bitmap codebitmap;
/**
* 绘制贝塞尔曲线的路径集合
*/
private ArrayList<Path> mPaths = new ArrayList<Path>();
private float offset=5;//扭曲偏移
private String vCode;
public VerificationCodeView(Context context) {
this(context,null);
}
public VerificationCodeView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽和高的SpecMode和SpecSize
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//分别判断宽高是否设置为wrap_content
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
//宽高都为wrap_content,直接指定为400
setMeasuredDimension(mWidth, mWidth);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
//只有宽为wrap_content,宽直接指定为400,高为获取的SpecSize
setMeasuredDimension(mWidth, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
//只有高为wrap_content,高直接指定为400,宽为获取的SpecSize
setMeasuredDimension(wSpecSize, mWidth);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBounds=new RectF(getLeft(),getTop(),getRight(),getBottom());
mWidth= (int) (mBounds.right-mBounds.left);
mHeight= (int) (mBounds.bottom-mBounds.top);
createCodeBitmap();
}
/**
* 初始化
*/
private void initView() {
mRandom=new Random();
bgPaint=new Paint();
bgPaint.setAntiAlias(true);
bgPaint.setColor(YELLOW_BG_COLOR);
linePaint=new Paint();
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(Color.BLACK);
linePaint.setStrokeWidth(5);
linePaint.setStrokeCap(Paint.Cap.ROUND);
textPaint=new Paint();
textPaint.setAntiAlias(true);
textPaint.setTextSize(DisplayUtils.spToPx(getContext(),30));
textPaint.setShadowLayer(5,3,3,0xFF999999);
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setTextScaleX(0.8F);
textPaint.setColor(Color.GREEN);
textBound=new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(codebitmap,0,0,null);
}
/**
* 生成验证码图片
*/
private void createCodeBitmap() {
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);
}
mbitmap = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
codebitmap = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
Canvas myCanvas=new Canvas(mbitmap);
Canvas canvas=new Canvas(codebitmap);
tempCode=getCharAndNumr();
//画背景
myCanvas.drawColor(YELLOW_BG_COLOR);
textPaint.getTextBounds(tempCode,0,codeNum,textBound);
float charLength=(textBound.width())/codeNum;
for (int i=0;i<codeNum;i++){
int offsetDegree=mRandom.nextInt(15);
// 这里只会产生0和1,如果是1那么正旋转正角度,否则旋转负角度
offsetDegree = mRandom.nextInt(2) == 1?offsetDegree:-offsetDegree;
myCanvas.save();
myCanvas.rotate(offsetDegree,mWidth/2,mHeight/2);
// 给画笔设置随机颜色,+20是为了去除一些边界值
textPaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
myCanvas.drawText(String.valueOf(tempCode.charAt(i)), i * charLength * 1.6f+30, mHeight * 2 / 3f, textPaint);
myCanvas.restore();
}
int index=0;
float bitmapwidth= mbitmap.getWidth();
float bitmapheight= mbitmap.getHeight();
for(int i=0;i<HEIGHT+1;i++){
float fy=bitmapheight/HEIGHT*i;
for(int j=0;j<WIDTH+1;j++){
float fx=bitmapwidth/WIDTH*j;
//偶数位记录x坐标 奇数位记录Y坐标
origs[index*2+0]=verts[index*2+0]=fx;
origs[index*2+1]=verts[index*2+1]=fy;
index++;
}
}
//设置变形点,这些点将会影响变形的效果
offset=bitmapheight/HEIGHT/3;
verts[12]=verts[12]-offset;
verts[13]=verts[13]+offset;
verts[16]=verts[16]+offset;
verts[17]=verts[17]-offset;
verts[24]=verts[24]+offset;
verts[25]=verts[25]+offset;
// 对验证码图片进行扭曲变形
canvas.drawBitmapMesh(mbitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
// 产生干扰效果2 -- 干扰线
for(Path path : mPaths){
linePaint.setARGB(255, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20, mRandom.nextInt(200) + 20);
canvas.drawPath(path, linePaint);
}
}
/**
* java生成随机数字和字母组合
* @return 随机验证码
*/
public String getCharAndNumr() {
String val = "";
Random random = new Random();
for (int i = 0; i < codeNum; 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));
}
}
vCode=val;
return val;
}
/**
* refresh verification Code
*/
public void refreshCode(){
createCodeBitmap();
invalidate();
}
/**
* get verification code
* @return verification code
*/
public String getvCode() {
return vCode;
}
}
具体项目已经上传至github欢迎star
github项目地址
参考文献
android自定义view(一),打造绚丽的验证码
Android仿斗鱼领取鱼丸文字验证(三)
Android简单实现界面扭曲与简单的物理效果实现
自定义控件其实很简单5/12