簡単なスケッチパッドを作成する
解析要件:最もシンプルなので、最も基本的な機能が実現できれば十分
- 絵を描く(これは必須です)
- 消しゴム
- 写真を保存できる
- 元に戻すとやり直す
描く
いわゆる絵を描くということは、指の動きの軌跡を記録することにほかなりません。
その後、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 オブジェクトを使用して、毎回描画するパスを保存する必要があります。
保存するパス オブジェクトは 1 つだけ使用できますが、これにより後続の元に戻す機能が難しくなるため、各 dowm-move-up イベントを新しいパス オブジェクトに保存し、draw メソッドでパスを bitmap に描画します。 、drawBitmapを呼び出すだけでリアルタイム描画機能も実現可能
override fun onDraw(canvas: Canvas){
super.onDraw(canvas)
//直接绘制位图
canvas.drawBitmap(mBufferBitmap, 0f,0f,null)
// canvas.drawPath(path, paint) //这样会导致不好撤销
}
消しゴム
消しゴムに関して言えば、Android で描画されるグラフィックス ブレンド モードについて言及する必要があります。
ここではクリアモードのみを使用する必要があります。クリアモードを使用する必要がある場合は、消しゴム機能を使用する必要がある場合は、ブレンドモードをクリアに変更するだけです。
/**
* 设置画笔模式
*/
fun setModel(model:Long){
mMode = model
when(model){
EDIT_MODE_PEN -> {
paint.xfermode = null //空就是普通画笔
}
EDIT_MODE_ERASER ->{
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) //橡皮擦模式
}
}
}
元に戻すとやり直し
元に戻すと復元はスタックで実現するのに非常に適しています。ストロークを書き終えたら、パスのパスを保存してスタックに置きます。元に戻すをクリックすると、スタック上でスタック操作を実行するだけです。 、復元スタックを使用してそれを受け取ります。最後に、スタック内の残りのパスをすべて描画します。
/***
* 撤销功能
* 预期实现,使用一个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()
}
}
注意すべき点は、パスを保存するときに、そのときに使用されたペイント情報も保存する必要があるため、両側を保存するクラスが必要であることです。
/***
* 一个保存绘制路径的类
* 主要是保存绘制路径以及所采用的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) バージョン以降、ファイルを読み書きしたい場合は、動的に権限を取得する必要があり、マニフェストで宣言するだけでは済みません。
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()
}
}
}
}
ファイルの保存を見てください
まず、画像を保存する前に、ビューに描画されたものを画像に変換して保存する方法を知る必要があります。
ビットマップ ビットマップを保存するために必要なのは、ビットマップ オブジェクトを取得することだけです。ビットマップ オブジェクトを取得するには、ビューのdrawメソッドを使用して、新しく作成したホワイトボード キャンバスにビューのすべてのコンテンツを描画します。そうすると、ホワイトボード キャンバスがビットマップになります。私たちが欲しいオブジェクトは
/***
* 获取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
}
Android 10 より前では、File クラスを通じてファイル システムを直接変更できるため、画像を特定のパスに保存し、ブロードキャスト通知システムを通じてギャラリーを更新するだけで済みます。画像の簡易バージョンを使用して挿入することもできますが、これは非推奨の方法のようです
/***
* 直接用mediaStore的insertImage插入到picture目录
*/
private fun insertImages(bitmap: Bitmap){
val resolver = contentResolver
MediaStore.Images.Media.insertImage(resolver, bitmap, "YMD${
System.currentTimeMillis()}.jpg", "op")
}
Android 10 以降では、メディア ライブラリを使用し、メディア情報を挿入し、URI を取得し、その URI を通じて出力ストリームを開く必要があります。ビットマップの圧縮メソッドで、出力ストリームを渡し、画像をシステムのファイルに保存します。メディアライブラリ。
/***
* 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)