Android实现动态验证码的技术调研与实现

来源:互联网 发布:切片软件哪个好 编辑:程序博客网 时间:2024/05/22 14:38

前言

本文链接: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);            // 这里只会产生01,如果是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

1 0
原创粉丝点击