Live boards
Live boards let you write with the stylus during a call while others watch and draw from a browser. A board is an Excalidraw scene running in a WebView. Folio filters the pen before the page sees it, so the board feels like a Folio notebook: the same threshold, pressure curve and sensitivity.
Status: proof of concept on the development branch (commit 9c50cd1), tested on the tablet emulator and the XPPen MNP1095.
For users
- Library → Live tab (after Books) lists boards as cards with a preview. + on that tab creates a board; the card’s ⌄ menu opens, renames or deletes it.
- Board header: back (saves and returns to the library), the board name (tap to rename; shows ● Live session during a session), ✋ Fingers (off by default: only the pen works, so a resting hand can’t zoom or tap), Pen on the board (pressure settings) and Share.
- Share dialog:
- Live session: Start live session opens Excalidraw’s own collaboration dialog (Start session). While a session runs, the dialog shows the link with Copy link, Send link… and Stop session….
- Join a session: paste an Excalidraw room link (
…#room=…, filled in from the clipboard when one is there) and Join. - Export: Image (PNG) of the board, or an Excalidraw file (
.excalidraw) that excalidraw.com and the Excalidraw apps open.
- Pen on the board: Sensitivity (default 0.75), Lightest and Strongest stroke, Smoothing, Reset to defaults, and Pen settings… (the app-wide threshold and pressure curve).
How it works
| File | Role |
|---|---|
LiveBoardActivity.kt |
Board screen: WebView setup, header, Share and pressure dialogs, loading and saving boards |
PressureFilteringWebView.kt |
Pen filter between Android and the page |
PenPressureMapper.kt |
Raw pressure → board pressure; penPressure() is shared with the notebook canvas |
LiveBoardStore.kt |
Boards on disk, plus LiveBoardPreview for library cards |
shared/.../home/HomeBoards.kt |
The Live tab (cards, rename, delete, new board dialog) |
DrawingProfiles.kt |
sensitivityCurve(), shared by brushes and the board |
Pen filter (PressureFilteringWebView)
- Threshold as hover. A stroke starts when pressure passes the app’s pen threshold and ends when it drops back to it. The MNP1095 keeps sending
MOVEwith near-zero pressure after the tip leaves the glass; without this the board kept drawing in the air. - Pressure. Every sample, including batched historical ones, gets
min + sensitivityCurve(penPressure(raw), sensitivity) × (max − min). Chromium hands it to the page asPointerEvent.pressurewithpointerType = "pen"; Excalidraw stores it per point (pressures), so collaborators see the same widths. - Single-pointer pen stream. The page only ever sees the pen, as one pointer, and every pen down gets its up or cancel. When a palm and the pen touched together, the pen’s lift-off used to be lost; Excalidraw then kept a “pressed” pointer and turned later strokes into pinch-zoom.
- Palm rejection. Fingers are dropped while the pen touches or hovers, for 0.7 s after, and always when Fingers is off. A finger gesture the page never saw start (or saw cancelled) is dropped as a whole.
- Smoothing (optional): exponential smoothing of the pen position.
Excalidraw specifics
- excalidraw.com keeps the scene in
localStorage:excalidraw(elements) andexcalidraw-state(appState). - Pressure only counts with variable width. Excalidraw stores
strokeOptions.variabilityper stroke; its default,"constant", ignores pen pressure. Folio setscurrentItemStrokeVariability = "variable"every time a board loads. - The Live collaboration button is
.collab-button; a session shows up as#room=<id>,<key>in the page URL, which the header polls once a second.
Saving and loading boards
files/liveboards/index.jsonlists boards; each has<id>.json(Excalidraw’selementsandappStatestrings) and<id>.jpg(preview).- Load: a document-start script (
WebViewCompat.addDocumentStartJavaScript, androidx.webkit) writes the board’s scene intolocalStoragebefore Excalidraw’s scripts run. AsessionStoragekey (folio-board) stops a reload of the same board from overwriting newer, unsaved changes. WebViews without document-start scripts get the same script after load, then one reload. - Save: every 15 s and on back,
localStorageis read withevaluateJavascriptand written atomically (temp file and rename). Back waits for the save, so the preview is taken while the board is still on screen. - Preview:
PixelCopyof the WebView’s area (API 26+).View.drawcan’t see Excalidraw’s GPU canvas and gives a blank image.
Pitfalls found while building it
| Symptom | Cause | Fix |
|---|---|---|
| Excalidraw blank, toolbar 0 px tall | AndroidView gives the WebView WRAP_CONTENT, so the page’s viewport collapses |
Set MATCH_PARENT layout params; load the URL after the first layout (doOnLayout) |
| Lines keep drawing after lifting the pen | MNP1095 sends MOVE with low pressure in the air |
End the stroke when pressure ≤ threshold |
| Pen zooms in and out instead of drawing | Lost pointer-up for the pen in mixed pen + palm events | Extract the pen as a single pointer; always send up or cancel |
| Toolbar tool or zoom changes by itself | Palm taps and two-finger touches reach the page | Fingers off by default; palm rejection window |
| Pressure “does nothing” | Excalidraw’s constant stroke variability |
Force variable on load |
| Widths barely change | Excalidraw’s width range is small (~3×) and pens rarely pass 0.6–0.7 raw | Board Sensitivity, same curve as notebook brushes |
Settings edited with run-as don’t apply |
SharedPreferences are cached in the running process | Restart the app, or change them in the app |
Testing
adb shell input stylus swipe …sends real stylus events (constant pressure) on the emulator;input tapis a finger and is ignored while Fingers is off.- WebView DevTools:
adb forward tcp:9333 localabstract:webview_devtools_remote_<pid>, thencurl localhost:9333/jsonfor the page’s WebSocket URL. Useful to readlocalStorageor what pointer events the page receives.
Limits and next steps
- Depends on excalidraw.com and its room server. Self-hosting Excalidraw (and
excalidraw-room) under our domain would remove the dependency and allow more line thinning than Excalidraw’s fixed width range. - Images inserted on a board live in the page’s IndexedDB and aren’t saved with the board yet.
- During a live session Excalidraw keeps the scene in the room; drawing done in a session may not reach Folio’s saved copy.
- A preview taken while a dialog is open shows the dialog.
- Live boards aren’t in
.foliobackups yet. - Privacy: boards load excalidraw.com, and a live session sends the drawing (end-to-end encrypted) through Excalidraw’s servers. The privacy policy covers this; the Play Data safety form needs a review before live boards ship to production.