制作一个最简易的画板
分析需求:既然是最简易的,那么只要实现最基本的功能就可以了
画画
所谓画画,不过是记录下手指移动的痕迹而已
那么刚好在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)
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)
}
橡皮擦
提到橡皮擦,就不得不提一下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绘制即可。
fun undo(){
if(!undoStack.empty()){
cancelStack.push(undoStack.pop())
clear()
for (pa in undoStack){
mBufferCanvas.drawPath(pa.path, pa.paint)
}
invalidate()
} }
fun cancelUndo(){
if (!cancelStack.empty()){
undoStack.push(cancelStack.pop())
for (pa in undoStack){
mBufferCanvas.drawPath(pa.path, pa.paint)
}
invalidate()
}
}
有点需要注意的是,我们在保存path时,也需要保存当时使用的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对象了
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类来直接对文件系统进行修改,因此只需要将图片保存在某个路径当中,然后通过广播通知系统去刷新图库即可。也可以采用简易版本的图片插入,不过貌似是一个被弃用的方法
private fun insertImages(bitmap: Bitmap){
val resolver = contentResolver
MediaStore.Images.Media.insertImage(resolver, bitmap, "YMD${System.currentTimeMillis()}.jpg", "op")
}
在Android10之后,需要利用媒体库,插入媒体信息,获得uri,再通过uri打开输出流,在bitmap的compress方法中,传入输出流,将图片保存到系统的媒体库中
@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)
|