BUILD · 2026-09-06 · the FrankenKeys log

Thirty press effects at full frame rate

How FrankenKeys draws thirty press effects, fifteen trails and ten shaders without dropping frames, and the one place it has to fall back.

2026-09-06 · by Corey Redwine · android keyboard key press animation effects

A keyboard is the one app that is not allowed to stutter. It sits on top of somebody else's app, it is drawn at every key press, and it runs in a process the system will kill the moment it gets greedy. So "thirty press effects" is only interesting if it comes with "and none of them cost a frame". This is how that was done, and where it was not possible.

The rules the engine lives by

The press-effect engine is one class, thirty styles, one table, one draw path. Its hot-path contract, written at the top of the file and enforced by review:

  • Zero allocation after construction. Effects are events in a fixed pool, stored as arrays of numbers rather than as objects, so firing one writes a few array slots and allocates nothing. The pool holds sixteen; at eight presses a second with a second effect stacked, that keeps a 900 millisecond style and its partner alive without recycling one still on screen.
  • Two paints of its own, a fill and a stroke, and never the view's shared glow paint, whose style is never restored by the code that borrows it.
  • Every fade keeps a floor. The alpha formula is floor + t * (peak - floor). A pure quadratic ease looked elegant on paper and shipped styles that were effectively invisible, because the tail went transparent before the eye caught it.
  • No shaders in the press effects. Everything is circles, lines, arcs and one reused path, so all thirty render identically on Android 10, the oldest version the keyboard runs on.

Each effect picks its layer: behind the keys, above the labels, or both, so a blast past the key edge is not clipped by its neighbours. That was the original bug that forced the rewrite: five ripples drawn inline in the per-key loop, where an effect on one key was overdrawn by the next key's fill.

The thirty

Ring pulse, Shockwave, Starburst, Echo, Square pulse, Sonar, Pond, Bloom, Lightning, Tesla, Static, Gravity well, Embers, Spark shower, Confetti, Ashfall, Orbit, Hex pulse, Crosshair, Scanline, Diamond, Comet, Lava, Flame, Ink drop, Neon flicker, Spotlight, Grid wave, Domino, Portal.

Styles one to five are the original five with their exact constants, so nobody who chose one before the rewrite saw it change. Domino is the one style that still touches the key loop, because its whole point is that neighbouring keys tint when you press one. Tesla and Lightning jump between the keys nearest your thumb and hold still for a few frames rather than boiling, because their jitter is re-rolled on a coarse clock rather than every frame.

Stack two at once, which is part of the Pro unlock. The Effect Forge, also Pro, lets you build a thirty-first from shape, motion, layer, size, duration, colour and fade, and it renders through the same primitives as the catalogue, so a custom effect costs nothing new.

Fifteen trails

The swipe trail is the most-watched animation in the app, because it is under your thumb during every swipe. Fifteen styles: Comet, Neon, Ribbon, Rainbow, Beads, Lightning, Ember, Ink, Laser, Smoke, Glitch, Sparkle, Dashes, Bubbles, Fire. Same contract: two paints, one scratch array, one path, no strings. Every style tapers toward the tail so it reads as motion rather than as a drawn line, and the trail only ever runs while a swipe is in flight. Nothing here animates on its own.

Ten shaders, and the one fallback

Background shaders are the exception to "identical everywhere". RGB wave, Glass, Sunset, Aurora, Matrix, Plasma, Nebula, Starfield, Lava lamp and Circuit are real GPU programs, written in AGSL, which Android added in version 13. On Android 13 and later they run on the GPU with every paint and shader object created once at resize and only uniforms updated per frame. On Android 10 to 12 the same theme draws a static gradient instead and reports itself as non-animated, so the view never spins an invalidate loop for it.

That is the honest version of "ten shaders": ten on Android 13 and up, ten still gradients below it.

N/I

Check the fallback yourself. Pick any shader theme on an Android 12 phone: the background is a still gradient. On Android 13 or later it moves. The version check is Build.VERSION.SDK_INT >= 33 in studio.forgedev.forgeboard.theme.ShaderFx, readable in the shipped build. How to open it.

What "full frame rate on a mid-range phone" actually means

I have no telemetry, so I cannot tell you a frame-time distribution across the installed base. What I can tell you is the design: the draw path allocates nothing, so the garbage collector has nothing to pause for; effects are bounded by a fixed pool, so a burst of typing cannot grow the work; and the expensive tier, the shaders, is confined to hardware that has a GPU path for it. Whether that holds on your phone is something you can see by turning on Tesla and typing fast. If it drops frames, the address is at the bottom of the page and I want to know the model.

The tuning knobs

Every shader is tunable through the Shader Forge, a Pro tool: speed, density, a hue rotation, a direction so Matrix rain can fall sideways, an accent mix toward the theme's colour, and brightness. All defaults reproduce the built-in look exactly, so a theme that never opened the Forge renders as it did. The tune encodes to integers only, so it survives the share-code grammar and every locale's decimal mark, and travels with a scene.

The numbers, and where they are

  • 30 press effects: PressFx.STYLES. Pool size 16: the default cap.
  • 15 trails: TrailFx.STYLES.
  • 10 shaders: ForgeTheme.ShaderFxKind, every entry but NONE. Android 13 gate: ShaderFx.
  • The alpha-floor rule and the "no AGSL in press effects" rule: the class comment at the top of PressFx.

Every rule above was learned from a bug that shipped. The comments say which. That is what the comments are for.