制作一个最简易的画板(自定义view绘制+媒体库适配保存内容)

制作一个最简易的画板

分析需求:既然是最简易的,那么只要实现最基本的功能就可以了

  • 画画(这个一定要的)
  • 橡皮擦
  • 能保存图片
  • 撤销和复原

画画

所谓画画,不过是记录下手指移动的痕迹而已

那么刚好在View的onTouchEvent方法中,可以实时跟随手指的移动坐标

 override fun onTouchEvent(event: MotionEvent?): Boolean {
    
    
        event?.let {
    
    
            val x = it.x
            val y = it.y
            when(it.action){
    
    
                MotionEvent.ACTION_DOWN->{
    
    
                    path.moveTo(x, y)
                    preX = x
                    preY = y
                    return falsezz
                }
                MotionEvent.ACTION_MOVE->{
    
    
                    //在这里实时刷新
                    path.quadTo(preX, preY, x, y)
//                    path.lineTo(x,y)
                    mBufferCanvas.drawPath(path, paint)
                    invalidate()
                    preX = x
                    preY = y
                }
                MotionEvent.ACTION_UP->{
    
    
                //在这里保存路径
                    val drawPath = DrawPath()
                    val oldPath = Path(path)
                    val oldPaint = Paint(paint)
                    drawPath.path = oldPath
                    drawPath.paint = oldPaint
                    undoStack.push(drawPath) //入栈
                    cancelStack.clear() //清空下取消撤回栈的缓存
                    path.reset() //清除路径内容
                }
            }
        }
        return true
  }
    

这里需要用到一个Path对象,来保存我们每次绘制的路径

可以只用一个path对象储存,但是这样会导致后续的撤销功能不好做,于是将每次dowm-move-up事件,都保存在一个新的path对象中,然后将该path绘制到一个bitmap里面,在draw方法中,只要调用drawBitmap也能实现实时绘制功能

    override fun onDraw(canvas: Canvas){
    
    
        super.onDraw(canvas)
        //直接绘制位图
        canvas.drawBitmap(mBufferBitmap, 0f,0f,null)
//        canvas.drawPath(path, paint) //这样会导致不好撤销
    }

橡皮擦

提到橡皮擦,就不得不提一下Android里面绘制的图形混合模式

这里只要用到clear这一种模式-清除模式,在我们需要使用橡皮擦功能时,只需要将混合模式改为Clear即可

    /**
     * 设置画笔模式
     */
    fun setModel(model:Long){
    
    
        mMode = model
        when(model){
    
    
            EDIT_MODE_PEN -> {
    
    
                paint.xfermode = null //空就是普通画笔
            }
            EDIT_MODE_ERASER ->{
    
    
                paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)  //橡皮擦模式
            }
        }
    }

撤销与复原

撤销与复原,很适合用stack来实现,当我们写完一笔时,将保存该路径的path入栈,在点击撤销时,我们只需要对stack执行出栈操作,然后用复原栈来接收出来的path即可。最后将栈内剩下的所有path绘制即可。

    /***
     * 撤销功能
     * 预期实现,使用一个path的stack去保存每次绘制的一个路径
     * 在使用撤销后,就移除前面的path。并且可以维护一个反撤销的一个栈
     */
    fun undo(){
    
    
        if(!undoStack.empty()){
    
    
            cancelStack.push(undoStack.pop())
            clear()
            for (pa in undoStack){
    
    
                mBufferCanvas.drawPath(pa.path, pa.paint)
            }
            invalidate()
        }   }
    /***
     * 取消撤回
     * 每次撤回操作,都会在反撤回栈入栈一个drawPath
     * 当需要取消撤回时,就将栈中出栈一个drawPath
     */
    fun cancelUndo(){
    
    
        if (!cancelStack.empty()){
    
    
            undoStack.push(cancelStack.pop())
            for (pa in undoStack){
    
    
                mBufferCanvas.drawPath(pa.path, pa.paint)
            }
            invalidate()
        }
    }

有点需要注意的是,我们在保存path时,也需要保存当时使用的paint信息,因此需要一个类来对双方都进行保存

/***
 * 一个保存绘制路径的类
 * 主要是保存绘制路径以及所采用的paint
 */
public class DrawPath {
    
    
    private Path path;
    private Paint paint;
    
    public Path getPath() {
    
    
        return path;
    }
    public void setPath(Path path) {
    
    
        this.path = path;
    }
    public Paint getPaint() {
    
    
        return paint;
    }
    public void setPaint(Paint paint) {
    
    
        this.paint = paint;
    }
}

保存图片

图片保存方面和很多适配相关

首先在Android sdk23(6.0.1)版本之后,想要对读写文件都需要动态进行权限获取,不能仅仅在Manifest里面声明

然后是在Android 29 (10, Q)之后,文件操作要用媒体库来实现了,不能直接对路径文件进行操作

先看看权限申请相关
    /***
     * 动态获取权限
     */
    private fun requestPermissions() {
    
    
        if (ActivityCompat.checkSelfPermission(
                this,
                android.Manifest.permission.WRITE_EXTERNAL_STORAGE
            )
            != PackageManager.PERMISSION_GRANTED  //检测是否有权限,无则申请,有则执行需要权限的操作
            
        ) {
    
    
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ), REQUEST_STATE_CODE
            ) //调用权限申请方法
        } else {
    
    
            mBitmap?.let {
    
     insertImages(it) }
        }
    }

在申请后,我们要在onRequestPermissionsResult方法中,获得申请结果

   /***
     * 权限申请回调
     */
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
    
    
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
    
    
            REQUEST_STATE_CODE -> {
    
    
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    
    
                    if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q){
    
    
                        mBitmap?.let {
    
     insertImageQ(it) }
                    }else{
    
    
                        mBitmap?.let {
    
     insertImages(it) }
                    }
                } else {
    
    
                    Toast.makeText(this, "权限授予失败,请重试", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
再看看文件保存相关

首先,在保存图片前,要知道我们怎么将view上绘制出来的东西,去变成图片去保存。

我们只需要获得一个bitmap对象,就能将bitmap位图进行保存,要获得bitmap对象,利用view的draw方法,将view的所有内容画在我们新建的白板画布上,那么这个白板画布就会变成我们想要的bitmap对象了

    /***
     * 获取bitmap对象
     */
    private fun getBitmap(view: View): Bitmap {
    
    
        //创建白板画布
        val bitmap: Bitmap = Bitmap.createBitmap(
            view.measuredWidth, view.measuredHeight,
            Bitmap.Config.ARGB_8888
        )
        val canvas: Canvas = Canvas(bitmap)
        canvas.drawColor(Color.WHITE)
        view.draw(canvas)
        return bitmap
    }

在Android10之前,我们可以通过File类来直接对文件系统进行修改,因此只需要将图片保存在某个路径当中,然后通过广播通知系统去刷新图库即可。也可以采用简易版本的图片插入,不过貌似是一个被弃用的方法

    /***
     * 直接用mediaStore的insertImage插入到picture目录
     */
    private fun insertImages(bitmap: Bitmap){
    
    
        val resolver = contentResolver
        MediaStore.Images.Media.insertImage(resolver, bitmap, "YMD${
      
      System.currentTimeMillis()}.jpg", "op")
    }

在Android10之后,需要利用媒体库,插入媒体信息,获得uri,再通过uri打开输出流,在bitmap的compress方法中,传入输出流,将图片保存到系统的媒体库中

    /***
     * Android 10以上插入图片
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun insertImageQ(bitmap: Bitmap){
    
    
        val fileName: String = "YMD${
      
      System.currentTimeMillis()}.jpg"
        var outputStream: OutputStream?
        var imageUri: Uri?
        val contentValues = ContentValues().apply {
    
    
            put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName)
            put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpg")
            put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            // 设置独占锁:耗时操作,独占访问权限,完成操作需复位
            put(MediaStore.Video.Media.IS_PENDING, 1)
        }
        val contentResolver = App.instance.contentResolver
        contentResolver.also {
    
     resolver->
            imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            outputStream = imageUri?.let {
    
    
                resolver.openOutputStream(it)
      

猜你喜欢

转载自blog.csdn.net/weixin_43637780/article/details/126949600