I am creating a pixel art editor for Android, and to do this, I am using a Canvas with a Bitmap.
Here is an excerpt of some of my code (MyCanvasView
) which handles the majority of pixel art functionality:
package com.realtomjoney.pyxlmoose.customviews.mycanvasview
import android.content.Context
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.realtomjoney.pyxlmoose.listeners.CanvasFragmentListener
import com.realtomjoney.pyxlmoose.models.BitmapAction
import com.realtomjoney.pyxlmoose.models.XYPosition
import android.graphics.*
import com.realtomjoney.pyxlmoose.activities.canvas.canvasInstance
import com.realtomjoney.pyxlmoose.models.BitmapActionData
class MyCanvasView (context: Context, private var spanCount: Int) : View(context) {
lateinit var extraCanvas: Canvas
lateinit var extraBitmap: Bitmap
private var scaleWidth = 0f
private var scaleHeight = 0f
var prevX: Int? = null
var prevY: Int? = null
val bitmapActionData: MutableList<BitmapAction> = mutableListOf()
var currentBitmapAction: BitmapAction? = null
var lifecycleOwner: LifecycleOwner? = null
private lateinit var caller: CanvasFragmentListener
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
caller = context as CanvasFragmentListener
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(spanCount, spanCount, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val coordinateX = (event.x / scaleWidth).toInt()
val coordinateY = (event.y / scaleWidth).toInt()
if (currentBitmapAction == null) {
currentBitmapAction = BitmapAction(mutableListOf())
}
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> {
if (coordinateX in 0 until spanCount && coordinateY in 0 until spanCount) {
caller.onPixelTapped(extraBitmap, XYPosition(coordinateX, coordinateY))
} else {
prevX = null
prevY = null
}
}
MotionEvent.ACTION_DOWN -> {
if (coordinateX in 0 until spanCount && coordinateY in 0 until spanCount) {
caller.onPixelTapped(extraBitmap, XYPosition(coordinateX, coordinateY))
} else {
prevX = null
prevY = null
}
}
MotionEvent.ACTION_UP -> {
caller.onActionUp()
}
}
invalidate()
return true
}
fun undo() {
if (bitmapActionData.size > 0) {
if (!bitmapActionData.last().isFilterBased) {
for ((key, value) in bitmapActionData.last().actionData.distinctBy { it.xyPosition }) {
extraBitmap.setPixel(key.x, key.y, value)
}
} else {
for ((key, value) in bitmapActionData.last().actionData) {
extraBitmap.setPixel(key.x, key.y, value)
}
}
invalidate()
bitmapActionData.removeLast()
}
}
fun clearCanvas() {
for (i_1 in 0 until extraBitmap.width) {
for (i_2 in 0 until extraBitmap.height) {
extraBitmap.setPixel(i_1, i_2, Color.TRANSPARENT)
}
}
invalidate()
bitmapActionData.clear()
}
private fun getResizedBitmap(bm: Bitmap, newHeight: Int, newWidth: Int): Bitmap? {
val width = bm.width
val height = bm.height
val scaleWidth = newWidth.toFloat() / width
val scaleHeight = newHeight.toFloat() / height
this.scaleWidth = scaleWidth
this.scaleHeight = scaleHeight
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false)
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(getResizedBitmap(extraBitmap, this.width, this.width)!!, 0f, 0f, null)
}
}
('MyCanvasView' is then loaded into a Fragment which resides inside CanvasActivity
.)
Pixels are set simply with the Bitmap.setPixel
method, if you didn't see from that code. And you might have already seen that pixels are connected to each other with a line algorithm to give the user the illusion of infinite hardware input rate.
I seem to be having quite a strange problem (this is the worst bug, as my app has numerous other bugs as well as this one).
Say I create a canvas with a span count of around 100 and draw some stuff with the pencil tool:
As you can see from the picture - the pixels render fine when you zoom in, and they appear to be a perfect square.
Now, suppose I create a canvas with a span count of around 670 (note that this is a rare edge case, but everything still needs to be functioning properly - even for larger Bitmaps) and draw some stuff:
It appears relatively fine from the outside, but once you zoom in:
..the pixels appear rectangular, and it overall looks very strange.
For canvas sizes of more than 1500x1500 (yes I know, it is a very rare edge case) the artifacts are even more visible, and spaces even appear between each pixel:
I've spoken to a couple of people who have experience with pixel art editors and they couldn't tell me why this is happening - but they assume it has something to do with the getResizedBitmap
method - although I'm not entirely sure whether that's true or not.
This problem isn't major per se - as it's a mobile editor most users won't be using canvas sizes of 670x670, but it's still worth fixing in my opinion. I'm doing most things by the book, so I'm confused why these artifacts are appearing.
What might be the main cause of this issue?
Why are you resizing the bitmap? Why are you creating a new bitmap every time onDraw
is called?
Every time you resize the bitmap, you cause a magnification or minification algorithm to be applied. That's probably the cause of the distortions you're seeing.
You should create one bitmap at start, and render that in the onDraw method. The matrix you're calculating in getResizedBitmap
should instead be used as an argument to this function