Signature Pad for Android


foreword

With the development needs of the company, many financial apps will involve the link that requires the user's signature, so the code is posted here for reference and less pitfalls.

1. Rendering

insert image description here

2. Implementation steps

1. GestureSignatureView class

package com.example.kotlinbasedome.activity.utils;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @Author : CaoLiulang
 * @Time : 2023/7/7 17:02
 * @Description :自定义签字板
 */
public class GestureSignatureView extends View {
    
    

    private static final String TAG = "GestureSignatureView";
    private Path mPath;//绘制路径
    private Paint mPaint;// 绘制画笔
    private Canvas mCanvas;//背景画布
    private Bitmap mMBitmap;//背景bitmap
    private boolean isTouchedSignature = false;//是否签名 默认为false

    public GestureSignatureView(Context context) {
    
    
        this(context, null);
    }

    public GestureSignatureView(Context context, AttributeSet attrs) {
    
    
        this(context, attrs, 0);
    }

    public GestureSignatureView(Context context, AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "onMeasure: 测量的宽高:" + getMeasuredWidth() + "-----------" + getMeasuredHeight());
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    
        super.onLayout(changed, left, top, right, bottom);
        mMBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mMBitmap);
        mCanvas.drawColor(Color.TRANSPARENT);
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
    }

    private void initPaint() {
    
    
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(10.0f);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setDither(true);
        mPath = new Path();

    }

    @Override
    protected void onDraw(Canvas canvas) {
    
    
        canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
        canvas.drawBitmap(mMBitmap, 0, 0, mPaint);
        // 通过画布绘制多点形成的图形
        canvas.drawPath(mPath, mPaint);
    }

    private float[] downPoint = new float[2];

    private float[] previousPoint = new float[2];

    /**
     * 监听触摸事件的回调
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
    
        //获取距离自身(点击位置)左边界的距离
        downPoint[0] = event.getX();
        //获取距离自身(点击位置)上边界的距离
        downPoint[1] = event.getY();
        switch (event.getAction()) {
    
    
            //手势开始
            case MotionEvent.ACTION_DOWN:
                previousPoint[0] = downPoint[0];
                previousPoint[1] = downPoint[1];
                // moveTo 不会进行绘制,只用于移动移动画笔。
                mPath.moveTo(downPoint[0], downPoint[1]);
                break;
            //手势过程
            case MotionEvent.ACTION_MOVE:
                float dX = Math.abs(downPoint[0] - previousPoint[0]);
                float dY = Math.abs(downPoint[1] - previousPoint[1]);

                // 两点之间的距离大于等于3时,生成贝塞尔绘制曲线
                if (dX >= 3 || dY >= 3) {
    
    
                    // 设置贝塞尔曲线的操作点为起点和终点的一半
                    float cX = (downPoint[0] + previousPoint[0]) / 2;
                    float cY = (downPoint[1] + previousPoint[1]) / 2;
                    //quadTo 用于绘制圆滑曲线,即贝塞尔曲线 二次贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点
                    mPath.quadTo(previousPoint[0], previousPoint[1], cX, cY);
                    // 第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值
                    previousPoint[0] = downPoint[0];
                    previousPoint[1] = downPoint[1];
                }
                break;
            //手势结束
            case MotionEvent.ACTION_UP:
                //设置签名成功状态
                isTouchedSignature = true;
                mCanvas.drawPath(mPath, mPaint);
                mPath.reset();
                break;
        }

        invalidate();
        return true;
    }

    // 缩放
    public static Bitmap resizeImage(Bitmap bitmap, int width, int height) {
    
    
        int originWidth = bitmap.getWidth();
        int originHeight = bitmap.getHeight();

        float scaleWidth = ((float) width) / originWidth;
        float scaleHeight = ((float) height) / originHeight;

        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, originWidth,
                originHeight, matrix, true);
        return resizedBitmap;
    }

    public Bitmap getPaintBitmap() {
    
    
        return resizeImage(mMBitmap, 320, 480);
    }

    public void clear() {
    
    
        if (mCanvas != null) {
    
    
            isTouchedSignature = false;
            mPath.reset();
            mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            invalidate();
        }
    }

    /**
     * 保存画板
     *
     * @param path 保存到路径
     */

    public void save(String path) {
    
    
        try {
    
    
            save(path, true, 50);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public Bitmap getBitmap() {
    
    
        return mMBitmap;
    }

    /**
     * 保存画板
     *
     * @param path       保存到路径
     * @param clearBlank 是否清除空白区域
     * @param blank      边缘空白区域
     */
    public void save(String path, boolean clearBlank, int blank) throws IOException {
    
    

        Bitmap bitmap = mMBitmap;
        if (clearBlank) {
    
    
            bitmap = clearBlank(mMBitmap, blank);
        }
        Bitmap littleBmp = ConstantsUtil.smallImage(bitmap, 700);
        Bitmap newBitmap = Bitmap.createBitmap(littleBmp.getWidth(), littleBmp.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(newBitmap);
        canvas.drawColor(Color.WHITE);
        canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
        canvas.drawBitmap(littleBmp, 0, 0, null);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        newBitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos);
        byte[] buffer = bos.toByteArray();
        if (buffer != null) {
    
    
            File file = new File(path);

            OutputStream outputStream = new FileOutputStream(file);
            outputStream.write(buffer);
            outputStream.close();
            scanMediaFile(file);
        }
    }

    /**
     * 是否有签名
     *
     * @return
     */
    public boolean getTouched() {
    
    
        return isTouchedSignature;
    }

    /**
     * 逐行扫描 清除边界空白。
     *
     * @param bp
     * @param blank 边距留多少个像素
     * @return
     */
    private Bitmap clearBlank(Bitmap bp, int blank) {
    
    
        int HEIGHT = bp.getHeight();
        int WIDTH = bp.getWidth();
        int top = 0, left = 0, right = 0, bottom = 0;
        int[] pixs = new int[WIDTH];
        boolean isStop;
        for (int y = 0; y < HEIGHT; y++) {
    
    
            bp.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1);
            isStop = false;
            for (int pix : pixs) {
    
    
                if (pix != Color.TRANSPARENT) {
    
    
                    top = y;
                    isStop = true;
                    break;
                }
            }
            if (isStop) {
    
    
                break;
            }
        }
        for (int y = HEIGHT - 1; y >= 0; y--) {
    
    
            bp.getPixels(pixs, 0, WIDTH, 0, y, WIDTH, 1);
            isStop = false;
            for (int pix : pixs) {
    
    
                if (pix != Color.TRANSPARENT) {
    
    
                    bottom = y;
                    isStop = true;
                    break;
                }
            }
            if (isStop) {
    
    
                break;
            }
        }
        pixs = new int[HEIGHT];
        for (int x = 0; x < WIDTH; x++) {
    
    
            bp.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT);
            isStop = false;
            for (int pix : pixs) {
    
    
                if (pix != Color.TRANSPARENT) {
    
    
                    left = x;
                    isStop = true;
                    break;
                }
            }
            if (isStop) {
    
    
                break;
            }
        }
        for (int x = WIDTH - 1; x > 0; x--) {
    
    
            bp.getPixels(pixs, 0, 1, x, 0, 1, HEIGHT);
            isStop = false;
            for (int pix : pixs) {
    
    
                if (pix != Color.TRANSPARENT) {
    
    
                    right = x;
                    isStop = true;
                    break;
                }
            }
            if (isStop) {
    
    
                break;
            }
        }
        if (blank < 0) {
    
    
            blank = 0;
        }
        left = left - blank > 0 ? left - blank : 0;
        top = top - blank > 0 ? top - blank : 0;
        right = right + blank > WIDTH - 1 ? WIDTH - 1 : right + blank;
        bottom = bottom + blank > HEIGHT - 1 ? HEIGHT - 1 : bottom + blank;
        return Bitmap.createBitmap(bp, left, top, right - left, bottom - top);
    }

    private void scanMediaFile(File photo) {
    
    
        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        Uri contentUri = Uri.fromFile(photo);
        mediaScanIntent.setData(contentUri);
        getContext().sendBroadcast(mediaScanIntent);
    }
}



2.xml layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="28dp"
        android:src="#DEDFE2" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2">

        <com.example.kotlinbasedome.activity.utils.GestureSignatureView
            android:id="@+id/signSave_gsv_signature"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white" />


    </LinearLayout>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:src="#DEDFE2" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="20dp"
            android:text="Preview:"
            android:textSize="20sp"
            android:typeface="serif">

        </TextView>

        <ImageView
            android:id="@+id/singImg"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:layout_marginLeft="20dp">

        </ImageView>


    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp">

        <TextView
            android:id="@+id/signSave_tv_cancel"
            android:layout_width="0dp"
            android:layout_height="35dp"
            android:layout_weight="1"
            android:background="#CDCDCD"
            android:clickable="true"
            android:gravity="center"
            android:onClick="onClick"
            android:text="EXIT"
            android:textColor="@color/white"
            android:textSize="16sp"
            android:typeface="serif" />

        <TextView
            android:id="@+id/signSave_tv_clear"
            android:layout_width="0dp"
            android:layout_height="35dp"
            android:layout_marginLeft="10dp"
            android:layout_weight="1"
            android:background="#acacac"
            android:clickable="true"
            android:gravity="center"
            android:onClick="onClick"
            android:text="CLEAN"
            android:textColor="@color/white"
            android:textSize="16sp"
            android:typeface="serif" />

        <TextView
            android:id="@+id/signSave_tv_save"
            android:layout_width="0dp"
            android:layout_height="35dp"
            android:layout_marginLeft="10dp"
            android:layout_weight="1"
            android:background="#1d9eec"
            android:clickable="true"
            android:gravity="center"
            android:onClick="onClick"
            android:text="CONFIRM"
            android:textColor="@color/white"
            android:textSize="16sp"
            android:typeface="serif" />
    </LinearLayout>

</LinearLayout>

3. Activity class (kotlin)

/**
 * @Author : CaoLiulang
 * @Time : 2023/7/7 17:02
 * @Description :签名
 */
class Signature : Activity(), OnClickListener {
    
    

    private lateinit var signSave_gsv_signature: GestureSignatureView
    private lateinit var singImg: ImageView
    private lateinit var signSave_tv_save: TextView
    private lateinit var signSave_tv_clear: TextView
    private lateinit var signSave_tv_cancel: TextView
    private lateinit var message: String
    private var imagurl: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        //去掉状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    
            val decorView = window.decorView
            val option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            decorView.systemUiVisibility = option
            window.statusBarColor = Color.parseColor("#00000000")
        }
        //修改状态栏文字为黑色
        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
                View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
        setContentView(R.layout.signature)
        instantiation()
    }

    fun instantiation() {
    
    
        signSave_gsv_signature = findViewById(R.id.signSave_gsv_signature)
        singImg = findViewById(R.id.singImg)
        signSave_tv_save = findViewById(R.id.signSave_tv_save)
        signSave_tv_clear = findViewById(R.id.signSave_tv_clear)
        signSave_tv_cancel = findViewById(R.id.signSave_tv_cancel)
        signSave_tv_save.setOnClickListener(this)
        signSave_tv_clear.setOnClickListener(this)
        signSave_tv_cancel.setOnClickListener(this)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onClick(v: View?) {
    
    
        when (v?.id) {
    
    
            //保存
            R.id.signSave_tv_save ->
                //防止多次触发
                if (ButtonUtils.isFastDoubleClick(R.id.signSave_tv_save) === false) {
    
    
                    if (imagurl == "") {
    
    
                        if (!signSave_gsv_signature.touched) {
    
    
                            ToastUtilsKT.showToast1("Please sign first")
                            return
                        }
                        val file = File(ConstantsUtil.IMG_FOLDER_PATH)
                        file.mkdirs()
                        val fillPath: String =
                            ConstantsUtil.IMG_FOLDER_PATH + "signImg" + System.currentTimeMillis() + ".jpg"
                        Log.i("w--", fillPath)
                        signSave_gsv_signature.save(fillPath)
                        println("图片路径打印:$fillPath")
                        Glide.with(this)
                            .load(fillPath)
                            .into(singImg)
                        imagurl = fillPath
                       //网络请求
                    } else {
    
    
                        //网络请求
                    }
                }
            //清空
            R.id.signSave_tv_clear -> {
    
    
                signSave_gsv_signature.clear()
                singImg.setImageDrawable(null)
                imagurl = ""
            }
            //返回
            R.id.signSave_tv_cancel ->
                finish()
        }
    }

4. Activity class (Java)

public class Signature extends Activity implements View.OnClickListener {
    
    

    private TextView signSave_tv_cancel;//退出
    private TextView signSave_tv_clear;//清除
    private TextView signSave_tv_save;//保存
    private GestureSignatureView signSave_gsv_signature;//签字板
    private ImageView singImg;//签名图片展示
    private String message;//返回消息
    private String imagurl = "";//图片路径

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        //去掉状态栏
        if (Build.VERSION.SDK_INT >= 21) {
    
    
            View decorView = getWindow().getDecorView();
            int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            decorView.setSystemUiVisibility(option);
            getWindow().setStatusBarColor(Color.parseColor("#00000000"));
        }
        //修改状态栏文字为黑色
        getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                        View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        setContentView(R.layout.signature);
        instantiation();
    }

    //实例化
    private void instantiation() {
    
    
        signSave_tv_cancel = findViewById(R.id.signSave_tv_cancel);
        signSave_tv_clear = findViewById(R.id.signSave_tv_clear);
        signSave_tv_save = findViewById(R.id.signSave_tv_save);
        signSave_gsv_signature = findViewById(R.id.signSave_gsv_signature);
        singImg = findViewById(R.id.singImg);
        signSave_tv_save.setOnClickListener(this);
        signSave_tv_clear.setOnClickListener(this);
        signSave_tv_cancel.setOnClickListener(this);
    }

    @SuppressLint("NewApi")
    @Override
    public void onClick(View view) {
    
    
        switch (view.getId()) {
    
    
            //退出
            case R.id.signSave_tv_cancel:
                finish();
                break;
            //清除
            case R.id.signSave_tv_clear:
                signSave_gsv_signature.clear();
                singImg.setImageDrawable(null);
                imagurl = "";
                break;
            //保存
            case R.id.signSave_tv_save:
                //防止多次触发
                if (ButtonUtils.isFastDoubleClick(R.id.signSave_tv_save) == false) {
    
    
                    if (imagurl.equals("")) {
    
    
                        if (!signSave_gsv_signature.getTouched()) {
    
    
                            ToastUtils.ToastCllShow("您尚未签字");
                            return;
                        }
                        File file = new File(ConstantsUtil.IMG_FOLDER_PATH);
                        file.mkdirs();
                        String fillPath = ConstantsUtil.IMG_FOLDER_PATH + "signImg" + System.currentTimeMillis() + ".jpg";
                        Log.i("w--", fillPath);
                        signSave_gsv_signature.save(fillPath);
                        System.out.println("图片路径打印:" + fillPath);
                        Glide.with(this)
                                .load(fillPath)
                                .into(singImg);
                        imagurl = fillPath;
                        //网络请求
                    } else {
    
    
                        //网络请求
                    }
                }
                break;
        }
    }

5. Dynamic application permissions (kotlin)

                   //6.0才用动态权限
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
                        //读写权限
                        if (ContextCompat.checkSelfPermission(
                                this@PDFWebViewActivity,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE
                            )
                            != PackageManager.PERMISSION_GRANTED
                        ) {
    
    
                            ActivityCompat.requestPermissions(
                                this@PDFWebViewActivity,
                                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                                1
                            )
                        } else {
    
    //有权限
                            startActivity(Intent(this@PDFWebViewActivity,Signature::class.java))
                        }
                    }

6. Dynamic application permission (Java)

    //6.0才用动态权限
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
                        //读写权限
                        if (ContextCompat.checkSelfPermission(PDFWebViewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                                != PackageManager.PERMISSION_GRANTED) {
    
    
                            ActivityCompat.requestPermissions(PDFWebViewActivity.this,
                                    new String[]{
    
    Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
                        } else {
    
    
                            startActivity(new Intent(PDFWebViewActivity.this, Signature.class));
                        }
                    }

Summarize

The above is all the codes of the signature board. I have pasted both kotlin and Java respectively. When registering an activity, you need to set the horizontal screen display, and the vertical screen is also fine. See the requirements, and then keep the pictures that need to use permissions. This must be remembered. , welcome to discuss and correct!

Guess you like

Origin blog.csdn.net/Android_Cll/article/details/131643356