I make ReadFocus+, a small Android app that floats a translucent strip (a "reading ruler") over the screen, mainly for people with dyslexia or ADHD who lose their place in long text. It has to work on top of any app: ebooks, PDFs, browsers, news apps. So it is built on a SYSTEM_ALERT_WINDOW overlay. These are the parts that took the most time.
Layout: one strip, two masks, one control panel
-
The strip: one window,
MATCH_PARENTwide with adjustable height.TYPE_APPLICATION_OVERLAY(TYPE_PHONEbelow API 26),PixelFormat.TRANSLUCENT. -
Line highlight mode: a View inside the strip painted with a
GradientDrawablein the theme colour; the View's alpha sets the strength. No customonDraw. -
Reading mask mode: two separate windows, a solid View pinned to
Gravity.TOPand one toGravity.BOTTOM, with the gap between them as the reading window. When you drag the strip, both masks follow. - Control panel: another window holding the mode switch and the lock button.
Letting touches through to the app underneath
(The code below is simplified for the article.)
A reading ruler is only useful if you can keep scrolling the app under it.
int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
if (locked) flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; // locked = touches pass through
- The default is locked (touch pass-through). To move the strip you unlock it from the panel, then drag the strip itself with
ACTION_DOWN/MOVE/UPandupdateViewLayoutonparams.y. - When toggling
FLAG_NOT_TOUCHABLE, I remove the window and add it again rather than only updating the flags. -
yis clamped between the status bar and the navigation bar, usingWindowInsets(statusBars, navigationBars, systemGestures, displayCutout).
Android 12: the opacity cap on pass-through windows
From Android 12 (API 31), an overlay that lets touches pass through to another app has a maximum opacity. Go above it and the system blocks the touches instead (the "untrusted touches" protection).
float maxAlpha = 1f;
if (Build.VERSION.SDK_INT >= 31) {
InputManager im = getSystemService(InputManager.class);
maxAlpha = (im != null) ? im.getMaximumObscuringOpacityForTouch() : 0.8f;
}
params.alpha = maxAlpha;
So the window alpha is pinned to that ceiling, and the visible strength comes from the inner View's alpha and the theme colour. This is also why the mask can never be fully black.
Display cutouts
On phones with a notch, the top mask must not leave a gap above it:
- API 30+:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - API 28–29:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
On rotation, onConfigurationChanged re-clamps the strip and redraws the masks. Every window also has FLAG_KEEP_SCREEN_ON, so the screen doesn't go dark mid-page.
Staying alive and coming back after process death
- A foreground service with
foregroundServiceType="specialUse"(FOREGROUND_SERVICE_SPECIAL_USE) and a low-importance, ongoing notification. - Besides
START_STICKY, the overlay is restored on app launch, on process restart, and by a WorkManager watchdog every 15 minutes. - Users can revoke the overlay permission at any time, and adding a window after that throws
BadTokenException, soSettings.canDrawOverlays()is checked before everyaddView.
The permission flow
ACTION_MANAGE_OVERLAY_PERMISSION with a package: URI → retry without it → fall back to ACTION_APPLICATION_DETAILS_SETTINGS. The guide screen checks canDrawOverlays() in onResume and returns as soon as it's true.
Wrapping up
Most of the work was in small details that let the overlay sit on top without getting in the way. If you're building an overlay of your own, I hope this saves you some time. The app is here (there's a Windows version too):
Top comments (0)