制作一个最简易的画板
分析需求:既然是最简易的,那么只要实现最基本的功能就可以了
- 画画(这个一定要的)
- 橡皮擦
- 能保存图片
- 撤销和复原
画画
所谓画画,不过是记录下手指移动的痕迹而已
那么刚好在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)