Android 画布使用之电子签名

时间:2021-01-02 02:20:56

前言

促进小飞哥写代码的动力只有两个。第一个是为了挣钱钱,第二个是为了挣更多的钱。所以毫无疑问,电子签名又是公司最近需要开发的新功能。

应用场景:以前去银行办理业务都得去柜台,填N张表格,写N多个签名。随着智能机的普及、移动终端app的使用场景越来越广泛。手机银行app也给越来越多的人办理银行业务提供便捷,妈妈再也不用但是我们去银行排队浪费时间了。然而,有的业务该走的流程必须得走,所以电子签名的需求也就凸显出来。

这是一篇技术博客,那废话就不多说了。

原理

在Android开发中,所有用户可见的页面都是由一个个View(视图)拼接而成。Google 已经提供了很多的基础的View组件,如:显示图片的ImageView,显示文字的TextView,这些已经被造好的*能够让我们很方便的去开发绝大多数app。除此之外,有些*还得自己造,不然要程序员干嘛呢?虽然网上已经有很多已经实现过电子签名,但是小飞哥一直以来都崇尚自己动手,哪怕是Hello Word 也绝不copy。(又扯远了!!!)

说了那么多,实现电子签名的组件(在程序中命名SignView)需要继承View,这也是面向对象编程的一大特色,直接继承View,为我们省去不少麻烦。记录用户在触屏上滑动的轨迹、重写onDraw方法将轨迹在屏幕上绘制出来,就实现了我们需要的效果了。然而一切并没有结束,还需要保存为图片,先上图:

Android 画布使用之电子签名

定义属性


    private Paint linePaint;// 画笔

    private ArrayList<Path> lines;// 写字的笔迹,支持多笔画
    private int lineCount;// 记录笔画数目

    private final int DEFAULT_LINE_WIDTH = 10;// 默认笔画宽度

    private int lineColor = Color.BLACK;// 默认字迹颜色(黑色)
    private float lineWidth = DEFAULT_LINE_WIDTH;// 笔画宽度
  1. 画笔:在屏幕上绘制出我们写下的笔迹,画笔主要有两个属性,颜色和粗细。这也是在程序中只开放设置接口的两个属性。
  2. 笔迹集合:不否认有人写字时喜欢一气呵成,但是支持多笔输入,可以让程序支持更多输入场景。
  3. 默认值:默认字迹颜色黑色,字迹宽度10 个像素点(这是一个很细的线,随便写的,不要介意)

接受输入信息

/** * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 上午10:49:58 * @see android.view.View#onTouchEvent(android.view.MotionEvent) */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        /** * 考虑到这个view会出现在lib工程里,因此使用if else */
        if (event.getAction() == MotionEvent.ACTION_DOWN) {// 按下这个屏幕
            Path path = new Path();
            path.moveTo(event.getX(), event.getY());
            lines.add(path);
            lineCount = lines.size();
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {// 在屏幕上移动
            lines.get(lineCount - 1).lineTo(event.getX(), event.getY());
            invalidate();
        } else {

        }
        return true;
    }

用户点击和在屏幕上移动都会触发该方法。MotionEvent指的是手指在屏幕上的运动事件。包含动作类型:按下、移动、抬起。点击屏幕上的位置,通过event.getX(),event.getY()方法获取。

用Path(路径)类,按下屏幕为记录笔画的开始,在屏幕上移动记录笔画的轨迹。调用invalidate方法,清空当前视图的图像信息并通知系统刷新视图,增加显示刚刚输入的信息。

显示输入信息

    /** * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 上午10:47:19 * @see android.view.View#onDraw(android.graphics.Canvas) */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (lines != null && lines.size() > 0) {
            for (Path path : lines)
                canvas.drawPath(path, linePaint);
        }
    }

onDraw方法,多数情况下为系统调用,(用户也可以自己调用,后面用到),通过Canvas(画布)将之前保存的笔迹绘制出现。参数中,指定了一个画笔。

定义画笔

    /** * 初始化画笔 * * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 下午5:23:26 */
    private void initLinePaint() {
        linePaint = new Paint();
        linePaint.setColor(lineColor);
        linePaint.setStrokeWidth(lineWidth);
        linePaint.setStrokeCap(Cap.ROUND);
        linePaint.setPathEffect(new CornerPathEffect(50));
        linePaint.setStyle(Style.STROKE);
    }

简简单单的几行,每行作用都极大。
首先new一个Paint对象,(程序猿间经常自嘲,我们的对象都是new出来的)。其次是设置画笔颜色和宽度。除了最后一行,设置画笔风格为stroke(绳子,这里命名很形象,还有其他两个风格就不多做表述,很抽象的概念)。其余两行是设置笔迹平滑的重要方法。Cap.ROUND使笔迹起始、结束位置为圆形,PahtEfect指笔迹的风格,CornerPathEffect在拐角处添加弧度,弧度半径50像素点。

绘制和显示笔迹的原理部分就介绍完了。在使用中,有以上的代码还远远不够。

  • 需要开放出接口,使别人可以自主的设置笔迹颜色、宽度。
  • 可以让别人获取到输入的图像信息,转成Bitmap对象或存文件。
  • 当书写错误,可以清空屏幕重新书写等。

设置画笔属性

在Android中,使用View有两种方式。
1. 在xml布局文件中添加view并指定组件属性
2. 在代码中动态添加view,最不被开发者所接受的方式

代码设置

    /** * @param lineColor * the lineColor to set */
    public void setLineColor(int lineColor) {
        this.lineColor = lineColor;
        linePaint.setColor(lineColor);
    }

    /** * @param lintWidth * the lintWidth to set */
    public void setLineWidth(float lineWidth) {
        this.lineWidth = lineWidth;
        linePaint.setStrokeWidth(lineWidth);
    }

通过在代码中调用组件的set方法,可以在任意时候设置画笔的属性。

自定义组件属性

既然可以通过再xml布局中使用自定义的组件,那么我们当然也希望可以在xml布局中静态的指定画笔颜色和宽度。10像素点的粗细是不被使用者所接受的。

通过如下的代码,给SignView声明两个新属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="SignView">
        <attr name="lineColor" format="color" />
        <attr name="lineWidth" format="dimension" />
    </declare-styleable>

</resources>

使用自定义属性

    <com.flueky.android.view.SignView  android:id="@+id/main_sign" android:layout_width="400dp" android:layout_height="400dp" app:lineColor="#FF0000" app:lineWidth="6dp" >
    </com.flueky.android.view.SignView>

眼尖的读者们很定奇怪,app是怎么来的,怎么通过app:lineColor就能使用我们之前新声明的两个属性。

Android 画布使用之电子签名

如图所所示,需要在布局文件的根节点定义app属性。同第一行定义android属性一样。我们只需要将res后面被涂改的部分替换成我们自己应用的包名即可。如果是在lib工程里,需要写成xmlns:app="http://schemas.android.com/apk/res-auto"

最后,还需要在代码中,获取到在xml布局布局中设置的属性值。需要介绍下view的四个构造函数的作用。

/** * @param context * @param attrs * @param defStyleAttr * @param defStyleRes */
    @SuppressLint("NewApi")
    public SignView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        if (attrs != null) {
            TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView, defStyleAttr, defStyleRes);
            parseTyepdArray(tArray);
        }
        initLinePaint();
        lines = new ArrayList<Path>();
    }

    /** * @param context * @param attrs * @param defStyleAttr */
    public SignView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (attrs != null) {
            TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView, defStyleAttr, 0);
            parseTyepdArray(tArray);
        }
        initLinePaint();
        lines = new ArrayList<Path>();
    }

    /** * @param context * @param attrs */
    public SignView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (attrs != null) {
            TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.SignView);
            parseTyepdArray(tArray);
        }
        initLinePaint();
        lines = new ArrayList<Path>();
    }

    /** * @param context */
    public SignView(Context context) {
        super(context);
        initLinePaint();
        lines = new ArrayList<Path>();
    }

以上4个构造函数中,三个都包含AttributeSet参数。除了第四个是在代码中动态添加组件使用,其余三个可以映射到布局文件中的代码。

    /** * 解析类型数组 * * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 下午5:15:25 * @param tArray */
    private void parseTyepdArray(TypedArray tArray) {
        lineColor = tArray.getColor(R.styleable.SignView_lineColor, Color.BLACK);
        lineWidth = tArray.getDimension(R.styleable.SignView_lineWidth, DEFAULT_LINE_WIDTH);
    }

获取图像信息

    /** * 保存view视图的bitmap信息 * * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 下午7:00:01 * @return */
    public Bitmap getImage() {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Config.RGB_565);
        Canvas canvas = new Canvas(bitmap);
        /** * 绘制背景 */
        Drawable bgDrawable = getBackground();
        if (bgDrawable != null)
            bgDrawable.draw(canvas);
        else
            canvas.drawColor(Color.WHITE);
        draw(canvas);// 绘制view视图的内容
        return bitmap;
    }

保存图像到文件

/** * 将图像保存到文件 * * @file Framework:com.flueky.android.view.SignView.java * @author flueky zuokefei0217@163.com * @time 2016年12月19日 下午7:00:49 * @param filePath * @return 返回false表示保存失败 */
    public boolean saveImageToFile(String filePath) {
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            getImage().compress(CompressFormat.PNG, 100, fos);
            fos.flush();
            fos.close();
            return true;
        } catch (FileNotFoundException e) {
            return false;
        } catch (IOException e) {
            return false;
        }
    }

清空输入

    public void clearPath() {
        lines.removeAll(lines);
        invalidate();
    }