ディスレクシアやADHDの方向けに、画面上に半透明の帯(リーディングルーラー)を浮かべるAndroidアプリ「ReadFocus+」を個人で作っています。電子書籍・PDF・ブラウザなどどのアプリの上でも動かす必要があり、SYSTEM_ALERT_WINDOW のオーバーレイで実装しました。その中でハマった点をまとめます。
構成:帯1枚+マスク2枚+操作パネル
-
帯(フォーカスストリップ):幅
MATCH_PARENT、高さは可変のウィンドウ1枚。TYPE_APPLICATION_OVERLAY(API 26未満はTYPE_PHONE)、PixelFormat.TRANSLUCENT。 -
行ハイライトモード:帯の中の View に
GradientDrawableでテーマ色を塗り、View の alpha で濃さを調整。カスタムonDrawは使っていません。 -
読書マスクモード:帯の上と下にそれぞれ別ウィンドウ(
Gravity.TOP/Gravity.BOTTOMの単色 View)を置き、その隙間を「読む窓」にしています。帯をドラッグするとマスク2枚も追従させます。 - 操作パネル:モード切替やロックのボタンを持つ別ウィンドウ。
タッチを下のアプリに通す
(以下のコードは説明用に簡略化しています)
読書定規は「上に乗っているのに、下のアプリを普通にスクロールできる」ことが必須です。
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; // ロック中=タッチ透過
- 既定はロック(タッチ透過)。帯を動かしたいときだけパネルのロックボタンで解除し、帯自体を
ACTION_DOWN/MOVE/UPでドラッグしてparams.yをupdateViewLayoutします。 -
FLAG_NOT_TOUCHABLEを付け外しするときは、ウィンドウを一度外して付け直しています。 -
yはステータスバーとナビゲーションバーの間にクランプ(WindowInsetsの statusBars / navigationBars / systemGestures / displayCutout から取得)。
Android 12:タッチ透過ウィンドウの不透明度上限
Android 12(API 31)以降、他アプリのタッチを素通りさせるオーバーレイは不透明度に上限があり、超えるとタッチがブロックされます(untrusted touch 対策)。
float maxAlpha = 1f;
if (Build.VERSION.SDK_INT >= 31) {
InputManager im = getSystemService(InputManager.class);
maxAlpha = (im != null) ? im.getMaximumObscuringOpacityForTouch() : 0.8f;
}
params.alpha = maxAlpha;
ウィンドウ自体の alpha は上限値に固定し、見た目の濃さは中の View の alpha とテーマ色で作るようにしました。マスクを「真っ黒」にできないのはこの制約のためです。
ノッチ(ディスプレイカットアウト)
ノッチのある端末(カットアウト75pxの機種など)で、マスクの上端に隙間が残らないよう指定しています。
- API 30+:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - API 28–29:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
回転時は onConfigurationChanged で帯を再クランプし、マスクを引き直します。読書中に画面が消えないよう、全ウィンドウに FLAG_KEEP_SCREEN_ON を付けています。
常駐とプロセス再起動
-
foregroundServiceType="specialUse"(FOREGROUND_SERVICE_SPECIAL_USE)、通知はIMPORTANCE_LOW・ongoing。 -
START_STICKYに加え、アプリ起動時・プロセス再起動時・WorkManager の15分ごとのウォッチドッグでオーバーレイを復帰させています。 - 権限を後から取り消されたケースで
BadTokenExceptionが出たので、addViewの前に毎回Settings.canDrawOverlays()を確認しています。
権限導線
ACTION_MANAGE_OVERLAY_PERMISSION(package: 付き)→ 付けずに再試行 → ACTION_APPLICATION_DETAILS_SETTINGS の順にフォールバックし、案内画面の onResume で canDrawOverlays() が true になったら戻るようにしています。
おわりに
「上に乗っているのに邪魔をしない」ための細かい処理がほとんどでした。同じようにオーバーレイを作る方の参考になればうれしいです。アプリはこちらです(Windows版もあります)。