工作之余,自己想着利用空闲时间做一些小工具出来,今天分享的是一个简单的画板工具,支持轨迹绘制、更换笔迹颜色等功能,并且可以把成品保存到系统相册。支持Android 13

先看一下效果,吐槽一下csdn的视频上传,质量压缩的比较厉害,然后比例也发生变化了,反正是大家凑合看吧,文末会放源码(我的所有demo的源码都是不需要积分的)

Android画板小工具测试视频

我主要放一下关键代码吧

 1.自定义画板SignatureView 

public class SignatureView extends View {
    private Context context;
    private Paint paint;
    private Bitmap bitmap;
    private Canvas canvas;
    private Path path;

    public SignatureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(10);
        paint.setStyle(Paint.Style.STROKE);
        // 获取屏幕尺寸
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;
        bitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bitmap);
        canvas.drawColor(Color.WHITE);
        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.reset();
                path.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(x, y);
                canvas.drawPath(path, paint);
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                return false;
        }

        invalidate();
        return true;
    }

    public void setColor(int newColor) {
        paint.setColor(newColor);
    }

    public void clear() {
        canvas.drawColor(Color.WHITE);
        invalidate();
    }

    public Bitmap getSignatureBitmap() {
        return bitmap;
    }

    public int dpToPx(float dp) {
        float density = context.getResources().getDisplayMetrics().density;
        return Math.round(dp * density);
    }

}

2.布局文件

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

    <com.swy.signdemo.SignatureView
        android:id="@+id/signatureView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#ffffff" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#dcdcdc" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:layout_gravity="bottom|end"
        android:layout_margin="10dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="当前颜色:" />

        <FrameLayout
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_gravity="center_vertical"
            android:background="#000">

            <View
                android:id="@+id/view_color"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_gravity="center"
                android:background="@color/black" />

        </FrameLayout>

        <Button
            android:id="@+id/pickColor"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="20dp"
            android:text="更换颜色" />

        <Button
            android:id="@+id/clearButton"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="清空" />

        <Button
            android:id="@+id/saveButton"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:text="保存" />

    </LinearLayout>

</LinearLayout>

3.选择颜色的弹窗

public class PickColorWindow extends PopupWindow {

    private WindowPickColorBinding binding;
    private CommonAdapter<ColorData> commonAdapter;
    private List<ColorData> colors = new ArrayList<>();
    private ColorData colorDataSelected = null;

    public PickColorWindow(Activity context, ColorData color, PickColorCallBack callBack) {
        super(context);
        binding = WindowPickColorBinding.inflate(context.getLayoutInflater());
        setWidth(WindowManager.LayoutParams.MATCH_PARENT);
        setHeight(WindowManager.LayoutParams.MATCH_PARENT);
        setContentView(binding.getRoot());

        initColors();

        binding.viewColor.setBackgroundColor(Color.parseColor(color.getColorValue()));

        binding.btnCancel.setOnClickListener(v -> {
            dismiss();
        });

        binding.btnConfirm.setOnClickListener(v -> {
            callBack.onPick(colorDataSelected);
            dismiss();
        });

        binding.recycler.setLayoutManager(new GridLayoutManager(context, 4));
        commonAdapter = new CommonAdapter<ColorData>(context,
                R.layout.item_color, colors) {
            @Override
            public void convert(CommonViewHolder holder, ColorData bean, int position) {
                holder.setBackgroundColor(R.id.view_color, Color.parseColor(bean.getColorValue()));
                holder.setOnClickListener(R.id.view_color, v -> {
                    colorDataSelected = bean;
                    binding.viewColor.setBackgroundColor(Color.parseColor(colorDataSelected.getColorValue()));
                });
            }

            @Override
            public void footConvert(CommonViewHolder holder, int size) {
            }
        };
        binding.recycler.setAdapter(commonAdapter);
    }

    private void initColors() {
        colors.clear();
        colors.add(new ColorData("#000000"));
        colors.add(new ColorData("#e6194B"));
        colors.add(new ColorData("#3cb44b"));
        colors.add(new ColorData("#ffe119"));
        colors.add(new ColorData("#4363d8"));
        colors.add(new ColorData("#f58231"));
        colors.add(new ColorData("#42d4f4"));
        colors.add(new ColorData("#f032e6"));
        colors.add(new ColorData("#fabed4"));
        colors.add(new ColorData("#469990"));
        colors.add(new ColorData("#dcbeff"));
        colors.add(new ColorData("#9A6324"));
        colors.add(new ColorData("#fffac8"));
        colors.add(new ColorData("#800000"));
        colors.add(new ColorData("#aaffc3"));
        colors.add(new ColorData("#a9a9a9"));
    }

    public interface PickColorCallBack {
        void onPick(ColorData colorData);
    }
}

4.主界面

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private AlertDialog dialog;
    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    private boolean havePermission = false;
    private PickColorWindow pickColorWindow;
    private ColorData currentColor = new ColorData("#000000");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        Window window = getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setStatusBarColor(Color.TRANSPARENT); // 设置状态栏颜色为透明
        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
        setContentView(binding.getRoot());

        binding.pickColor.setOnClickListener(v -> {
            showColorPickerDialog();
        });

        binding.clearButton.setOnClickListener(v -> {
            binding.signatureView.clear();
        });

        binding.saveButton.setOnClickListener(v -> {
            if (havePermission) {
                saveBitmap(binding.signatureView.getSignatureBitmap());
            } else {
                checkPermission();
            }
        });
    }

    private void saveBitmap(Bitmap bitmap) {
        // 获取外部存储目录
        String folderName = Environment.DIRECTORY_PICTURES;
        File file = new File(Environment.getExternalStoragePublicDirectory(folderName), "signature.png");
        try {
            if (file.exists()) {
                file.delete();
            }
            // 创建目录(如果不存在)
            file.getParentFile().mkdirs();
            // 尝试创建文件
            if (file.createNewFile()) {
                OutputStream os = new FileOutputStream(file);
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); // 保存为PNG格式
                os.close();
                // 发送广播通知相册刷新
                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
                Toast.makeText(MainActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void showColorPickerDialog() {
        if (pickColorWindow != null) {
            pickColorWindow.dismiss();
            pickColorWindow = null;
        }
        pickColorWindow = new PickColorWindow(this, currentColor, (ColorData color) -> {
            currentColor = color;
            binding.signatureView.setColor(Color.parseColor(currentColor.getColorValue()));
            binding.viewColor.setBackgroundColor(Color.parseColor(currentColor.getColorValue()));
        });
        pickColorWindow.showAsDropDown(binding.getRoot());
    }

    private void checkPermission() {
        //检查权限(NEED_PERMISSION)是否被授权 PackageManager.PERMISSION_GRANTED表示同意授权
        if (Build.VERSION.SDK_INT >= 30) {
            if (!Environment.isExternalStorageManager()) {
                if (dialog != null) {
                    dialog.dismiss();
                    dialog = null;
                }
                dialog = new AlertDialog.Builder(this)
                        .setTitle("提示")//设置标题
                        .setMessage("请开启文件访问权限,否则无法正常使用本应用!")
                        .setNegativeButton("取消", (dialog, i) -> dialog.dismiss())
                        .setPositiveButton("确定", (dialog, which) -> {
                            dialog.dismiss();
                            Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                            startActivity(intent);
                        }).create();
                dialog.show();
            } else {
                havePermission = true;
                saveBitmap(binding.signatureView.getSignatureBitmap());
                Log.i("swyLog", "Android 11以上,当前已有权限");
            }
        } else {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                    //申请权限
                    if (dialog != null) {
                        dialog.dismiss();
                        dialog = null;
                    }
                    dialog = new AlertDialog.Builder(this)
                            .setTitle("提示")//设置标题
                            .setMessage("请开启文件访问权限,否则无法正常使用本应用!")
                            .setPositiveButton("确定", (dialog, which) -> {
                                dialog.dismiss();
                                ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
                            }).create();
                    dialog.show();
                } else {
                    havePermission = true;
                    saveBitmap(binding.signatureView.getSignatureBitmap());
                    Log.i("swyLog", "Android 6.0以上,11以下,当前已有权限");
                }
            } else {
                havePermission = true;
                saveBitmap(binding.signatureView.getSignatureBitmap());
                Log.i("swyLog", "Android 6.0以下,已获取权限");
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case REQUEST_EXTERNAL_STORAGE: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    havePermission = true;
                    saveBitmap(binding.signatureView.getSignatureBitmap());
                    Toast.makeText(this, "授权成功!", Toast.LENGTH_SHORT).show();
                } else {
                    havePermission = false;
                    Toast.makeText(this, "授权被拒绝!", Toast.LENGTH_SHORT).show();
                }
                return;
            }
        }
    }

}

这个demo的功能还是相对比较简单的,然后没有什么好讲的,只不过这个demo中有涉及到Android 的运行时权限申请,兼容Android13的,可以重点关注一下,其他的都是UI层的东西,基本上把代码复制过去,就可以用了,真的有什么问题了,评论区留言

demo源码

10-21 11:19