Android 位图:缩放时像素显示为矩形(可能是位图伪像)

Android Bitmap: Pixels appear to be rectangular when zooming (possible Bitmap artifacts)

我正在为 Android 创建一个像素艺术编辑器,为此,我使用了带有位图的 Canvas。

这是我的一些代码 (MyCanvasView) 的一部分,它处理大部分像素艺术功能:

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' 然后被加载到位于 CanvasActivity 内的片段中。)

像素可以简单地使用 Bitmap.setPixel 方法设置,如果您没有从该代码中看到的话。并且你可能已经看到像素点之间通过直线算法连接在一起,给用户一种无限硬件输入速率的错觉。

我似乎遇到了一个很奇怪的问题(这是最严重的错误,因为我的应用程序还有许多其他错误以及这个错误)。

假设我创建了一个跨度数约为 100 的 canvas,并使用铅笔工具绘制了一些东西:

正如您从图片中看到的那样 - 放大时像素呈现良好,它们看起来是一个完美的正方形。

现在,假设我创建了一个跨度数约为 670 的 canvas(请注意,这是一种罕见的边缘情况,但一切仍需要正常运行 - 即使对于较大的位图也是如此)并绘制一些东西:

从外面看还算不错,但放大后:

..像素点呈矩形,整体看起来很奇怪

对于超过 1500x1500 的 canvas 尺寸(是的,我知道,这是一种非常罕见的边缘情况),伪像更加明显,每个像素之间甚至出现空格:

我已经和一些有像素艺术编辑经验的人谈过,他们无法告诉我为什么会发生这种情况 - 但他们认为这与 getResizedBitmap 方法有关 -虽然我不完全确定这是不是真的。

这个问题本身并不严重 - 因为它是一个移动编辑器,大多数用户不会使用 canvas 670x670 大小,但我认为它仍然值得修复。大多数事情我都是按照书做的,所以我很困惑为什么会出现这些工件。


如果有人知道这个问题的主要原因是什么或者能以任何方式帮助我,我将不胜感激!

为什么要调整位图的大小?为什么每次调用 onDraw 时都创建一个新位图?

每次调整位图大小时,都会应用放大或缩小算法。这可能是您看到的失真的原因。

您应该在开始时创建一个位图,并在 onDraw 方法中渲染它。您在 getResizedBitmap 中计算的矩阵应该用作 this function

的参数