Android之签字板


前言

随着公司发展需求,很多金融APP都会涉及到需要用户签字的环节,所以在此贴出代码以供参考少踩坑。

一、效果图

在这里插入图片描述

二、实现步骤

1.GestureSignatureView类

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布局

<?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类(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类(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.动态申请权限(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.动态申请权限(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));
                        }
                    }

总结

以上便是签字板所有代码了,kotlin和Java我都分别贴了上去,注册activity的时候需要设置横屏显示即可,竖屏也行,看需求,然后保持图片需要用到权限这个一定要记得,欢迎讨论指正!

猜你喜欢

转载自blog.csdn.net/Android_Cll/article/details/131643356