Fix drawing flicker with screen-resolution backing bitmap
Completed strokes are now rendered to a backing bitmap at the view's pixel resolution. The bitmap is only redrawn when strokes change or zoom/pan changes. During active drawing, onDraw just composites the cached bitmap + the in-progress stroke, eliminating flicker from re-rendering all paths every frame. Unlike the earlier 1/4-resolution bitmap that caused blur, this one uses full screen resolution with the view matrix applied, so strokes remain pixel-perfect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,11 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
// --- Strokes ---
|
// --- Strokes ---
|
||||||
private val completedStrokes = mutableListOf<StrokeRender>()
|
private val completedStrokes = mutableListOf<StrokeRender>()
|
||||||
|
|
||||||
|
// --- Backing bitmap for completed strokes (screen resolution) ---
|
||||||
|
private var backingBitmap: android.graphics.Bitmap? = null
|
||||||
|
private var backingCanvas: Canvas? = null
|
||||||
|
private var bitmapDirty = true
|
||||||
|
|
||||||
// --- In-progress stroke ---
|
// --- In-progress stroke ---
|
||||||
private var currentPath: Path? = null
|
private var currentPath: Path? = null
|
||||||
private var currentPaint: Paint? = null
|
private var currentPaint: Paint? = null
|
||||||
@@ -203,6 +208,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
val paint = buildPaint(stroke.penSize, stroke.color, stroke.style)
|
val paint = buildPaint(stroke.penSize, stroke.color, stroke.style)
|
||||||
completedStrokes.add(StrokeRender(path, paint, stroke.id, stroke.style, points))
|
completedStrokes.add(StrokeRender(path, paint, stroke.id, stroke.style, points))
|
||||||
}
|
}
|
||||||
|
bitmapDirty = true
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +216,13 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
val path = buildPathFromPoints(points)
|
val path = buildPathFromPoints(points)
|
||||||
val paint = buildPaint(penSize, color, style)
|
val paint = buildPaint(penSize, color, style)
|
||||||
completedStrokes.add(StrokeRender(path, paint, id, style, points))
|
completedStrokes.add(StrokeRender(path, paint, id, style, points))
|
||||||
|
bitmapDirty = true
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeStroke(id: Long) {
|
fun removeStroke(id: Long) {
|
||||||
completedStrokes.removeAll { it.id == id }
|
completedStrokes.removeAll { it.id == id }
|
||||||
|
bitmapDirty = true
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +231,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
super.onSizeChanged(w, h, oldw, oldh)
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
rebuildViewMatrix()
|
rebuildViewMatrix()
|
||||||
|
bitmapDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
@@ -241,26 +250,12 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
// Draw grid in screen space with pixel-snapped positions
|
// Draw grid in screen space with pixel-snapped positions
|
||||||
drawGridScreenSpace(canvas)
|
drawGridScreenSpace(canvas)
|
||||||
|
|
||||||
// Draw everything else in canonical space
|
// Draw completed strokes from backing bitmap (screen resolution)
|
||||||
canvas.withMatrix(viewMatrix) {
|
ensureBacking()
|
||||||
// Completed strokes
|
backingBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
|
||||||
for (sr in completedStrokes) {
|
|
||||||
drawPath(sr.path, sr.paint)
|
|
||||||
// Draw arrow heads for arrow-style strokes
|
|
||||||
val pts = sr.points
|
|
||||||
if (pts != null && pts.size >= 4) {
|
|
||||||
val x1 = pts[0]
|
|
||||||
val y1 = pts[1]
|
|
||||||
val x2 = pts[2]
|
|
||||||
val y2 = pts[3]
|
|
||||||
if (sr.style == Stroke.STYLE_ARROW || sr.style == Stroke.STYLE_DOUBLE_ARROW) {
|
|
||||||
drawArrowHeads(this, x1, y1, x2, y2,
|
|
||||||
if (sr.style == Stroke.STYLE_ARROW) LineStyle.ARROW else LineStyle.DOUBLE_ARROW,
|
|
||||||
sr.paint.strokeWidth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Draw dynamic elements in canonical space (in-progress stroke, previews, selection)
|
||||||
|
canvas.withMatrix(viewMatrix) {
|
||||||
// In-progress stroke
|
// In-progress stroke
|
||||||
currentPath?.let { path ->
|
currentPath?.let { path ->
|
||||||
currentPaint?.let { paint ->
|
currentPaint?.let { paint ->
|
||||||
@@ -319,6 +314,42 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
* artifacts that cause uneven grid spacing when drawn in canonical space
|
* artifacts that cause uneven grid spacing when drawn in canonical space
|
||||||
* and scaled by the view matrix.
|
* and scaled by the view matrix.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Ensure the backing bitmap is up-to-date. The bitmap is at view pixel
|
||||||
|
* resolution with the viewMatrix applied, so completed strokes are
|
||||||
|
* pixel-perfect. Only redrawn when strokes change or zoom/pan changes.
|
||||||
|
*/
|
||||||
|
private fun ensureBacking() {
|
||||||
|
val w = width
|
||||||
|
val h = height
|
||||||
|
if (w <= 0 || h <= 0) return
|
||||||
|
|
||||||
|
if (backingBitmap == null || backingBitmap!!.width != w || backingBitmap!!.height != h) {
|
||||||
|
backingBitmap?.recycle()
|
||||||
|
backingBitmap = androidx.core.graphics.createBitmap(w, h)
|
||||||
|
backingCanvas = Canvas(backingBitmap!!)
|
||||||
|
bitmapDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapDirty) {
|
||||||
|
backingBitmap!!.eraseColor(android.graphics.Color.TRANSPARENT)
|
||||||
|
val c = backingCanvas!!
|
||||||
|
c.setMatrix(viewMatrix)
|
||||||
|
for (sr in completedStrokes) {
|
||||||
|
c.drawPath(sr.path, sr.paint)
|
||||||
|
val pts = sr.points
|
||||||
|
if (pts != null && pts.size >= 4) {
|
||||||
|
if (sr.style == Stroke.STYLE_ARROW || sr.style == Stroke.STYLE_DOUBLE_ARROW) {
|
||||||
|
drawArrowHeads(c, pts[0], pts[1], pts[2], pts[3],
|
||||||
|
if (sr.style == Stroke.STYLE_ARROW) LineStyle.ARROW else LineStyle.DOUBLE_ARROW,
|
||||||
|
sr.paint.strokeWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bitmapDirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun drawGridScreenSpace(canvas: Canvas) {
|
private fun drawGridScreenSpace(canvas: Canvas) {
|
||||||
val pageW = canvasState.pageSize.widthPt.toFloat()
|
val pageW = canvasState.pageSize.widthPt.toFloat()
|
||||||
val pageH = canvasState.pageSize.heightPt.toFloat()
|
val pageH = canvasState.pageSize.heightPt.toFloat()
|
||||||
@@ -600,6 +631,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
}
|
}
|
||||||
moveLastX = pt[0]
|
moveLastX = pt[0]
|
||||||
moveLastY = pt[1]
|
moveLastY = pt[1]
|
||||||
|
bitmapDirty = true
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -870,6 +902,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
sr.path.offset(dx, dy)
|
sr.path.offset(dx, dy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bitmapDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
@@ -970,6 +1003,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
viewMatrix.postTranslate(panX, panY)
|
viewMatrix.postTranslate(panX, panY)
|
||||||
|
|
||||||
viewMatrix.invert(inverseMatrix)
|
viewMatrix.invert(inverseMatrix)
|
||||||
|
bitmapDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun screenToCanonical(screenX: Float, screenY: Float): FloatArray {
|
private fun screenToCanonical(screenX: Float, screenY: Float): FloatArray {
|
||||||
|
|||||||
Reference in New Issue
Block a user