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

前言

本文链接: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
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值