Kevin Learn Android:Android 手签板

Posted Kevin_小飞象

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kevin Learn Android:Android 手签板相关的知识,希望对你有一定的参考价值。


前言
android 屏幕手写签名的原理就是把手机屏幕当作画板,把用户手指当作画笔,手指在屏幕上在屏幕上划来划去,屏幕就会显示手指的移动轨迹,就像画笔在画板上写字一样。实现手写签名需要结合绘图的路径工具 Path ,在有按下动作时调用 Path 对象的 moveTo 方法,将路径起始点移动到触摸点;在有移动操作时调用 Path 对象的 quadTo 方法,将记录本次触摸点与上次触摸点之间的路径;在有移动操作与提起动作时调用 Canvas 对象的 drawPath 方法,将本次触摸绘制在画布上。

效果图

 

功能


- 空白画板手写
- 实现笔锋效果
- 支持橡皮擦,撤回/恢复,清空画布功能
- 支持画笔颜色大小设置
- 支持传入初始图片
- 支持画布大小设置,文字区域裁剪
- 主题颜色设置
- 支持传入初始显示图片## 代码
1. 布局文件
 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/rl_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:clipToPadding="true"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <include
        android:id="@+id/actionbar"
        layout="@layout/sign_actionbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/sign_actionbar_height"
        android:layout_alignParentTop="true" />

    <com.hkt.handwritten.view.PaintView
        android:id="@+id/paint_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/divider"
        android:layout_below="@id/actionbar"
        android:layout_margin="12dp"
        android:background="@drawable/shape_dash_bg"/>


    <View
        android:id="@+id/divider"
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_above="@+id/setting"
        android:background="@color/sign_border_gray" />

    <LinearLayout
        android:id="@+id/setting"
        android:layout_width="match_parent"
        android:layout_height="@dimen/y105"
        android:layout_alignParentBottom="true"
        android:background="@drawable/bottom_bg_shape"
        android:gravity="center"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/btn_hand"
            android:layout_width="@dimen/x38"
            android:layout_height="@dimen/y38"
            android:layout_alignParentLeft="true"
            android:layout_marginRight="@dimen/x40"
            android:scaleType="centerCrop"
            android:layout_weight="1"
            android:background="@drawable/sign_bg_btn_clicked"
            android:src="@mipmap/sign_ic_hand"
            android:visibility="gone" />


        <ImageView
            android:id="@+id/btn_undo"
            android:layout_width="@dimen/x78"
            android:layout_height="@dimen/y78"
            android:scaleType="centerCrop"
            android:layout_marginRight="@dimen/x40"
            android:layout_toLeftOf="@+id/btn_redo"
            android:padding="@dimen/x12"
            android:background="@drawable/sign_bg_btn_clicked"
            android:src="@mipmap/sign_ic_undo" />

        <ImageView
            android:id="@+id/btn_redo"
            android:layout_width="@dimen/x78"
            android:layout_height="@dimen/y78"
            android:scaleType="centerCrop"
            android:layout_marginRight="@dimen/x40"
            android:layout_toLeftOf="@+id/btn_clear"
            android:background="@drawable/sign_bg_btn_clicked"
            android:padding="@dimen/x12"
            android:src="@mipmap/sign_ic_redo" />


        <ImageView
            android:id="@+id/btn_pen"
            android:layout_width="@dimen/x78"
            android:layout_height="@dimen/y78"
            android:scaleType="centerCrop"
            android:layout_marginRight="@dimen/x40"
            android:layout_toLeftOf="@+id/btn_setting"
            android:padding="@dimen/x12"
            android:background="@drawable/sign_bg_btn_clicked"
            android:src="@mipmap/sign_ic_pen" />

        <ImageView
            android:id="@+id/btn_clear"
            android:layout_width="@dimen/x78"
            android:layout_height="@dimen/y78"
            android:layout_marginRight="@dimen/x40"
            android:layout_toLeftOf="@+id/btn_pen"
            android:padding="@dimen/x12"
            android:scaleType="centerCrop"
            android:background="@drawable/sign_bg_btn_clicked"
            android:src="@mipmap/sign_ic_clear" />

        <com.hkt.handwritten.view.CircleView
            android:id="@+id/btn_setting"
            android:layout_width="@dimen/x78"
            android:layout_height="@dimen/y78"
            android:layout_alignParentRight="true"
            android:padding="@dimen/x12"
            app:showOutBorder="false"
            app:sizeLevel="2" />
    </LinearLayout>
</RelativeLayout>

2. 核心代码 PaintView.java
 

/**
 * Created on 2021/12/1 11:58
 *
 * @author Gong Youqiang
 */
public class PaintView extends View 
    public static final int TYPE_PEN = 0;
    public static final int TYPE_ERASER = 1;

    private Paint mPaint;
    private Canvas mCanvas;
    private Bitmap mBitmap;
    private int strokeWidth;
    private BasePen mStokeBrushPen;

    /**
     * 是否允许写字
     */
    private boolean isFingerEnable = true;
    /**
     * 是否橡皮擦模式
     */
    private boolean isEraser = false;

    /**
     * 是否有绘制
     */
    private boolean hasDraw = false;


    /**
     * 画笔轨迹记录
     */
    private StepOperator mStepOperation;

    private StepCallback mCallback;

    /**
     * 是否可以撤销
     */
    private boolean mCanUndo;
    /**
     * 是否可以恢复
     */
    private boolean mCanRedo;

    private int mWidth;
    private int mHeight;

    private boolean isDrawing = false;//是否正在绘制
    private int toolType = 0;  //记录手写笔类型:触控笔/手指

    private Eraser eraser;

    public PaintView(Context context) 
        this(context, null);
    

    public PaintView(Context context, AttributeSet attrs) 
        this(context, attrs, 0);
    

    public PaintView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);

    

    /**
     * 初始化画板
     *
     * @param width  画板宽度
     * @param height 画板高度
     * @param path   初始图片路径
     */
    public void init(int width, int height, String path) 
        this.mWidth = width;
        this.mHeight = height;

        mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_4444);
        mStokeBrushPen = new SteelPen();

        initPaint();
        initCanvas();

        mStepOperation = new StepOperator();
        if (!TextUtils.isEmpty(path)) 
            Bitmap bitmap = BitmapFactory.decodeFile(path);
            resize(bitmap, mWidth, mHeight);
         else 
            mStepOperation.addBitmap(mBitmap);
        
        //橡皮擦
        eraser = new Eraser(getResources().getDimensionPixelSize(R.dimen.sign_eraser_size));
    

    /**
     * 初始画笔设置
     */
    private void initPaint() 
        strokeWidth = DisplayUtil.dip2px(getContext(), PaintSettingWindow.PEN_SIZES[PenConfig.PAINT_SIZE_LEVEL]);
        mPaint = new Paint();
        mPaint.setColor(PenConfig.PAINT_COLOR);
        mPaint.setStrokeWidth(strokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAlpha(0xFF);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeMiter(1.0f);
        mStokeBrushPen.setPaint(mPaint);
    

    private void initCanvas() 
        mCanvas = new Canvas(mBitmap);
        //设置画布的背景色为透明
        mCanvas.drawColor(Color.TRANSPARENT);
    


    @Override
    protected void onDraw(Canvas canvas) 
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        if (!isEraser) 
            mStokeBrushPen.draw(canvas);
        
        super.onDraw(canvas);
    

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) 
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.dispatchTouchEvent(ev);
    


    @Override
    public boolean onTouchEvent(MotionEvent event) 

        toolType = event.getToolType(event.getActionIndex());
        if (!isFingerEnable && toolType != MotionEvent.TOOL_TYPE_STYLUS) 
            return false;
        
        if (isEraser) 
            eraser.handleEraserEvent(event, mCanvas);
         else 
            mStokeBrushPen.onTouchEvent(event, mCanvas);
        

        switch (event.getActionMasked()) 
            case MotionEvent.ACTION_DOWN:
                isDrawing = false;
                break;
            case MotionEvent.ACTION_MOVE:
                hasDraw = true;
                mCanUndo = true;
                isDrawing = true;
                break;
            case MotionEvent.ACTION_CANCEL:
                isDrawing = false;
                break;
            case MotionEvent.ACTION_UP:
                if (mStepOperation != null && isDrawing) 
                    mStepOperation.addBitmap(mBitmap);
                
                mCanUndo = !mStepOperation.currentIsFirst();
                mCanRedo = !mStepOperation.currentIsLast();
                if (mCallback != null) 
                    mCallback.onOperateStatusChanged();
                
                isDrawing = false;
                break;
            default:
                break;
        
        invalidate();
        return true;
    

    /**
     * @return 判断是否有绘制内容在画布上
     */
    public boolean isEmpty() 
        return !hasDraw;
    

    /**
     * 撤销
     */
    public void undo() 

        if (mStepOperation == null || !mCanUndo) 
            return;
        
        if (!mStepOperation.currentIsFirst()) 
            mCanUndo = true;
            mStepOperation.undo(mBitmap);
            hasDraw = true;
            invalidate();

            if (mStepOperation.currentIsFirst()) 
                mCanUndo = false;
                hasDraw = false;
            
         else 
            mCanUndo = false;
            hasDraw = false;
        
        if (!mStepOperation.currentIsLast()) 
            mCanRedo = true;
        
        if (mCallback != null) 
            mCallback.onOperateStatusChanged();
        
    

    /**
     * 恢复
     */
    public void redo() 
        if (mStepOperation == null || !mCanRedo) 
            return;
        
        if (!mStepOperation.currentIsLast()) 
            mCanRedo = true;
            mStepOperation.redo(mBitmap);
            hasDraw = true;
            invalidate();
            if (mStepOperation.currentIsLast()) 
                mCanRedo = false;
            
         else 
            mCanRedo = false;
        
        if (!mStepOperation.currentIsFirst()) 
            mCanUndo = true;
        
        if (mCallback != null) 
            mCallback.onOperateStatusChanged();
        
    

    /**
     * 清除画布,记得清除点的集合
     */
    public void reset() 
        mBitmap.eraseColor(Color.TRANSPARENT);
        hasDraw = false;
        mStokeBrushPen.clear();
        if (mStepOperation != null) 
            mStepOperation.reset();
            mStepOperation.addBitmap(mBitmap);
        
        mCanRedo = false;
        mCanUndo = false;
        if (mCallback != null) 
            mCallback.onOperateStatusChanged();
        
        invalidate();
    


    public void release() 
        destroyDrawingCache();
        if (mBitmap != null) 
            mBitmap.recycle();
            mBitmap = null;
        
        if (mStepOperation != null) 
            mStepOperation.freeBitmaps();
            mStepOperation = null;
        
    

    public interface StepCallback 
        /**
         * 操作变更
         */
        void onOperateStatusChanged();
    

    public void setStepCallback(StepCallback callback) 
        this.mCallback = callback;
    

    /**
     * 设置画笔样式
     *
     * @param penType
     */
    public void setPenType(int penType) 
        isEraser = false;
        switch (penType) 
            case TYPE_PEN:
                mStokeBrushPen = new SteelPen();
                break;
            case TYPE_ERASER:
                isEraser = true;
                break;
        
        //设置
        if (mStokeBrushPen.isNullPaint()) 
            mStokeBrushPen.setPaint(mPaint);
        
        invalidate();
    

    /**
     * 设置画笔大小
     *
     * @param width 大小
     */
    public void setPaintWidth(int width) 
        if (mPaint != null) 
            mPaint.setStrokeWidth(DisplayUtil.dip2px(getContext(), width));
//            eraser.setPaintWidth(DisplayUtil.dip2px(getContext(), width));
            mStokeBrushPen.setPaint(mPaint);
            invalidate();
        
    


    /**
     * 设置画笔颜色
     *
     * @param color 颜色
     */
    public void setPaintColor(int color) 
        if (mPaint != null) 
            mPaint.setColor(color);
            mStokeBrushPen.setPaint(mPaint);
            invalidate();
        
    

    /**
     * 构建Bitmap
     *
     * @return 所绘制的bitmap
     */
    public Bitmap buildAreaBitmap(boolean isCrop) 
        if (!hasDraw) 
            return null;
        
        Bitmap result;
        if (isCrop) 
            result = BitmapUtil.clearBlank(mBitmap, 50, Color.TRANSPARENT);
         else 
            result = mBitmap;
        
        destroyDrawingCache();
        return result;
    

    public boolean isFingerEnable() 
        return isFingerEnable;
    

    public void setFingerEnable(boolean fingerEnable) 
        isFingerEnable = fingerEnable;
    

    public boolean isEraser() 
        return isEraser;
    

    public boolean canUndo() 
        return mCanUndo;
    

    public boolean canRedo() 
        return mCanRedo;
    

    public Bitmap getBitmap() 
        return mBitmap;
    

    /**
     * 图片大小调整适配画布宽高
     *
     * @param bitmap 源图
     * @param width  新宽度
     * @param height 新高度
     */
    public void resize(Bitmap bitmap, int width, int height) 

        if (mBitmap != null) 
            if (width >= this.mWidth) 
                height = width * mBitmap.getHeight() / mBitmap.getWidth();
            
            this.mWidth = width;
            this.mHeight = height;

            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
            restoreLastBitmap(bitmap, mBitmap);
            initCanvas();
            if (mStepOperation != null) 
                mStepOperation.addBitmap(mBitmap);
            
            invalidate();
        
    

    /**
     * 恢复最后画的bitmap
     *
     * @param srcBitmap 最后的bitmap
     * @param newBitmap 新bitmap
     */
    private void restoreLastBitmap(Bitmap srcBitmap, Bitmap newBitmap) 
        try 
            if (srcBitmap == null || srcBitmap.isRecycled()) 
                return;
            
            srcBitmap = BitmapUtil.zoomImg(srcBitmap, newBitmap.getWidth());
            //缩放后如果还是超出新图宽高,继续缩放
            if (srcBitmap.getWidth() > newBitmap.getWidth() || srcBitmap.getHeight() > newBitmap.getHeight()) 
                srcBitmap = BitmapUtil.zoomImage(srcBitmap, newBitmap.getWidth(), newBitmap.getHeight());
            
            //保存所有的像素的数组,图片宽×高
            int[] pixels = new int[srcBitmap.getWidth() * srcBitmap.getHeight()];
            srcBitmap.getPixels(pixels, 0, srcBitmap.getWidth(), 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight());
            newBitmap.setPixels(pixels, 0, srcBitmap.getWidth(), 0, 0,
                    srcBitmap.getWidth(), srcBitmap.getHeight());
         catch (OutOfMemoryError e) 
        

    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        int width = onMeasureR(0, widthMeasureSpec);
        int height = onMeasureR(1, heightMeasureSpec);
        setMeasuredDimension(width, height);
    

    /**
     * 计算控件宽高
     */
    public int onMeasureR(int attr, int oldMeasure) 

        int newSize = 0;
        int mode = MeasureSpec.getMode(oldMeasure);
        int oldSize = MeasureSpec.getSize(oldMeasure);

        switch (mode) 
            case MeasureSpec.EXACTLY:
                newSize = oldSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:

                float value = mWidth;

                if (attr == 0) 
                    if (mBitmap != null) 
                        value = mBitmap.getWidth();
                    
                    // 控件的宽度
                    newSize = (int) (getPaddingLeft() + value + getPaddingRight());

                 else if (attr == 1) 
                    value = mHeight;
                    if (mBitmap != null) 
                        value = mBitmap.getHeight();
                    
                    // 控件的高度
                    newSize = (int) (getPaddingTop() + value + getPaddingBottom());
                
                break;
            default:
                break;
        
        return newSize;
    

    public Bitmap getLastBitmap() 
        return mBitmap;
    

3. 核心代码 SteelPen.java
 

/**
 * Created on 2021/12/1 11:38
 * 钢笔
 * @author Gong Youqiang
 */
public class SteelPen extends BasePen 
    @Override
    protected void doPreDraw(Canvas canvas) 
        for (int i = 1; i < mHWPointList.size(); i++) 
            ControllerPoint point = mHWPointList.get(i);
            drawToPoint(canvas, point, mPaint);
            mCurPoint = point;
        
    

    @Override
    protected void doMove(double curDis) 
        int steps = 1 + (int) curDis / STEP_FACTOR;
        double step = 1.0 / steps;
        for (double t = 0; t < 1.0; t += step) 
            ControllerPoint point = mBezier.getPoint(t);
            mHWPointList.add(point);
        
    

    @Override
    protected void doDraw(Canvas canvas, ControllerPoint point, Paint paint) 
        drawLine(canvas, mCurPoint.x, mCurPoint.y, mCurPoint.width, point.x,
                point.y, point.width, paint);
    

    /**
     * 绘制方法,实现笔锋效果
     */
    private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint) 
        //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
        double curDis = Math.hypot(x0 - x1, y0 - y1);
        int steps;
        //绘制的笔的宽度是多少,绘制多少个椭圆
        if (paint.getStrokeWidth() < 6) 
            steps = 1 + (int) (curDis / 2);
         else if (paint.getStrokeWidth() > 60) 
            steps = 1 + (int) (curDis / 4);
         else 
            steps = 1 + (int) (curDis / 3);
        
        double deltaX = (x1 - x0) / steps;
        double deltaY = (y1 - y0) / steps;
        double deltaW = (w1 - w0) / steps;
        double x = x0;
        double y = y0;
        double w = w0;

        for (int i = 0; i < steps; i++) 
            RectF oval = new RectF();
            float top = (float) (y - w / 2.0f);
            float left = (float) (x - w / 4.0f);
            float right = (float) (x + w / 4.0f);
            float bottom = (float) (y + w / 2.0f);
            oval.set(left, top, right, bottom);
            //最基本的实现,通过点控制线,绘制椭圆
            canvas.drawOval(oval, paint);
            x += deltaX;
            y += deltaY;
            w += deltaW;
        
    

4. 使用

public class HandWrittenBoardActivity extends BaseActivity implements View.OnClickListener, PaintView.StepCallback 
    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private ImageView mHandView; //切换 滚动/手写
    private ImageView mUndoView;
    private ImageView mRedoView;
    private ImageView mPenView;
    private ImageView mClearView;
    private CircleView mSettingView;

    private PaintView mPaintView;

    private ProgressDialog mSaveProgressDlg;
    private static final int MSG_SAVE_SUCCESS = 1;
    private static final int MSG_SAVE_FAILED = 2;

    private String mSavePath;
    private boolean hasSize = false;

    private float mWidth;
    private float mHeight;
    private float widthRate = 1.0f;
    private float heightRate = 1.0f;
    private int bgColor;
    private boolean isCrop;
    private String format;

    private PaintSettingWindow settingWindow;

    private static String[] PERMISSIONS_STORAGE = 
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE" ;

    @Override
    protected int getLayout() 
        return R.layout.activity_hand_written_board;
    

    @Override
    protected void initView() 
        verifyStoragePermissions(this);
        View mCancelView = findViewById(R.id.tv_cancel);
        View mOkView = findViewById(R.id.tv_ok);

        mPaintView = findViewById(R.id.paint_view);
        mHandView = findViewById(R.id.btn_hand);
        mUndoView = findViewById(R.id.btn_undo);
        mRedoView = findViewById(R.id.btn_redo);
        mPenView = findViewById(R.id.btn_pen);
        mClearView = findViewById(R.id.btn_clear);
        mSettingView = findViewById(R.id.btn_setting);
        mUndoView.setOnClickListener(this);
        mRedoView.setOnClickListener(this);
        mPenView.setOnClickListener(this);
        mClearView.setOnClickListener(this);
        mSettingView.setOnClickListener(this);
        mHandView.setOnClickListener(this);
        mCancelView.setOnClickListener(this);
        mOkView.setOnClickListener(this);

        mPenView.setSelected(true);
        mUndoView.setEnabled(false);
        mRedoView.setEnabled(false);
        mClearView.setEnabled(!mPaintView.isEmpty());

        mPaintView.setStepCallback(this);

        PenConfig.PAINT_SIZE_LEVEL = PenConfig.getPaintTextLevel(this);
        PenConfig.PAINT_COLOR = PenConfig.getPaintColor(this);

        mSettingView.setPaintColor(PenConfig.PAINT_COLOR);
        mSettingView.setRadiusLevel(PenConfig.PAINT_SIZE_LEVEL);

        setThemeColor(PenConfig.THEME_COLOR);
        BitmapUtil.setImage(mClearView, R.mipmap.sign_ic_clear, Color.WHITE);
        BitmapUtil.setImage(mPenView, R.mipmap.sign_ic_pen, Color.WHITE);
        BitmapUtil.setImage(mRedoView, R.mipmap.sign_ic_redo, mPaintView.canRedo() ? Color.WHITE : Color.LTGRAY);
        BitmapUtil.setImage(mUndoView, R.mipmap.sign_ic_undo, mPaintView.canUndo() ? Color.WHITE : Color.LTGRAY);
        BitmapUtil.setImage(mClearView, R.mipmap.sign_ic_clear, !mPaintView.isEmpty() ? Color.WHITE : Color.LTGRAY);

    


    /**
     * 获取画布默认宽度
     *
     * @return
     */
    private int getResizeWidth() 
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE && dm.widthPixels < dm.heightPixels) 
            return (int) (dm.heightPixels * widthRate);
        
        return (int) (dm.widthPixels * widthRate);
    

    /**
     * 获取画布默认高度
     *
     * @return
     */
    private int getResizeHeight() 
        int toolBarHeight = getResources().getDimensionPixelSize(R.dimen.sign_grid_toolbar_height);
        int actionbarHeight = getResources().getDimensionPixelSize(R.dimen.sign_actionbar_height);
        int statusBarHeight = StatusBarCompat.getStatusBarHeight(this);
        int otherHeight = toolBarHeight + actionbarHeight + statusBarHeight;
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE && dm.widthPixels < dm.heightPixels) 
            return (int) ((dm.widthPixels - otherHeight) * heightRate);
        
        return (int) ((dm.heightPixels - otherHeight) * heightRate);
    

    @Override
    protected void initData() 
        isCrop = getIntent().getBooleanExtra("crop", false);
        format = getIntent().getStringExtra("format");
        bgColor = getIntent().getIntExtra("background", Color.TRANSPARENT);
        String mInitPath = getIntent().getStringExtra("image");
        float bitmapWidth = getIntent().getFloatExtra("width", 1.0f);
        float bitmapHeight = getIntent().getFloatExtra("height", 1.0f);

        if (bitmapWidth > 0 && bitmapWidth <= 1.0f) 
            widthRate = bitmapWidth;
            mWidth = getResizeWidth();
         else 
            hasSize = true;
            mWidth = bitmapWidth;
        
        if (bitmapHeight > 0 && bitmapHeight <= 1.0f) 
            heightRate = bitmapHeight;
            mHeight = getResizeHeight();
         else 
            hasSize = true;
            mHeight = bitmapHeight;
        

        //初始画板设置
        if (!hasSize && !TextUtils.isEmpty(mInitPath)) 
            Bitmap bitmap = BitmapFactory.decodeFile(mInitPath);
            mWidth = bitmap.getWidth();
            mHeight = bitmap.getHeight();
            hasSize = true;
        
        mPaintView.init((int) mWidth, (int) mHeight, mInitPath);
        if (bgColor != Color.TRANSPARENT) 
            mPaintView.setBackgroundColor(bgColor);
        
    

    /**
     * 横竖屏切换
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) 
        super.onConfigurationChanged(newConfig);
        if (settingWindow != null) 
            settingWindow.dismiss();
        

        int resizeWidth = getResizeWidth();
        int resizeHeight = getResizeHeight();
        if (mPaintView != null && !hasSize) 
            mPaintView.resize(mPaintView.getLastBitmap(), resizeWidth, resizeHeight);
        
    

    public static void verifyStoragePermissions(Activity activity) 
        try 
            //检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) 
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);
            
         catch (Exception e) 
            e.printStackTrace();
        
    


    @Override
    public void onClick(View v) 
        int i = v.getId();
        if (i == R.id.btn_setting) 
            showPaintSettingWindow();

         else if (i == R.id.btn_hand) 
            //切换是否允许写字
            mPaintView.setFingerEnable(!mPaintView.isFingerEnable());
            if (mPaintView.isFingerEnable()) 
                BitmapUtil.setImage(mHandView, R.mipmap.sign_ic_hand, PenConfig.THEME_COLOR);
             else 
                BitmapUtil.setImage(mHandView, R.mipmap.sign_ic_drag, PenConfig.THEME_COLOR);
            

         else if (i == R.id.btn_clear) 
            mPaintView.reset();

         else if (i == R.id.btn_undo) 
            mPaintView.undo();

         else if (i == R.id.btn_redo) 
            mPaintView.redo();

         else if (i == R.id.btn_pen) 
            if (!mPaintView.isEraser()) 
                mPaintView.setPenType(PaintView.TYPE_ERASER);
                BitmapUtil.setImage(mPenView, R.mipmap.sign_ic_eraser, Color.WHITE);
             else 
                mPaintView.setPenType(PaintView.TYPE_PEN);
                BitmapUtil.setImage(mPenView, R.mipmap.sign_ic_pen, Color.WHITE);
            
         else if (i == R.id.tv_ok) 
            save();

         else if (i == R.id.tv_cancel) 
            if (!mPaintView.isEmpty()) 
                showQuitTip();
             else 
                setResult(RESULT_CANCELED);
                finish();
            
        
    

    @Override
    protected void onDestroy() 
        if (mPaintView != null) 
            mPaintView.release();
        
        if (mHandler != null) 
            mHandler.removeCallbacksAndMessages(null);
        
        super.onDestroy();
    


    /**
     * 弹出画笔设置
     */
    private void showPaintSettingWindow() 
        settingWindow = new PaintSettingWindow(this);
        settingWindow.setSettingListener(new PaintSettingWindow.OnSettingListener() 
            @Override
            public void onColorSetting(int color) 
                mPaintView.setPaintColor(color);
                mSettingView.setPaintColor(color);
            

            @Override
            public void onSizeSetting(int index) 
                mSettingView.setRadiusLevel(index);
                mPaintView.setPaintWidth(PaintSettingWindow.PEN_SIZES[index]);
            
        );

        View contentView = settingWindow.getContentView();
        //需要先测量,PopupWindow还未弹出时,宽高为0
        contentView.measure(SystemUtil.makeDropDownMeasureSpec(settingWindow.getWidth()),
                SystemUtil.makeDropDownMeasureSpec(settingWindow.getHeight()));

        int padding = DisplayUtil.dip2px(this, 45);
        settingWindow.popAtTopRight();
        settingWindow.showAsDropDown(mSettingView, -540, -2 * padding - settingWindow.getContentView().getMeasuredHeight());

    


    private void initSaveProgressDlg() 
        mSaveProgressDlg = new ProgressDialog(this);
        mSaveProgressDlg.setMessage("正在保存,请稍候...");
        mSaveProgressDlg.setCancelable(false);
    

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() 
        @Override
        public void handleMessage(Message msg) 
            switch (msg.what) 
                case MSG_SAVE_FAILED:
                    mSaveProgressDlg.dismiss();
                    Toast.makeText(getApplicationContext(), "保存失败", Toast.LENGTH_SHORT).show();
                    break;
                case MSG_SAVE_SUCCESS:
                    mSaveProgressDlg.dismiss();
                    Intent intent = new Intent();
                    intent.putExtra(PenConfig.SAVE_PATH, mSavePath);
                    setResult(RESULT_OK, intent);
                    break;
                default:
                    break;
            
        
    ;

    /**
     * 保存
     */
    private void save() 
        if (mPaintView.isEmpty()) 
            Toast.makeText(getApplicationContext(), "没有写入任何文字", Toast.LENGTH_SHORT).show();
            return;
        
        //先检查是否有存储权限
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) 
            Toast.makeText(getApplicationContext(), "没有读写存储的权限", Toast.LENGTH_SHORT).show();
            return;
        
        if (mSaveProgressDlg == null) 
            initSaveProgressDlg();
        
        mSaveProgressDlg.show();
        new Thread(() -> 
            try 
                Bitmap result = mPaintView.buildAreaBitmap(isCrop);
                if (PenConfig.FORMAT_JPG.equals(format) && bgColor == Color.TRANSPARENT) 
                    bgColor = Color.WHITE;
                
                if (bgColor != Color.TRANSPARENT) 
                    result = BitmapUtil.drawBgToBitmap(result, bgColor);
                
                if (result == null) 
                    mHandler.obtainMessage(MSG_SAVE_FAILED).sendToTarget();
                    return;
                
                mSavePath = BitmapUtil.saveImage(HandWrittenBoardActivity.this, result, 100, format);
                if (mSavePath != null) 
                    mHandler.obtainMessage(MSG_SAVE_SUCCESS).sendToTarget();
                 else 
                    mHandler.obtainMessage(MSG_SAVE_FAILED).sendToTarget();
                
             catch (Exception e) 

            
        ).start();

    

    /**
     * 画布有操作
     */
    @Override
    public void onOperateStatusChanged() 
        mUndoView.setEnabled(mPaintView.canUndo());
        mRedoView.setEnabled(mPaintView.canRedo());
        mClearView.setEnabled(!mPaintView.isEmpty());

        BitmapUtil.setImage(mRedoView, R.mipmap.sign_ic_redo, mPaintView.canRedo() ? Color.WHITE : Color.LTGRAY);
        BitmapUtil.setImage(mUndoView, R.mipmap.sign_ic_undo, mPaintView.canUndo() ? Color.WHITE : Color.LTGRAY);
        BitmapUtil.setImage(mClearView, R.mipmap.sign_ic_clear, !mPaintView.isEmpty() ? Color.WHITE : Color.LTGRAY);

    

    @Override
    public void onBackPressed() 
        if (!mPaintView.isEmpty()) 
            showQuitTip();
         else 
            setResult(RESULT_CANCELED);
            finish();
        
    

    /**
     * 弹出退出提示
     */
    private void showQuitTip() 
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示")
                .setMessage("当前文字未保存,是否退出?")
                .setNegativeButton("取消", null)
                .setPositiveButton("确定", (dialog, which) -> 
                    setResult(RESULT_CANCELED);
                    finish();
                );
        builder.show();
    


[Demo](https://download.csdn.net/download/duoduo_11011/63793446)

以上是关于Kevin Learn Android:Android 手签板的主要内容,如果未能解决你的问题,请参考以下文章

Kevin Learn Android:Android 手签板

Kevin Learn Android:Android 手签板

Kevin Learn Android--> Android Studio 小技巧

Kevin Learn Android-->NFC 技术解析

Kevin Learn Kotlin-->Kotlin 学习资料

Kevin Learn Kotlin:循环控制