← Все гайды AI Release
EN Подписаться на @ai_release1

Как сделать ИИ-оверлей на Android с распознаванием экрана: плавающая кнопка, скриншоты и OCR

Обновлено: 19.10.2026 · AI Release · @ai_release1
Как сделать ИИ-оверлей на Android с распознаванием экрана: плавающая кнопка, скриншоты и OCR

TL;DR

Итог: плавающая кнопка поверх любых приложений, по нажатию — снимок экрана, OCR и вывод распознанного текста в лог и в собственный оверлей.

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

Чек-лист перед стартом:

Пример: если adb devices печатает пустой список, включите «Отладка по USB» в «Настройки → О системе → Номер сборки (7 тапов) → Для разработчиков». Если эмулятора нет, поднимите его по инструкции из гайда Как запускать Android-приложения и игры на ПК: эмуляторы, Waydroid и облачные сервисы.

Схема пайплайна, которую будем собирать:

1. Плавающая кнопка (TYPE_APPLICATION_OVERLAY) — ожидаемый результат: кружок висит поверх рабочего стола и других приложений.

2. Запрос захвата экрана (MediaProjectionManager.createScreenCaptureIntent()) — ожидаемый результат: системный диалог «Начать запись экрана?».

3. VirtualDisplay + ImageReader — ожидаемый результат: в лог приходит размер кадра.

4. OCR по Bitmap — ожидаемый результат: в лог приходит распознанная строка.

Шаг 1. Каркас приложения и плавающая кнопка

Что сделать: объявите разрешения и foreground service в app/src/main/AndroidManifest.xml, затем добавьте кнопку через WindowManager.

Зачем: окно TYPE_APPLICATION_OVERLAY — единственный штатный способ рисовать поверх чужих приложений без root. Разрешение SYSTEM_ALERT_WINDOW выдаётся пользователем вручную и без него addView падает.

Как проверить: после запуска приложения кнопка видна поверх рабочего стола, а appops показывает allow.

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<service
    android:name=".CaptureService"
    android:exported="false"
    android:foregroundServiceType="mediaProjection"/>
private fun showFloatingButton() {
    if (!Settings.canDrawOverlays(this)) {
        startActivity(
            Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:$packageName"))
        )
        return
    }
    val button = Button(this).apply { text = "OCR" }
    val params = WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT
    ).apply {
        gravity = Gravity.TOP or Gravity.START
        x = 24
        y = 300
    }
    getSystemService(WINDOW_SERVICE).let {
        (it as WindowManager).addView(button, params)
    }
}

Пример: adb shell appops get com.example.screenocr SYSTEM_ALERT_WINDOW → SYSTEM_ALERT_WINDOW: allow. Если вывод SYSTEM_ALERT_WINDOW: deny — разрешение не выдано, кнопки на экране не будет.

Почему это работает: WindowManager рисует view в отдельном окне, которое принадлежит вашему процессу, но отображается в общем слое поверх остальных приложений.

Шаг 2. Захват экрана через MediaProjection и OCR

Иллюстрация к разделу «Шаг 2. Захват экрана через MediaProjection и OCR»
Иллюстрация к разделу «Шаг 2. Захват экрана через MediaProjection и OCR»

Что сделать: по нажатию кнопки запросите согласие на захват, передайте resultCode и Intent в foreground service, создайте VirtualDisplay с ImageReader и отдайте полученный Bitmap в OCR.

Зачем: MediaProjection отдаёт видеопоток экрана, а ImageReader превращает один кадр в Bitmap — это и есть «скриншот» без хранения файла на диске.

Как проверить: в adb logcat -s CAPTURE:D OCR:D появляются размер кадра и распознанный текст.

КомпонентРольКак проверить
`WindowManager` + `TYPE_APPLICATION_OVERLAY`плавающая кнопка`adb shell appops get <пакет> SYSTEM_ALERT_WINDOW`
`MediaProjectionManager`запрос доступа к экранусистемный диалог захвата
`ImageReader` + `VirtualDisplay`кадры экраналог с тегом `CAPTURE`
OCR-библиотекатекст из `Bitmap`лог с тегом `OCR`
// В Activity: получаем согласие пользователя
val mgr = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
startActivityForResult(mgr.createScreenCaptureIntent(), REQ_CAPTURE)

// В CaptureService: стартуем foreground и поднимаем захват
startForeground(1, buildNotification(),
    ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)

val projection = mgr.getMediaProjection(resultCode, data)
val dm = resources.displayMetrics
val reader = ImageReader.newInstance(
    dm.widthPixels, dm.heightPixels, PixelFormat.RGBA_8888, 2)

projection.createVirtualDisplay(
    "ocr-capture",
    dm.widthPixels, dm.heightPixels, dm.densityDpi,
    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
    reader.surface, null, null)

reader.setOnImageAvailableListener({ r ->
    val image = r.acquireLatestImage() ?: return@setOnImageAvailableListener
    val bitmap = image.toBitmap()
    image.close()
    runOcr(bitmap)
}, handler)

Конвертация кадра с учётом rowStride (иначе картинка «съезжает» по диагонали):

fun Image.toBitmap(): Bitmap {
    val plane = planes[0]
    val rowPadding = plane.rowStride - plane.pixelStride * width
    val raw = Bitmap.createBitmap(
        width + rowPadding / plane.pixelStride, height, Bitmap.Config.ARGB_8888)
    raw.copyPixelsFromBuffer(plane.buffer)
    return Bitmap.createBitmap(raw, 0, 0, width, height)
}
private fun runOcr(bitmap: Bitmap) {
    val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
    recognizer.process(InputImage.fromBitmap(bitmap, 0))
        .addOnSuccessListener { Log.d("OCR", it.text) }
        .addOnFailureListener { Log.e("OCR", "fail", it) }
}

Пример: на экране открыт текст «Оплата прошла успешно». В логе: D/OCR: Оплата прошла успешно. Плавающую кнопку перед снимком скройте (button.visibility = View.GONE) или исключите из кадра — иначе она попадёт в распознанный текст.

Почему это работает: VirtualDisplay дублирует содержимое дисплея в Surface, который принадлежит ImageReader; библиотека OCR принимает Bitmap и возвращает блок текста.

Шаг 3. Проверка работы и финальный результат

Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»
Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»

Что сделать: установите сборку, выдайте разрешения, нажмите кнопку и снимите лог.

Зачем: так вы отделяете проблему разрешений от проблемы OCR — по тегам видно, на каком шаге пайплайн остановился.

Как проверить: в логе есть и CAPTURE, и OCR; на экране — распознанный текст в вашем оверлее.

adb install -r app/build/outputs/apk/debug/app-debug.apk
adb logcat -c
adb logcat -s CAPTURE:D OCR:D

Ожидаемый вывод:

D/CAPTURE: frame 1080x2400
D/OCR: Оплата прошла успешно

Финальный чек-лист успеха:

Дальше распознанный текст можно отправить в модель — локальный запуск описан в гайде Как запустить ИИ-модель локально на Android: приложения и настройка, а если нужен Linux-инструментарий рядом — в Как запустить Linux на Android без root: Termux, PRoot и ИИ-инструменты.

Возможные проблемы и решения

Проблема. Кнопка не появляется, приложение падает при запуске

Проблема. Захват экрана падает сразу после согласия пользователя

Проблема. Кадр получен, но текст пустой

Резюме и следующие шаги

Вы собрали оверлей из трёх частей: плавающая кнопка на TYPE_APPLICATION_OVERLAY, захват кадра через MediaProjection + ImageReader и OCR по Bitmap с выводом в лог. Следующий шаг — отправить распознанный текст в модель и вернуть ответ в то же плавающее окно, не забывая скрывать кнопку перед снимком.

FAQ

Нужен ли root для ИИ-оверлея с распознаванием экрана?

Нет. SYSTEM_ALERT_WINDOW и MediaProjection — штатные механизмы Android, пользователь подтверждает их вручную. Root не требуется ни для кнопки, ни для снимка экрана.

Почему OCR возвращает пустой текст на некоторых приложениях?

Скорее всего, приложение ставит FLAG_SECURE (защита от скриншотов) — система отдаёт чёрный кадр. Проверьте на обычном приложении с текстом: если там текст распознаётся, дело именно в защите окна.

Можно ли обойтись без MediaProjection и читать экран через AccessibilityService?

Можно как альтернативу, но это другой источник данных и другие ограничения по доступным элементам интерфейса; конкретный набор возможностей сверьте с документацией по AccessibilityService для вашей версии Android.

Куда девать распознанный текст дальше?

В лог — для отладки, в собственный оверлей — для пользователя, в модель — для ответа по содержимому экрана. Для локального запуска модели на телефоне см. гайд Как запустить ИИ-модель локально на Android: приложения и настройка.

🤖 Кратко для ИИ