Привет! Добро пожаловать обратно в нашу серию статей о CameraX и Jetpack Compose. В предыдущих постах мы рассмотрели основы настройки предварительного просмотра камеры и добавили функцию фокусировки касанием.
🧱 Часть 1 : Создание базового предварительного просмотра камеры с использованием нового артефакта camera-compose. Мы рассмотрели обработку разрешений и базовую интеграцию.
👆 Часть 2 : Использование системы жестов Compose, графики и сопрограмм для реализации визуального эффекта фокусировки касанием.
🔦 Часть 3 (в этом посте): Рассмотрим, как наложить элементы пользовательского интерфейса Compose поверх предварительного просмотра камеры для более удобного пользовательского интерфейса.
📂 Часть 4 : Использование адаптивных API и фреймворка анимации Compose для плавной анимации перехода в настольный режим и обратно на складных телефонах.
В этом посте мы углубимся в нечто более визуально привлекательное — реализацию эффекта подсветки поверх предварительного просмотра камеры, используя распознавание лиц в качестве основы для эффекта. Зачем, спросите вы? Я не уверен. Но выглядит это, безусловно, круто 🙂. И, что более важно, это демонстрирует, как мы можем легко преобразовывать координаты сенсора в координаты пользовательского интерфейса, что позволит нам использовать их в Compose!

Включить распознавание лиц
Для начала изменим CameraPreviewViewModel, чтобы включить распознавание лиц. Мы будем использовать API Camera2Interop , который позволяет нам взаимодействовать с базовым API Camera2 из CameraX. Это даёт нам возможность использовать функции камеры, которые не предоставляются CameraX напрямую. Нам необходимо внести следующие изменения:
- Создайте StateFlow, который содержит границы граней в виде списка
Rect). - Установите параметр запроса
STATISTICS_FACE_DETECT_MODEв значение FULL, что включает распознавание лиц. - Установите
CaptureCallback, чтобы получить информацию о лице из результата захвата.
class CameraPreviewViewModel : ViewModel() { ... private val _sensorFaceRects = MutableStateFlow(listOf<Rect>()) val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow() private val cameraPreviewUseCase = Preview.Builder() .apply { Camera2Interop.Extender(this) .setCaptureRequestOption( CaptureRequest.STATISTICS_FACE_DETECT_MODE, CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) result.get(CaptureResult.STATISTICS_FACES) ?.map { face -> face.bounds.toComposeRect() } ?.toList() ?.let { faces -> _sensorFaceRects.update { faces } } } }) } .build().apply { ... }
Благодаря этим изменениям наша модель представления теперь выдает список объектов Rect , представляющих ограничивающие рамки обнаруженных лиц в координатах датчика.
Преобразовать координаты датчика в координаты пользовательского интерфейса.
Ограничивающие рамки обнаруженных лиц, которые мы сохранили в предыдущем разделе, используют координаты в системе координат датчика . Чтобы отобразить ограничивающие рамки в нашем пользовательском интерфейсе, нам необходимо преобразовать эти координаты таким образом, чтобы они соответствовали системе координат Compose. Нам необходимо:
- Преобразовать координаты датчика в координаты буфера предварительного просмотра.
- Преобразуйте координаты буфера предварительного просмотра в координаты пользовательского интерфейса Compose.
Эти преобразования выполняются с помощью матриц преобразования. Каждое из преобразований имеет свою собственную матрицу:
- В нашем
SurfaceRequestхранится экземплярTransformationInfo, содержащий матрицуsensorToBufferTranform. - В нашем
CameraXViewfinderесть связанный с нимCoordinateTransformer. Возможно, вы помните, что мы уже использовали этот преобразователь в предыдущей статье блога для преобразования координат, используемых для фокусировки касанием.
Мы можем создать вспомогательный метод, который выполнит преобразование за нас:
private fun List<Rect>.transformToUiCoords( transformationInfo: SurfaceRequest.TransformationInfo?, uiToBufferCoordinateTransformer: MutableCoordinateTransformer ): List<Rect> = this.map { sensorRect -> val bufferToUiTransformMatrix = Matrix().apply { setFrom(uiToBufferCoordinateTransformer.transformMatrix) invert() } val sensorToBufferTransformMatrix = Matrix().apply { transformationInfo?.let { setFrom(it.sensorToBufferTransform) } } val bufferRect = sensorToBufferTransformMatrix.map(sensorRect) val uiRect = bufferToUiTransformMatrix.map(bufferRect) uiRect }
- Мы перебираем список обнаруженных лиц и для каждого лица выполняем преобразование.
- Объект
CoordinateTransformer.transformMatrix, получаемый отCameraXViewfinderпо умолчанию преобразует координаты из пользовательского интерфейса в координаты буфера. В нашем случае нам нужно, чтобы матрица работала в обратном направлении, преобразуя координаты буфера в координаты пользовательского интерфейса. Поэтому мы используем методinvert()для инвертирования матрицы. - Сначала мы преобразуем координаты лица из сенсорных координат в буферные координаты с помощью функции
sensorToBufferTransformMatrix, а затем преобразуем эти буферные координаты в координаты пользовательского интерфейса с помощью функцииbufferToUiTransformMatrix.
Реализуйте эффект прожектора.
Теперь давайте обновим компонент CameraPreviewContent , чтобы отобразить эффект прожектора. Мы будем использовать компонент Canvas для рисования градиентной маски поверх предварительного просмотра, чтобы сделать обнаруженные грани видимыми:
@Composable fun CameraPreviewContent( viewModel: CameraPreviewViewModel, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle() val transformationInfo by produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) { try { surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo -> value = transformationInfo } awaitCancellation() } finally { surfaceRequest?.clearTransformationInfoListener() } } val shouldSpotlightFaces by remember { derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} } val spotlightColor = Color(0xDDE60991) .. surfaceRequest?.let { request -> val coordinateTransformer = remember { MutableCoordinateTransformer() } CameraXViewfinder( surfaceRequest = request, coordinateTransformer = coordinateTransformer, modifier = .. ) AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) { Canvas(Modifier.fillMaxSize()) { val uiFaceRects = sensorFaceRects.transformToUiCoords( transformationInfo = transformationInfo, uiToBufferCoordinateTransformer = coordinateTransformer ) // Fill the whole space with the color drawRect(spotlightColor) // Then extract each face and make it transparent uiFaceRects.forEach { faceRect -> drawRect( Brush.radialGradient( 0.4f to Color.Black, 1f to Color.Transparent, center = faceRect.center, radius = faceRect.minDimension * 2f, ), blendMode = BlendMode.DstOut ) } } } } }
Вот как это работает:
- Мы получаем список лиц из модели представления.
- Чтобы избежать перекомпоновки всего экрана каждый раз при изменении списка обнаруженных лиц, мы используем
derivedStateOfдля отслеживания того, обнаружены ли вообще какие-либо лица. Затем это можно использовать сAnimatedVisibilityдля анимации появления и исчезновения цветного наложения. - Объект
surfaceRequestсодержит информацию, необходимую для преобразования координат датчика в координаты буфера в объектеSurfaceRequest.TransformationInfo. Мы используем функциюproduceStateдля настройки слушателя в запросе поверхности и очищаем этот слушатель, когда компонуемый объект покидает дерево композиции. - Мы используем
Canvasдля рисования полупрозрачного розового прямоугольника, который покрывает весь экран. - Мы откладываем чтение переменной
sensorFaceRectsдо тех пор, пока не окажемся внутри блока отрисовкиCanvas. Затем мы преобразуем координаты в координаты пользовательского интерфейса. - Мы перебираем обнаруженные лица, и для каждого лица рисуем радиальный градиент, который сделает внутреннюю часть прямоугольника, окружающего лицо, прозрачной.
- Мы используем
BlendMode.DstOut, чтобы убедиться, что мы вырезаем градиент из розового прямоугольника, создавая эффект прожектора.
Примечание: При изменении камеры на DEFAULT_FRONT_CAMERA вы заметите, что прожектор отображается зеркально! Это известная проблема, отслеживаемая в системе отслеживания ошибок Google .
Результат
С помощью этого кода мы получаем полностью функциональный эффект подсветки, который выделяет обнаруженные лица. Полный фрагмент кода вы можете найти здесь .
Этот эффект — лишь начало: используя возможности Compose, вы можете создавать множество потрясающе красивых визуальных эффектов с помощью камеры. Возможность преобразовывать координаты сенсора и буфера в координаты пользовательского интерфейса Compose и обратно позволяет использовать все функции пользовательского интерфейса Compose и беспрепятственно интегрировать их с базовой системой камеры. Анимация, продвинутая графика пользовательского интерфейса, простое управление состоянием пользовательского интерфейса и полное управление жестами — ваши возможности безграничны!
В заключительной статье этой серии мы подробно рассмотрим, как использовать адаптивные API и фреймворк анимации Compose для плавного перехода между различными интерфейсами камеры на складных устройствах. Следите за обновлениями!
Приведенные в этом блоге фрагменты кода распространяются под следующей лицензией:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Большое спасибо Нику Бутчеру , Алексу Ваньо , Тревору Макгуайру , Дону Тернеру и Лорен Уорд за рецензирование и предоставленные отзывы. Это стало возможным благодаря кропотливой работе Ясита Виданаарачча .
Продолжить чтение

Инструкции
В этой статье вы узнаете, как использовать API waitUntil в Compose для ожидания выполнения определенных условий.
Jose Alcérreca • Чтение 3 минуты

Инструкции
Независимо от того, используете ли вы Gemini в Android Studio, Gemini CLI, Antigravity или сторонние агенты, такие как Claude Code или Codex, наша миссия — обеспечить возможность высококачественной разработки под Android повсюду.
Adarsh Fernando , Esteban de la Canal • Чтение 4 минуты

Инструкции
Понимая, что чрезмерный расход заряда батареи является одной из главных проблем для пользователей Android, Google предпринимает значительные шаги, чтобы помочь разработчикам создавать более энергоэффективные приложения.
Alice Yuan • 8 мин чтения
Будьте в курсе событий
Получайте еженедельно самые свежие новости о разработке Android прямо на свою электронную почту.






