Display a camera preview with PreviewView
Android Jetpack CameraX: PreviewView
A common use case for any camera app is to display a preview from the camera. So far, this has been quite difficult to get right, mostly due to the complexities that exist around the camera2 API edge cases and varying device behaviors. PreviewView , part of the CameraX Jetpack library, makes displaying a camera preview easier for developers by providing a developer-friendly, consistent, and stable API across a wide range of Android devices.
Introducing PreviewView
PreviewView is a custom View that enables the display of a camera feed. It was built to offload the burden of setting up and handling the preview surface used by the camera.
If you need to display a basic camera preview in your app, PreviewView is the recommended way to do so, as it is:
- Easier to use: PreviewView is a View . It implements all the work necessary to display what the camera sees in your layout by managing the Surface used by the Preview use case.
- Light-weight: PreviewView focuses only on preview. All its internal resources are geared towards getting a camera preview set up and managing the preview surface while the camera’s using it. This separation of concerns allows for PreviewView code to stay clean.
- Exhaustive: PreviewView handles the hardest parts of displaying the camera feed on the screen, including aspect ratio, scaling, and rotation. It also contains compatibility fixes and workarounds in order to provide a seamless experience on a multitude of devices, screen sizes, camera hardware support levels, and display configurations such as split-screen mode, locked orientation, and free-form multi-window.
PreviewView — Implementation modes
PreviewView is a subclass of FrameLayout . To display the camera feed, it uses either a SurfaceView or TextureView , provides a preview surface to the camera when it’s ready, tries to keep it valid as long as the camera is using it, and when released prematurely, provides a new surface if the camera is still in use.
SurfaceView is generally better than TextureView when it comes to certain key metrics, including power and latency, which is why PreviewView tries to use a SurfaceView by default. However, some devices (mainly legacy devices) crash when the preview surface is released prematurely. With SurfaceView , unfortunately, it isn’t possible to control when the surface is released, as this is controlled by the View hierarchy. On these devices, PreviewView falls back to using a TextureView instead. You should also force PreviewView to use a TextureView in cases where preview rotation, transparency, or animation are needed.
You can explicitly set the implementation you want PreviewView to use by calling PreviewView.setPreferredImplementationMode(ImplementationMode) , where ImplementationMode is either SURFACE_VIEW or TEXTURE_VIEW . PreviewView tries to honor your choice when the preferred mode is SURFACE_VIEW , and guarantees it when it’s TEXTURE_VIEW .
⚠️ Make sure to set your preferred implementation mode before starting preview by calling Preview.setSurfaceProvider(PreviewView.createSurfaceProvider()) .
The following shows how to set the preferred implementation mode of PreviewView .
PreviewView — Preview
PreviewView handles the nuts and bolts of creating a SurfaceProvider needed by the Preview use case to start a preview stream. The SurfaceProvider prepares the surface that will be provided to the camera in order to display a camera preview stream, and takes care of recreating the Surface when necessary. PreviewView.createSurfaceProvider(CameraInfo) accepts a nullable CameraInfo instance. PreviewView uses it, along with your preferred implementation mode and the camera’s capabilities, to determine the implementation to use internally. If you pass in a null CameraInfo , PreviewView uses a TextureView implementation, since it can’t tell whether the chosen camera will work with SurfaceView .
Once you build the Preview use case and any other use cases you need, bind them to a LifecycleOwner , use the CameraInfo from the bound camera to create a SurfaceProvider , and then attach it to the Preview use case to start the preview stream by calling Preview.setSurfaceProvider(SurfaceProvider) .
The following shows how to attach PreviewView to Preview to start a preview stream.
PreviewView — Scale types
PreviewView provides an API that allows you to control how the preview should look and where it should be positioned within its container.
- The how defines whether the preview should fit inside ( FIT) or fill ( FILL) its container.
- The where defines whether the preview should be at the top left ( START), center ( CENTER) or bottom right ( END) or its container.
The combinations created from the “ how” and “ where” represent the available scale type values PreviewView supports: FIT_START , FIT_CENTER , FIT_END , FILL_START , FILL_CENTER and FILL_END . The most commonly used are FIT_CENTER , which translates to a letterboxed preview, and FILL_CENTER which center-crops the preview in its container.
Setting the scale type can be done in one of two ways:
- In the XML layout by using PreviewView ’s scaleType attribute as shown in the following sample.
- Programmatically by calling PreviewView.setScaleType(ScaleType) as shown below.
To retrieve the current scale type that a PreviewView is using, call PreviewView.getScaleType() .
PreviewView — Camera Controls
Depending on the camera sensor orientation, the device’s rotation, the display mode, and the preview scale type, PreviewView may scale, rotate, and translate the preview frames it receives from the camera in order to correctly display the preview stream in the UI. This is why being able to convert UI coordinates to camera sensor coordinates is important. In CameraX, this conversion is done by a MeteringPointFactory . PreviewView provides an API to create one: PreviewView.createMeteringPointFactory(cameraSelector) , where the CameraSelector portrays the camera streaming the preview.
PreviewView ’s MeteringPointFactory comes in handy when you need to implement tap-to-focus. Even though autofocus is enabled by default on the camera preview (when the camera supports it), you may also control the focus target when tapping on PreviewView . The MeteringPointFactory will convert the coordinates of the focus target to the camera sensor coordinates, which then enables the camera to focus on that region.
The following sample shows how to implement tap-to-focus using a touch listener set on PreviewView .
Another common camera preview feature is pinch-to-zoom, which enables the camera to zoom in and out as you pinch the preview. To implement it with PreviewView , add a touch listener to PreviewView and attach it to a scale gesture listener. This will intercept pinch gestures and update the camera’s zoom ratio accordingly.
The following sample shows how to implement pinch-to-zoom with PreviewView .
PreviewView — How it’s tested
PreviewView provides a consistent camera behavior across a wide range of Android devices. This is thanks to the investment CameraX has made in testing PreviewView and its other APIs in its automated testing lab. These tests are divided into two main categories:
- Unit tests that verify PreviewView ’s behavior with regard to its implementation modes, scale types, and MeteringPointFactory . They also make sure PreviewView correctly adjusts the preview when it should, such as when the size of its container changes, when the display layout is updated, and when it is attached or reattached to a Window .
- Integration tests that ensure PreviewView behaves correctly when it’s part of an app, and displays or stops a preview stream accordingly. These tests include verifying the preview state while the app is running, after it’s closed and reopened multiple times, after the camera lens is toggled back and forth, and after the app’s lifecycle is destroyed and recreated. Currently, these tests mostly cover the TextureView implementation of PreviewView , as getting a signal on when the preview starts and stops from a SurfaceView has proved to be challenging.
Conclusion
- PreviewView is a custom View that makes displaying a camera preview easier.
- PreviewView handles the preview surface using a SurfaceView by default, but may fall back to using a TextureView when needed or requested.
- Bind your other use cases, like ImageCapture and ImageAnalysis , to a LifecycleOwner , and then start the camera preview by attaching a SurfaceProvider from PreviewView to the bound Preview use case.
- Control how the preview is displayed by defining PreviewView ’s scale type.
- Implement tap-to-focus on your preview by getting a MeteringPointFactory from PreviewView .
- Implement pinch-to-zoom on your preview by setting up a gesture listener on PreviewView .
Want more CameraX goodness? Check out:
Questions about anything related to PreviewView or preview? Leave a comment. Thanks for reading!
Источник
Полный список
— используем объект Camera для получения изображения с камеры
— подгоняем изображение под размеры экрана
— учитываем поворот устройства
Разберемся, какие основные объекты нам понадобятся для вывода изображения с камеры на экран. Их три: Camera, SurfaceView, SurfaceHolder.
Camera используется, чтобы получить изображение с камеры. А чтобы это изображение в приложении отобразить, будем использовать SurfaceView.
Нормального перевода слова Surface я не смог подобрать. «Поверхность» — как-то слишком абстрактно. Поэтому так и буду называть – surface. Это будет означать некий компонент, который отображает изображение с камеры.
Работа с surface ведется не напрямую, а через посредника – SurfaceHolder (далее holder). Именно с этим объектом умеет работать Camera. Также, holder будет сообщать нам о том, что surface готов к работе, изменен или более недоступен.
А если подытожить, то: Camera берет holder и с его помощью выводит изображение на surface.
Напишем приложение, в котором реализуем вывод изображения с камеры на экран.
Project name: P1321_CameraScreen
Build Target: Android 2.3.3
Application name: CameraScreen
Package name: ru.startandroid.develop.p1321camerascreen
Create Activity: MainActivity
SurfaceView по центру экрана.
В манифест добавьте права на камеру:
В onCreate настраиваем Activity так, чтобы оно было без заголовка и в полный экран. Затем мы определяем surface, получаем его holder и устанавливаем его тип = SURFACE_TYPE_PUSH_BUFFERS (настройка типа нужна только в Android версии ниже 3.0).
Далее для holder создаем callback объект HolderCallback (о нем чуть дальше), через который holder будет сообщать нам о состояниях surface.
В onResume получаем доступ к камере, используя метод open. На вход передаем id камеры, если их несколько (задняя и передняя). Этот метод доступен с API level 9. В конце этого урока есть инфа о том, как получить id камеры.
Также существует метод open без требования id на вход. Он даст доступ к задней камере. Он доступен и в более ранних версиях.
После этого вызываем метод setPreviewSize, в котором настраиваем размер surface. Его подробно обсудим ниже.
В onPause освобождаем камеру методом release, чтобы другие приложения могли ее использовать.
Класс HolderCallback, реализует интерфейс SurfaceHolder.Callback. Напомню, что через него holder сообщает нам о состоянии surface.
В нем три метода:
surfaceCreated – surface создан. Мы можем дать камере объект holder с помощью метода setPreviewDisplay и начать транслировать изображение методом startPreview.
surfaceChanged – был изменен формат или размер surface. В этом случае мы останавливаем просмотр (stopPreview), настраиваем камеру с учетом поворота устройства (setCameraDisplayOrientation, подробности ниже), и снова запускаем просмотр.
surfaceDestroyed – surface более недоступен. Не используем этот метод.
С этими методами, кстати, есть одна странность. В хелпе к методу surfaceChanged написано, что он обязательно будет вызван не только при изменении, но и при создании surface, т.е. сразу после surfaceCreated. Но при этом в хелпе к камере методы запуска просмотра (setPreviewDisplay, startPreview) вызываются и в surfaceCreated и в surfaceChanged. Т.е. при создании surface мы зачем-то два раза стартуем просмотр. Мне непонятно, зачем нужно это дублирование.
Если очистить метод surfaceCreated, то все продолжает работать. Но в уроке я, пожалуй не рискну так делать. Вдруг я чего не понимаю и в этом есть какой-то смысл. Если кто знает – пишите на форуме.
Размер превью
Метод setPreviewSize. Немного нетривиальный, особенно если вы никогда не работали с объектами Matrix и RectF.
В нем мы определяем размеры surface с учетом экрана и изображения с камеры, чтобы картинка отображалась с верным соотношением сторон и на весь экран.
Дальнейшие выкладки можно пропустить, если неохота мозг ломать и вникать в механизм. Хотя я постарался сделать эти выкладки понятными, интересными и даже картинки нарисовал. Если вы все поймете, будет отлично!) Когда-нибудь эти знания пригодятся.
Итак, у нас есть картинка, которая приходит с камеры – назовем ее превью. И у нас есть экран, на котором нам надо это превью отобразить.
Рассмотрим конкретный пример, чтобы было нагляднее. Планшет Galaxy Tab, задняя камера, нормальное горизонтальное положение.
Есть экран. Размер: 1280×752. Соотношение сторон: 1280/752 = 1,70
Есть превью. Размер: 640×480. Соотношение сторон: 640/480 = 1,33.
Допустим, что мы камеру навели на какой-то круг.
Мы хотим получить картинку на весь экран. Какие есть варианты? Их три.
1) Растянуть превью на экран. Плохой вариант, т.к. для этого соотношение сторон должно быть одинаковым, а у нас оно разное. Но все же попытаемся, чтобы увидеть результат.
Для этого нам ширину превью надо умножить на 1280/640 = 2. А высоту на 752/480 = 1,57. В итоге имеем:
видно, что картинка деформировалась и стала растянутой по горизонтали. Нам это не подходит.
2) Втиснуть превью в экран с сохранением пропорций. Для этого мы будем менять размеры превью (сохраняя соотношение сторон), пока оно изнутри не упрется в границы экрана по высоте или ширине. В нашем случае оно упрется по высоте.
Для этого нам надо умножить ширину и высоту превью на меньшее из чисел: 1280/640 = 2 и 752/480 = 1,57, т.е. на 1.57.
Смотрим, чего получилось
стало гораздо лучше. Теперь картинка превью не искажена. Единственное, что немного смущает – пустые области по бокам экрана. Но ничего не мешает закрасить их черным и пусть все думают, что так и задумано. Зато мы будем видеть полную и неискаженную картинку. Так, например, обычно делается в видео-плеерах.
3) Втиснуть экран в превью. Т.е. сделать второй вариант наоборот. Менять размер экрана (сохраняя соотношение сторон) до тех пор пока он изнутри не упрется в границы превью по высоте или ширине.
Для этого нам надо было бы ширину и высоту экрана разделить на большее из чисел: 1280/640 = 2 и 752/480 = 1,57, т.е. на 2.
Но т.к. менять размеры экрана мы не можем физически, то мы будем менять размеры превью чтобы достигнуть описанного результата.
Для этого нам надо умножить ширину и высоту превью на большее из чисел: 1280/640 = 2 и 752/480 = 1,57, т.е. на 2.
Картинка не искажена и занимает полный экран. Но есть нюанс. Мы не видим всего изображения. Оно выходит за границы экрана сверху и снизу.
На всякий случай укажу, что это лишь один пример. В других может быть по другому. Например, во втором варианте пустые области могут быть не по бокам, а сверху и снизу. А на мелких девайсах размер превью будет больше размера экрана. Но общий смысл и алгоритм от этого не меняются.
Мы рассмотрели три варианта, и увидели, что первый совсем плох, а второй и третий вполне годятся для реализации.
От картинок возвращаемся к коду. Метод setPreviewSize(boolean fullScreen) реализует второй (если fullScreen == false) и третий (если fullScreen == true) варианты.
Красота метода в том, что все преобразования за нас делает Matrix (матрица). И нам самим не надо будет ничего умножать или делить.
Сначала мы получаем размеры экрана и превью. Для экрана сразу выясняем что больше: ширина или высота. Т.е. если ширина больше, то устройство находится в горизонтальной ориентации, если высота больше – в вертикальной.
Для преобразований матрица потребует от нас RectF объекты. Если никогда еще не работали с ними, то это просто объект, который содержит координаты прямоугольника: left, top, right, bottom.
В качестве left и top мы всегда будем использовать 0, а в right и bottom помещать ширину и высоту экрана или превью. Тем самым мы будем получать прямоугольники точно совпадающие по размерам с экраном и превью.
rectDisplay – экран, rectPreview – превью. У превью обычно ширина всегда больше высоты. Если устройство в горизонтальной ориентации, то мы создаем rectPreview соответственно его размерам. А если устройство вертикально, то изображение с камеры будет также повернуто вертикально, следовательно ширина и высота поменяются местами.
Теперь самое интересное – подготовка преобразования. Используем метод setRectToRect. Он берет на вход два RectF. И вычисляет, какие преобразования надо сделать, чтобы первый втиснуть во второй. Про третий параметр метода я сейчас рассказывать не буду, мы всегда используем START. (Если все же интересно, задавайте вопрос на форуме, там обсудим)
Т.е. этот метод пока не меняет объекты. Это только настройка матрицы. Теперь матрица знает, какие расчеты ей надо будет произвести с координатами объекта, который мы ей позже предоставим.
Смотрим код. Если (!fullScreen), то это второй вариант, т.е. превью будет втиснут в экран. Для этого мы просто сообщаем матрице, что нам объект с размерами превью надо будет втиснуть в объект с размерами экрана. Т.е. если обратиться ко второму варианту, то матрица поняла, что ей надо будет умножить стороны объекта на 1.57. И когда мы ей потом предоставим объект с размерами превью – она это сделает и мы получим необходимые нам размеры.
Если же fullScreen (третий вариант), то алгоритм чуть сложнее. Мы сообщаем матрице, что нам надо объект с размерами экрана втиснуть в объект с размерами превью. Смотрим третий вариант. Поначалу мы выяснили, что экран надо будет разделить на два. Но потом мы поняли, что мы не можем менять размеры экрана и нам надо делать наоборот – не экран делить на два, а превью умножить на два. Это мы можем объяснить и матрице вызвав метод invert. Матрица возьмет алгоритм из переданной ей матрицы (т.е. из самой себя), и сделает все наоборот. Т.е. вместо того, чтобы разделить на два – умножит.
Очень надеюсь, что изложил понятно. Если же не понятно – перечитайте раз 5 и сверяйтесь с описанием вариантов и картинками в примере выше. Если все равно не понятно, вернитесь к этому где-нить через недельку. Мозг к тому времени уже усвоит и как-то уложит эту инфу. И повторное прочтение может пройти гораздо легче. По крайне мере у меня обычно это так) Я могу что-то прочесть – ничего не понять. Но через неделю/месяц/полгода снова заглянуть туда и удивиться: «а что здесь собственно непонятного то было?»
Итак, мы подготовили матрицу к преобразованию, осталось только вручить ей объект, который она этим преобразованием подвергнет. Для этого используем метод mapRect и передаем ему объект с размерами превью. Как и в примере выше, все преобразования мы будем проводить с ним.
После проведения преобразований мы берем получившиеся координаты и настраиваем по ним surface, которое отображает превью.
Поворот превью
Если мозг еще не разрушен, сейчас мы это исправим! Разбираем метод setCameraDisplayOrientation, который будет превью вращать.
Снова рассмотрим пример, когда используется планшет в горизонтальном состоянии, камера – задняя. Допустим, мы через камеру смотрим на такой объект:
Видим его на экране, все ок.
Важное замечание. Через стандартное приложение камеры нижеописанный пример не воспроизведется, т.к. стандартное приложение обрабатывает поворот устройства. А я хочу продемонстрировать, что было бы если бы не обрабатывало.
Я поворачиваю планшет по часовой (направо) на 90 градусов. При этом, разумеется поворачивается и камера. На экране я вижу теперь такую картинку:
Кстати, такую же картинку увидите и вы, если наклоните голову вправо на 90 градусов)
Т.е. система хоть и среагировала на поворот и повернула основное изображение, но камера возвращает нам именно такой повернутый вид. Его мы и видим.
Что надо сделать, чтобы это исправить? Повернуть картинку на 90 по часовой. Т.е. сделать тот же поворот, что сделала камера.
Получилась Аксиома Поворота Камеры: насколько и в какую сторону повернута камера, на столько же и в ту же сторону нам надо поворачивать и превью, чтобы получать правильную картинку.
Для этого будем использовать метод setDisplayOrientation. Он принимает на вход угол, на который камера будет поворачивать по часовой превью, перед тем как отдать его нам. Т.е. от нас ждут угол поворота превью по часовой. Его мы можем узнать, выяснив насколько повернута по часовой камера (см. Аксиому Поворота Камеры).
Для этого используем такую конструкцию — getWindowManager().getDefaultDisplay().getRotation(). Она возвращает нам градусы, на которые система поворачивает изображение по часовой, чтобы оно нормально отображалось при поворотах устройства.
Т.е. когда вы наклоняете устройство на 90 против часовой, система должна поворачивать изображение на 90 по часовой, чтобы компенсировать поворот. (сейчас речь не о камере, а просто об изображении которое показывает телефон, например — Home)
Аксиома Поворота Устройства: насколько и в какую сторону повернуто устройство, на столько же, но в другую сторону система поворачивает изображение, чтобы получать его в правильной ориентации.
Отсюда следует, что getWindowManager().getDefaultDisplay().getRotation() сообщает нам насколько устройство повернуто против часовой.
Кстати, от getRotation мы получаем константы, а далее в switch преобразуем их в градусы.
Итак, переменная degrees содержит кол-во градусов, на которые повернуто устройство против часовой.
До сих пор мозг цел? Тогда держите такой факт: камера в устройстве может быть повернута относительно этого устройства.
Так обычно делается на смартфонах. Т.е. там камера повернута на 90 градусов. И ее нормальная ориентация совпадает с горизонтальной ориентацией устройства. Чтобы и в превью и на экране ширина получалась больше высоты.
И вот этот поворот нам тоже надо учитывать при повороте превью. Получить данные о камере можно методом getCameraInfo. На вход требует id камеры и объект CameraInfo, в который будет помещена инфа о камере.
Нас интересует поле CameraInfo.orientation, которое возвращает на сколько по часовой надо повернуть превью, чтобы получить нормальное изображение. Т.е. исходя из Аксиомы Поворота Камеры – на столько же повернута по часовой и сама камера.
Ну и добиваем мозг следующим фактом. Камера может быть задней и передней (фронтальной). И для них по разному надо считать повороты)
Поле CameraInfo.facing содержит инфу о том, какая камера – задняя или передняя.
Попробуем посчитать. Напомню, что метод setDisplayOrientation ждет от нас градус поворота превью по часовой. Т.е. мы можем просто посчитать поворот камеры по часовой (Аксиома Поворота Камеры) и получим нужное значение.
Чтобы узнать итоговый поворот камеры по часовой в пространстве – надо сложить поворот устройства по часовой и CameraInfo.orientation. Это для задней камеры. А для передней – надо CameraInfo.orientation вычесть, потому что она смотрит в нашу сторону. И все, что для нас по часовой, для нее — против.
Все, считаем. У нас есть degrees — кол-во градусов, на которые повернуто устройство против часовой. Чтобы конвертнуть это кол-во в градусы по часовой, надо просто вычесть их из 360.
Т.е. (360 – degrees) – это поворот устройства по часовой. Я специально выделил в коде это выражение скобками для наглядности. Далее мы к этому значению прибавляем или вычитаем (задняя или передняя камера) встроенный поворот камеры. В случае с передней камерой на всякий случай прибавляем 360 градусов, чтобы не получилось отрицательное число. И в конце определяем итоговое кол-во градусов в пределах от 0 до 360, вычисляя остаток от деления на 360.
И торжественно передаем камере это значение.
На редкость мозгодробительная штука – работа с камерой, правда? В итоге, когда вы все это запустите, вы должны видеть адекватное изображение с камеры.
В начале кода есть две константы: CAMERA_ID и FULL_SCREEN.
Если у вас две камеры, вы можете передать в CAMERA_ID не 0, а 1, и получите картинку с передней камеры.
Ну а меняя FULL_SCREEN изменяйте вид превью.
Прочее
Как определить, есть ли камера в устройстве? Об этом сообщит конструкция context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
Получить id камеры, можно используя метод getNumberOfCameras (доступен с API Level 9). Он вернет нам некое кол-во камер N, которые доступны на устройстве. Соответственно их ID будут 0, 1, …, N-1. По этому id уже получаете CameraInfo и определяете, что это за камера.
Метод open может вернуть Exception при запуске если по каким-то причинам не удалось получить доступ к камере. Имеет смысл это обрабатывать и выдавать сообщение пользователю, а не вылетать с ошибкой.
Обработка поворота может криво работать на некоторых девайсах. Например, я тестил на HTC Desire (4.2.2) и Samsung Galaxy Tab (4.2.2) – было все ок. А на Samsung Galaxy Ace (2.3.6) сложилось ощущение, что камера просто игнорит градус поворота, который я ей сообщаю.
На следующем уроке:
— делаем снимок
— пишем видео
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник