Navigate to editor on notebook creation, reduce flicker with double buffering

- Creating a notebook now auto-navigates to its first page editor
  (createNotebook returns ID via callback, triggers onNotebookClick)
- Double-buffered rendering: all layers composited to an off-screen
  bitmap before presenting to the display in a single drawBitmap call.
  Eliminates visible intermediate states (white flash between layers)
  that caused flicker on e-ink displays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:59:56 -07:00
parent 86a2ba0f6b
commit 73f35cd33d
3 changed files with 37 additions and 18 deletions

View File

@@ -48,6 +48,11 @@ class PadCanvasView(context: Context) : View(context) {
private var gridCanvas: Canvas? = null private var gridCanvas: Canvas? = null
private var gridDirty = true private var gridDirty = true
// --- Compositing bitmap (eliminates flicker by drawing all layers
// off-screen before presenting to the display in one operation) ---
private var compositeBitmap: android.graphics.Bitmap? = null
private var compositeCanvas: Canvas? = null
// --- 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
@@ -269,11 +274,24 @@ class PadCanvasView(context: Context) : View(context) {
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
// Fill entire view with white (no super.onDraw to avoid double-clear flicker) val w = width
canvas.drawColor(Color.WHITE) val h = height
if (w <= 0 || h <= 0) return
// Draw page background in canonical space // Ensure compositing bitmap exists at view size
canvas.withMatrix(viewMatrix) { if (compositeBitmap == null || compositeBitmap!!.width != w || compositeBitmap!!.height != h) {
compositeBitmap?.recycle()
compositeBitmap = androidx.core.graphics.createBitmap(w, h)
compositeCanvas = Canvas(compositeBitmap!!)
}
val c = compositeCanvas!!
// Compose all layers off-screen
c.drawColor(Color.WHITE)
// Page background
c.withMatrix(viewMatrix) {
drawRect( drawRect(
0f, 0f, 0f, 0f,
canvasState.pageSize.widthPt.toFloat(), canvasState.pageSize.widthPt.toFloat(),
@@ -282,24 +300,22 @@ class PadCanvasView(context: Context) : View(context) {
) )
} }
// Draw cached grid // Cached grid
ensureGrid() ensureGrid()
gridBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } gridBitmap?.let { c.drawBitmap(it, 0f, 0f, null) }
// Draw completed strokes from backing bitmap (screen resolution) // Completed strokes
ensureBacking() ensureBacking()
backingBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } backingBitmap?.let { c.drawBitmap(it, 0f, 0f, null) }
// Draw dynamic elements in canonical space (in-progress stroke, previews, selection) // Dynamic elements
canvas.withMatrix(viewMatrix) { c.withMatrix(viewMatrix) {
// In-progress stroke
currentPath?.let { path -> currentPath?.let { path ->
currentPaint?.let { paint -> currentPaint?.let { paint ->
drawPath(path, paint) drawPath(path, paint)
} }
} }
// Box preview
if (isDrawingBox) { if (isDrawingBox) {
boxPreviewPaint.strokeWidth = canvasState.penWidthPt boxPreviewPaint.strokeWidth = canvasState.penWidthPt
val left = minOf(boxStartX, boxEndX) val left = minOf(boxStartX, boxEndX)
@@ -309,7 +325,6 @@ class PadCanvasView(context: Context) : View(context) {
drawRect(left, top, right, bottom, boxPreviewPaint) drawRect(left, top, right, bottom, boxPreviewPaint)
} }
// Line preview
if (isDrawingLine) { if (isDrawingLine) {
val paint = if (canvasState.lineStyle == LineStyle.DASHED) { val paint = if (canvasState.lineStyle == LineStyle.DASHED) {
dashedLinePaint.also { it.strokeWidth = canvasState.penWidthPt } dashedLinePaint.also { it.strokeWidth = canvasState.penWidthPt }
@@ -323,7 +338,6 @@ class PadCanvasView(context: Context) : View(context) {
) )
} }
// Selection highlights
if (selectedStrokeIds.isNotEmpty()) { if (selectedStrokeIds.isNotEmpty()) {
for (sr in completedStrokes) { for (sr in completedStrokes) {
if (sr.id in selectedStrokeIds) { if (sr.id in selectedStrokeIds) {
@@ -334,7 +348,6 @@ class PadCanvasView(context: Context) : View(context) {
} }
} }
// Selection rectangle
if (isSelecting) { if (isSelecting) {
val left = minOf(selectionStartX, selectionEndX) val left = minOf(selectionStartX, selectionEndX)
val top = minOf(selectionStartY, selectionEndY) val top = minOf(selectionStartY, selectionEndY)
@@ -343,6 +356,9 @@ class PadCanvasView(context: Context) : View(context) {
drawRect(left, top, right, bottom, selectionRectPaint) drawRect(left, top, right, bottom, selectionRectPaint)
} }
} }
// Present the fully composed frame in one operation
canvas.drawBitmap(compositeBitmap!!, 0f, 0f, null)
} }
/** /**

View File

@@ -243,7 +243,9 @@ fun NotebookListScreen(
CreateNotebookDialog( CreateNotebookDialog(
onDismiss = { showCreateDialog = false }, onDismiss = { showCreateDialog = false },
onCreate = { title, pageSize -> onCreate = { title, pageSize ->
viewModel.createNotebook(title, pageSize) viewModel.createNotebook(title, pageSize) { notebookId ->
onNotebookClick(notebookId)
}
showCreateDialog = false showCreateDialog = false
}, },
) )

View File

@@ -17,9 +17,10 @@ class NotebookListViewModel(
val notebooks: StateFlow<List<Notebook>> = repository.getAll() val notebooks: StateFlow<List<Notebook>> = repository.getAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun createNotebook(title: String, pageSize: String) { fun createNotebook(title: String, pageSize: String, onCreated: (Long) -> Unit = {}) {
viewModelScope.launch { viewModelScope.launch {
repository.create(title, pageSize) val id = repository.create(title, pageSize)
onCreated(id)
} }
} }