android studio 跟踪滑动轨迹手写签名

activity

package com.example.demo2;

import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private final static String TAG = "SignatureActivity";
    private SignatureView view_signature; // 声明一个签名视图对象
    private ImageView iv_signature_new; // 声明一个图像视图对象
    private String mImagePath; // 签名图片的文件路径

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        view_signature = findViewById(R.id.view_signature);
        iv_signature_new = findViewById(R.id.iv_signature_new);
        findViewById(R.id.btn_begin_signature).setOnClickListener(this);
        findViewById(R.id.btn_end_signature).setOnClickListener(this);
        findViewById(R.id.btn_reset_signature).setOnClickListener(this);
        findViewById(R.id.btn_revoke_signature).setOnClickListener(this);
        findViewById(R.id.btn_save_signature).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_save_signature) { // 点击了保存签名按钮
            if (TextUtils.isEmpty(mImagePath)) {
                Toast.makeText(this, "请先开始然后结束签名", Toast.LENGTH_LONG).show();
                return;
            }
            BitmapUtil.notifyPhotoAlbum(this, mImagePath); // 通知相册来了张新图片
            Toast.makeText(this, "已保存签名图片,请到系统相册查看", Toast.LENGTH_LONG).show();
        } else if (v.getId() == R.id.btn_begin_signature) { // 点击了开始签名按钮
            // 开启签名视图的绘图缓存
            view_signature.setDrawingCacheEnabled(true);
        } else if (v.getId() == R.id.btn_reset_signature) { // 点击了重置按钮
            view_signature.clear(); // 清空签名视图
        } else if (v.getId() == R.id.btn_revoke_signature) { // 点击了回退按钮
            view_signature.revoke(); // 回退签名视图的最近一笔绘画
        } else if (v.getId() == R.id.btn_end_signature) { // 点击了结束签名按钮
            if (!view_signature.isDrawingCacheEnabled()) { // 签名视图的绘图缓存不可用
                Toast.makeText(this, "请先开始签名", Toast.LENGTH_LONG).show();
            } else { // 签名视图的绘图缓存当前可用
                Bitmap bitmap = view_signature.getDrawingCache(); // 从绘图缓存获取位图对象
                // 生成图片文件的保存路径
                mImagePath = String.format("%s/%s.jpg", getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(), DateUtil.getNowDateTime());
                BitmapUtil.saveImage(mImagePath, bitmap); // 把位图保存为图片文件
                iv_signature_new.setImageURI(Uri.parse(mImagePath)); // 设置图像视图的路径对象
                // 延迟100毫秒后启动绘图缓存的重置任务
                new Handler(Looper.myLooper()).postDelayed(() -> {
                    // 关闭签名视图的绘图缓存
                    view_signature.setDrawingCacheEnabled(false);
                    // 开启签名视图的绘图缓存
                    view_signature.setDrawingCacheEnabled(true);
                }, 100);
            }
        }
    }

}

SignatureView
package com.example.demo2;


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class SignatureView extends View {
    private static final String TAG = "SignatureView";
    private Paint mPathPaint = new Paint(); // 声明一个画笔对象
    private Path mPath = new Path(); // 声明一个路径对象
    private int mPathPaintColor = Color.BLACK; // 画笔颜色
    private int mStrokeWidth = 3; // 画笔线宽
    private PathPosition mPathPos = new PathPosition(); // 路径位置
    private List<PathPosition> mPathList = new ArrayList<>(); // 路径位置列表
    private PointF mLastPos; // 上次触摸点的横纵坐标

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

    public SignatureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (attrs != null) {
            // 根据SignatureView的属性定义,从布局文件中获取属性数组描述
            TypedArray attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.SignatureView);
            // 根据属性描述定义,获取布局文件中的画笔颜色
            mPathPaintColor = attrArray.getColor(R.styleable.SignatureView_paint_color, Color.BLACK);
            // 根据属性描述定义,获取布局文件中的画笔线宽
            mStrokeWidth = attrArray.getInt(R.styleable.SignatureView_stroke_width, 3);
            attrArray.recycle(); // 回收属性数组描述
        }
        initView(); // 初始化视图
    }

    // 初始化视图
    private void initView() {
        mPathPaint.setStrokeWidth(mStrokeWidth); // 设置画笔的线宽
        mPathPaint.setStyle(Paint.Style.STROKE); // 设置画笔的类型。STROKE表示空心,FILL表示实心
        mPathPaint.setColor(mPathPaintColor); // 设置画笔的颜色
        setDrawingCacheEnabled(true); // 开启当前视图的绘图缓存
    }

    // 清空画布
    public void clear() {
        mPath.reset(); // 重置路径对象
        mPathList.clear(); // 清空路径列表
        postInvalidate(); // 立即刷新视图(线程安全方式)
    }

    // 撤销上一次绘制
    public void revoke() {
        if (mPathList.size() > 0) {
            // 移除路径位置列表中的最后一个路径
            mPathList.remove(mPathList.size() - 1);
            mPath.reset(); // 重置路径对象
            for (int i = 0; i < mPathList.size(); i++) {
                PathPosition pp = mPathList.get(i);
                // 移动到上一个坐标点
                mPath.moveTo(pp.prePos.x, pp.prePos.y);
                // 连接上一个坐标点和下一个坐标点
                mPath.quadTo(pp.prePos.x, pp.prePos.y, pp.nextPos.x, pp.nextPos.y);
            }
            postInvalidate(); // 立即刷新视图(线程安全方式)
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(mPath, mPathPaint); // 在画布上绘制指定路径线条
    }

    // 在发生触摸事件时触发
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: // 按下手指
                mPath.moveTo(event.getX(), event.getY()); // 移动到指定坐标点
                mPathPos.prePos = new PointF(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE: // 移动手指
                // 连接上一个坐标点和当前坐标点
                mPath.quadTo(mLastPos.x, mLastPos.y, event.getX(), event.getY());
                mPathPos.nextPos = new PointF(event.getX(), event.getY());
                mPathList.add(mPathPos); // 往路径位置列表添加路径位置
                mPathPos = new PathPosition(); // 创建新的路径位置
                mPathPos.prePos = new PointF(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_UP: // 提起手指
                // 连接上一个坐标点和当前坐标点
                mPath.quadTo(mLastPos.x, mLastPos.y, event.getX(), event.getY());
                break;
        }
        mLastPos = new PointF(event.getX(), event.getY());
        postInvalidate(); // 立即刷新视图(线程安全方式)
        return true;
    }

}
BitmapUtil
package com.example.demo2;


import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;

import java.io.FileOutputStream;
import java.io.InputStream;

public class BitmapUtil {
    private final static String TAG = "BitmapUtil";

    // 把位图数据保存到指定路径的图片文件
    public static void saveImage(String path, Bitmap bitmap) {
        // 根据指定的文件路径构建文件输出流对象
        try (FileOutputStream fos = new FileOutputStream(path)) {
            // 把位图数据压缩到文件输出流中
            bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 获得旋转角度之后的位图对象
    public static Bitmap getRotateBitmap(Bitmap bitmap, float rotateDegree) {
        Matrix matrix = new Matrix(); // 创建操作图片用的矩阵对象
        matrix.postRotate(rotateDegree); // 执行图片的旋转动作
        // 创建并返回旋转后的位图对象
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }

    // 获得比例缩放之后的位图对象
    public static Bitmap getScaleBitmap(Bitmap bitmap, double scaleRatio) {
        Matrix matrix = new Matrix(); // 创建操作图片用的矩阵对象
        matrix.postScale((float) scaleRatio, (float) scaleRatio);
        // 创建并返回缩放后的位图对象
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                bitmap.getHeight(), matrix, false);
    }

    // 获得自动缩小后的位图对象
    public static Bitmap getAutoZoomImage(Context ctx, Uri uri) {
        Log.d(TAG, "getAutoZoomImage uri=" + uri.toString());
        Bitmap zoomBitmap = null;
        // 打开指定uri获得输入流对象
        try (InputStream is = ctx.getContentResolver().openInputStream(uri)) {
            // 从输入流解码得到原始的位图对象
            Bitmap originBitmap = BitmapFactory.decodeStream(is);
            int ratio = originBitmap.getWidth() / 2000 + 1;
            // 获得比例缩放之后的位图对象
            zoomBitmap = getScaleBitmap(originBitmap, 1.0 / ratio);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return zoomBitmap;
    }

    // 通知相册来了张新图片
    public static void notifyPhotoAlbum(Context ctx, String filePath) {
        try {
            String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
            MediaStore.Images.Media.insertImage(ctx.getContentResolver(),
                    filePath, fileName, null);
            Uri uri = Uri.parse("file://" + filePath);
            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri);
            ctx.sendBroadcast(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
DateUtil
package com.example.demo2;


import android.annotation.SuppressLint;

import java.text.SimpleDateFormat;
import java.util.Date;

@SuppressLint("SimpleDateFormat")
public class DateUtil {
    // 获取当前的日期时间
    public static String getNowDateTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        return sdf.format(new Date());
    }

    // 获取当前的时间
    public static String getNowTime() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        return sdf.format(new Date());
    }

    // 获取当前的分钟
    public static String getNowMinute() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
        return sdf.format(new Date());
    }

    // 获取当前的时间(精确到毫秒)
    public static String getNowTimeDetail() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
        return sdf.format(new Date());
    }

    // 将长整型的时间数值格式化为日期时间字符串
    public static String formatDate(long time) {
        Date date = new Date(time);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

}
PathPosition
package com.example.demo2;


import android.graphics.PointF;

// 定义一个路径位置实体类,包括上个落点的横纵坐标,以及下个落点的横纵坐标
public class PathPosition {
    public PointF prePos; // 上个落点的横纵坐标
    public PointF nextPos; // 下个落点的横纵坐标
}
activity_main
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

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

                <Button
                    android:id="@+id/btn_begin_signature"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="3"
                    android:text="开始签名"
                    android:textColor="@color/black"
                    android:textSize="17sp" />

                <Button
                    android:id="@+id/btn_reset_signature"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="2"
                    android:text="重置"
                    android:textColor="@color/black"
                    android:textSize="17sp" />

                <Button
                    android:id="@+id/btn_revoke_signature"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="2"
                    android:text="回退"
                    android:textColor="@color/black"
                    android:textSize="17sp" />

                <Button
                    android:id="@+id/btn_end_signature"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="3"
                    android:text="结束签名"
                    android:textColor="@color/black"
                    android:textSize="17sp" />
            </LinearLayout>

            <com.example.demo2.SignatureView
                android:id="@+id/view_signature"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="@color/white"
                app:paint_color="#0000aa"
                app:stroke_width="5" />

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

                <Button
                    android:id="@+id/btn_save_signature"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="保存图片文件"
                    android:textColor="@color/black"
                    android:textSize="17sp" />
            </LinearLayout>

            <ImageView
                android:id="@+id/iv_signature_new"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="@color/white" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>
attrs.xml(values----> attrs)
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="SignatureView">
        <attr name="paint_color" format="color" />
        <attr name="stroke_width" format="integer" />
    </declare-styleable>

</resources>