- SurfaceView
- Создание нового объекта SurfaceView
- SurfaceView с поддержкой касаний
- Гроза эпилептиков
- Поймай меня
- Отскакивающие значки
- Анимация на Android с помощью SurfaceView
- Работа с SurfaceView в Android
- Класс SurfaceView
- Создание класса
- Реализация потока отрисовки
- Начало и завершение отрисовки
- Android. Surface
- Дисклеймер
- Пример использования с разным API
- Наложение нескольких Surface
- StateMachine / Машина состояний / Конечный автомат
- GLSurfaceView
- Camera API V2
- MediaCodec
SurfaceView
Класс SurfaceView предоставляет объект Surface, который поддерживает рисование в фоновом потоке и дает возможность использовать OpenGL для трехмерной графики. Это отличный вариант для насыщенных графикой элементов, которые нуждаются в частых обновлениях или должны отображать сложную графическую информацию, как в случае с играми и трехмерной визуализацией.
Найти данный элемент можно в разделе Advanced.
В основе SurfaceView объект Surface, а не Canvas. Это важно, потому как Surface поддерживает рисование из фоновых потоков. Данное отличие особенно полезно для ресурсоемких операций или быстрых обновлений, а также когда необходимо обеспечить высокую частоту изменения кадров (использование трехмерной графики, создание игр или предпросмотр видеопотока с камеры в режиме реального времени.
Возможность рисовать вне зависимости от графического потока ведет к повышенному потреблению памяти. Таким образом, хоть это и полезный (а иногда просто необходимый) способ создания нестандартных представлений, будьте осторожны, используя его.
SurfaceView используется точно таким же образом, как любые производные от View классы. Вы можете применять анимацию и размещать их внутри разметки так же, как и другие компоненты.
Применяя OpenGL, вы можете рисовать на Surface любые поддерживаемые двумерные или трехмерные объекты, получая при этом все выгоды от аппаратного ускорения (если таковое имеется). Таким образом, вы значительно повышаете производительность, если сравнивать с теми же операциями, выполненными на двумерном Canvas.
Объекты SurfaceView особенно пригодятся для отображения динамических трехмерных изображений, к примеру, в интерактивных играх, их можно назвать лучшим выбором для отображения предварительного просмотра видеопотоков с камеры в режиме реального времени.
Создание нового объекта SurfaceView
Чтобы создать данный тип, наследуйте класс SurfaceView и реализуйте интерфейс SurfaceHolder.Callback, описывающий функцию обратного вызова. Он уведомляет представление о том, что исходный объект Surface был создан/уничтожен/модифицирован и передает в объект SurfaceHolder ссылку, содержащую допустимый экземпляр Surface.
Типичный шаблон проектирования SurfaceView предусматривает классы, производные от Thread, которые принимают ссылку на текущий объект SurfaceHolder и немедленно его обновляют.
SurfaceView с поддержкой касаний
Напишем простой пример использования SurfaceView, на котором можно рисовать линии.
Запустив проект, вы можете рисовать пальцем по экрану.
Гроза эпилептиков
Попробовал данный пример. При рисовании рисуются окружности, которые постоянно мигают. Жуткое зрелище.
В примере реализовано:
- новый класс MySurfaceView, наследующий от SurfaceView и реализующий интерфейс SurfaceHolder.Callback
- используются методы surfaceCreated(), surfaceDestroyed(), surfaceChanged() для SurfaceHolder.Callback
- используется класс Thread для SurfaceView
Поймай меня
Тот автор на основе предыдущего примера сделал новый пример. На экране рисуется белая точка, которая начинает двигаться в место касания экрана пальцем пользователя.
Этот же пример, но с использованием разметки FrameLayout, внутри которой находится SurfaceView, находится здесь. Не буду приводить код.
Продолжение серии примеров — Exercise of SurfaceView: SurfaceView overlap with a LinearLayout. Здесь появляется возможность выбрать цвет точки.
Отскакивающие значки
Ещё один пример — при касании экрана в области SurfaceView будет появляться новая картинка, которая будет двигаться внутри контейнера, отскакивая от стенок.
Источник
Анимация на Android с помощью SurfaceView
В уроке Анимация на Android я показал, как делать анимацию с помощью View и функции invalidate(). Но у такого метода есть недостаток — метод invalidate() вызывает onDraw() не мгновенно, а когда решит операционная система. И если анимация требует тяжелых расчетов, она может заметно тормозить.
Для тяжелых случаев есть другой метод рисования через SurfaceView.
Класс SurfaceView предоставляет объект Surface, который поддерживает рисование в фоновом потоке и дает возможность использовать OpenGL для трехмерной графики. Это отличный вариант для насыщенных графикой элементов, которые нуждаются в частых обновлениях или должны отображать сложную графическую информацию, как в случае с играми и трехмерной визуализацией.
Основной плюс SurfaceView в том, что он работает в паре с объектом класса Thread, что позволяет выполнять тяжелые графические расчеты в отдельном потоке.
Создадим в нашем проекте еще два класса: MySurfaceView и MyThread.
MySurfaceView:
MyThread:
Не рекомендуется делать REDRAW_TIME слишком маленьким, это может сильно нагружать систему. 10 мс вполне достаточно.
Последний штрих: вернемся в MyActivity и сменим
setContentView ( new MyView ( this ) ) ;
на
setContentView ( new MySurfaceView ( this ) ) ;
Приложение должно выглядеть так:
Источник
Работа с SurfaceView в Android
Здравствуйте, Хабравчане!
При работе с 2D графикой в Android отрисовку можно выполнять используя Canvas. Проще всего это сделать с помощью своего класса, унаследованного от View. Необходимо просто описать метод onDraw(), и использовать предоставленный в качестве параметра canvas для выполнения всех необходимых действий. Однако этот подход имеет свои недостатки. Метод onDraw() вызывается системой. Вручную же можно использовать метод invalidate(), говорящий системе о необходимости перепрорисовки. Но вызов invalidate() не гарантирует незамедлительного вызова метода onDraw(). Поэтому, если нам необходимо постоянно делать отрисовку (например для какой-либо игры), вышеописанный способ вряд ли стоит считать подходящим.
Существует еще один подход — с использованием класса SurfaceView. Почитав официальное руководство и изучив несколько примеров, решил написать небольшую статью на русском, которая возможно поможет кому-то быстрее освоиться с данным способом выполнения отрисовки. Статья рассчитана на новичков. Никаких сложных и хитрых приемов здесь не описано.
Класс SurfaceView
Особенность класса SurfaceView заключается в том, что он предоставляет отдельную область для рисования, действия с которой должны быть вынесены в отдельный поток приложения. Таким образом, приложению не нужно ждать, пока система будет готова к отрисовке всей иерархии view-элементов. Вспомогательный поток может использовать canvas нашего SurfaceView для отрисовки с той скоростью, которая необходима.
Вся реализация сводится к двум основным моментам:
- Создание класса, унаследованного от SurfaceView и реализующего интерфейс SurfaceHolder.Callback
- Создание потока, который будет управлять отрисовкой.
Создание класса
Как было сказано выше, нам потребуется свой класс, расширяющий SurfaceView и реализующий интерфейс SurfaceHolder.Callback. Этот интерфейс предлагает реализовать три метода: surfaceCreated(), surfaceChanged() и surfaceDestroyed(), вызываемые соответственно при создании области для рисования, ее изменении и разрушении.
Работа с полотном для рисования осуществляется не напрямую через созданный нами класс, а с помощью объекта SurfaceHolder. Получить его можно вызовом метода getHolder(). Именно этот объект будет предоставлять нам canvas для отрисовки.
В конструкторе класса получаем объект SurfaceHolder и с помощью метода addCallback() указываем что хотим получать соответствующие обратные вызовы.
В методе surfaceCreated(), как правило, необходимо начинать выполнять отрисовку, а в surfaceDestroyed() наоборот завершать ее. Оставим пока тела этих методов пустыми и реализуем поток, отвечающий за отрисовку.
Реализация потока отрисовки
Создадим класс, унаследованный от Thread. В конструкторе он будет принимать два параметра: SurfaceHolder и Resources для загрузки картинки, которую мы будем рисовать на экране. Также в классе нам понадобится переменная-флаг, указывающая на то, что производится отрисовка и метод для установки этой переменной. Ну и, конечно, потребуется переопределить метод run().
В итоге получим следующий класс:
Для того, чтобы результат не выглядел совсем уж пресно, загруженную картинку увеличим в три раза, и немного сместим к центру экрана. Сделаем это при помощи матрицы преобразований. Также, не чаще чем раз в 30 миллисекунд, будем поворачивать картинку на 2 градуса вокруг ее центра. Само рисование на canvas лучше, конечно, вынести в отдельный метод, но в данном случае мы всего лишь очищаем экран и рисуем изображение. Так что можно оставить как есть.
Начало и завершение отрисовки
Теперь, после того как мы написали поток, управляющий отрисовкой, вернемся к редактированию нашего класса SurfaceView. В методе surfaceCreated() создадим поток и запустим его. А в методе surfaceDestroyed() завершим его работу. В итоге класс MySurfaceView примет следующий вид:
Следует отметить, что создание потока должно выполняться в методе surfaceCreated(). В примере LunarLander из официальной документации создание потока отрисовки происходит в конструкторе класса, унаследованного от SurfaceView. Но при таком подходе может возникнуть ошибка. Если свернуть приложение нажатием клавиши Home на устройстве, а затем снова открыть его, то возникнет исключительная ситуация IllegalThreadStateException.
Activity приложения может выглядеть следующим образом:
Результат работы программы выглядит так (из за вращения изображение немного размазано, но на устройстве выглядит вполне приемлемо):
Источник
Android. Surface
Дисклеймер
Данная статья предназначена для начинающих андроид разработчиков с небольшим опытом работы с видео и/или камерой, особенно тех кто начал разбирать примеры grafika и кому они показались сложными — здесь будет рассмотрен похожий код с упрощенным описанием основных шагов, проиллюстрированных диаграммами состояний.
Почему в заголовке вынесен класс Surface? В android множество классов имеют в своем названии слово Surface (Surface, SurfaceHolder, SurfaceTexture, SurfaceView, GLSurfaceView) они не связаны общей иерархией тем не менее объединены низкоуровневой логикой работы с вывод изображений. Мне показалось разумным использовать его в названии чтобы подчеркнуть попытку раскрытия работы именно с этой частью SDK.
Пример использования с разным API
Попробуем написать следующий пример: будем брать preview с камеры, накладывать на него анимированный drawable, выводить это все на экран и по необходимости записывать в файл. Полный код будет лежать https://github.com/tttzof351/AndroidSurfaceExample/
Для вывода на экраны мы воспользуемся GLSurfaceView, для записи классами MediaCodec и EGLSurface, а с камерой общаться через API V2. Общая схема примерно следующая:
Наложение нескольких Surface
Surface — фактически дескриптор области в памяти, которую нужно заполнить изображением. Скорее всего, мы получаем его пытаясь вывести что-то на экран или в файл, таким образом он работает как буфер для некоторого “процесса” который производит данные.
Чтобы создать наложение из нескольких Surface воспользуемся OpenGL.
Для этого мы создадим две квадратные external-текстуры и получим из них Surface-ы
В коде это будет выглядеть как то так:
XYZ координаты
Теперь нам нужно понять как создать и расположить текстуры, а для этого придется вспомнить как устроена координатная сетка в OpenGL: ее центр совпадает с центром сцены (окна), а границы нормированы т.е от -1 до 1.
На этой сцене мы хотим задать два прямоугольника (работа идет на плоскости поэтому все z координаты логично установлены в 0f) — красным мы обозначим тот куда будем помещать preview для камеры, а синим для анимированного drawable-а:
Выпишем наши координаты явно:
UV координаты
Достаточно ли этого? Оказывается, что нет 🙁
Текстура это отображение картинки на область сцены и чтобы его правильно совершить нужно указать в какое точно место точки на картинке попадут внутри этой области — для этого в OpenGL применяются UV координаты — они выходят из левого нижнего угла и имеют границы от 0 до 1 по каждой из осей.
Работает это следующим образом — каждой вершине нашей области мы зададим UV координаты и будем искать соответствующие точки на изображении, считая что там ширина и высота равны по 1.
Рассмотрим на примере — будем считать что камера отдает нам изображение в перевернутом и отраженном состоянии и при этом мы хотим показать только правую-верхнюю часть т.е взять 0.8 по широты и высоте изображения.
Тонкий момент — на данном этапе мы не знаем соотношения сторон области на экране — у нас есть только квадрат в относительных координатах, который заполнит собой всю сцену и соответственно растянется. Если бы мы делали fullscreen камеру то наши относительные размеры (2 по каждой стороне) растянулись бы до условных 1080×1920. Будем считать что размеры сцены мы зададим такие что их соотношение будет равно соотношению камеры.
Посмотрим куда перейдут координаты — правая верхняя точка нашей области (1, 1, 0) должна перейти в UV координату (0, 0), левая нижняя в (0.8f, 0.8f) и т. д
Таким образом получим соответствие XYZ и UV:
Если соотношение сторон между preview с камеры и областью на экране совпадало изначально то оно очевидным образом продолжит сохранятся т.к в нашем случаи мы просто умножили на 0.8f.
А что будет есть мы зададим значения больше 1? В зависимости от настроек которые мы передали OpenGL-у мы получим точки какой то части изображения. В нашем примере будет повторяться последняя линия по соответствующей оси и мы увидим артефакты в виде “полосок”
Итог: если мы хотим сжать/вырезать изображение сохраняя при этом позицию области на экране то UV координаты наш выбор!
Зададим координаты для наших текстур
Шейдеры
Иметь статичные XYZ и UV-координаты не очень удобно — мы например можем захотеть перемещать и масштабировать жестами наши текстуры. Чтобы их трансформировать заведем две матрицы для каждой текстуры: MVPMatrix и TexMatrix для для XYZ и UV координат соответственно.
Каждая OpenGL2 должна содержать шейдеры для того, чтобы вывести что-то на экран. Конечно, это не там тема которую можно раскрыть в одном абзаце, тем не менее в нашем случае они будут тривиальными, а потому можно быстро понять что они что они делают, без особого знания материала.
Прежде всего шейдера два — vertex и fragment.
Первый (vertex) будет обрабатывает наши вершины, а именно просто перемножать наши XYZ / UV координаты с соответствующими им матрицами и заполнять OpenGL переменную gl_Position которая как раз отвечает за финальное положение нашей текстуры на экране.
Второй (fragment) должен заполнить gl_FragColor пикселями изображения.
Итого имеем: переменные внутри vertex шейдера мы должны заполнить поля нашими данными, а именно:
- MVPMatrix ->uMVPMatrix
- TexMatrix -> uTexMatrix
- наши XYZ координаты вершины ->aPosition
- UV координаты ->aTextureCoord
vTextureCoord — нужна для проброса данных из vertex шейдера в fragment шейдер
В fragment шейдере мы берем преобразованные UV координаты и используем их для отображения пикселей изображения в области текстуры.
Ради справки укажем чем отличаются типы:
- uniform — переменная такого типа будет сохранять значения при многократном вызове, мы используем один шейдер которые вызывается последовательно для двух текстур, так что все равно будем перезаписывать при каждой отрисовки
- attribute — данные такого типа читаются из вершинного буфера, их нужно загружать при каждой отрисовки
- varying — нужны для передачи данных из vertex шейдера в fragment
Как передать параметры в шейдер? Для этого вначале нужно получить id (указатель) переменной:
Теперь по этому id нужно загрузить данные:
Непосредственно отрисовка
После того как мы заполнили наши шейдеры всеми данными мы должны попросить текстуру обновить изображение, а OpenGL отрисовать наши вершины:
В нашем примере мы разобьем работу с OpenGL сценой на два классы — непосредственно сцены и текстуры:
StateMachine / Машина состояний / Конечный автомат
Все API которое мы предполагаем использовать в нашем примере принципиально асинхронное (ну может за исключением анимированного Drawable-а). Мы будем заворачивать такие вызовы в отдельные StateMachine-ы — подходе когда явно выписывают состояния системы, а переходы между ними происходят через отправку событий.
Давайте на простом примере посмотрим как это будет выглядеть, предположим у нас есть такое код:
В целом все хорошо — красиво и компактно, но мы попробуем переписать его в следующим образом:
С одной стороны получилось сильно больше, тем не менее появилось несколько неявных, но полезных свойств: многократное нажатие теперь не приводит к лишним запускам loadImage, хотя и не очевидно с таким объемом, но мы избавились от вложенного вызова колбеков, чем и будем в последствии пользоваться, а еще стиль написания метода transition позволяет построить диаграмму переходов которая один в один повторяет код т.е в нашем случаи:
Серым указаны переходы, которые не выписаны явно. Часто их логируют или кидают исключение, считая признаком ошибки. Мы пока обойдемся простым игнорированием и в дальнейшем не будем указывать на схемах.
Создадим базовый интерфейсы для StateMachine:
GLSurfaceView
Самый простой способ вывести что-то на экран используя OpenGL в android это класс GLSurfaceView — он автоматически создает новые поток для рисования, запуск/пауза которого происходит по методам GLSurfaceView::onResume/onPause.
Для простоты мы будем задавать нашей вьюхе соотношение 16:9
Сам процесс отрисовки вынесен в отдельный колбек — GLSurfaceView.Renderer.
Завернув его в StateMachine-у мы получим что-то вроде этого:
Давайте нарисуем диаграмму переходов:
Теперь наш код пытается что-то выводить на экран, правда пока у него это получается плохо — ни чего кроме черного экрана мы не увидим. Как не сложно догадаться дело в том, что в наши Surface-ы сейчас ни чего не попадает т.к мы пока не реализовали источники изображений. Давайте это исправим — первым делом создадим CanvasDrawable:
Теперь секцию в GLSurfaceMachine мы можем дополнить отрисовкой canvasDrawable на canvas-е которые предоставляет surface у соответствующей текстуры:
После чего увидим что-то наподобие:
Camera API V2
Зеленый прямоугольник это конечно весело и интригующе, но пора попробовать вывести preview с камеры на оставшейся surface.
Давайте выпишем этапы работы с камерой:
- Ожидаем получение permission-а. У нас это будет состояние WaitingStart
- Получаем инстанс camera manager-а, находим логический id (обычно их два — для back и front, а логический он потому что на современных девайсах камера может состоять из множества датчиков) нужной камеры, выбираем подходящий размер, открываем камеру, получаем cameraDevice. Состояние WaitingOpen
- Имея открытую камеру мы обратимся в обратимся за получением Surface-а для вывода изображения. Состояние WaitingSurface
- Теперь имея cameraDevice, Surface мы должны открыть сессию чтобы камера наконец начала передавать данные. Состояние WaitingSession
- Теперь мы можем захватить preview. Состояние StartingPreview
Проиллюстрируем нашу текущую схему:
MediaCodec
MediaCodec класс для низкоуровневой работы с системными кодеками, в общем виде его API это набор input/output буферов (звучит, к сожалению, проще чем работать с ним) в которые помещаются данные (сырые или закодированные зависит от режима работы encoder/decoder), а на выходе мы получаем результат.
Несмотря на то, что к качестве буферов обычно выступают ByteBuffer, для работы с видео можно использовать Surface который вернет нам MediaCodec::createInputSurface, на нем мы должны отрисовывать кадры, которое хотим записать (при таком подходе документация обещает нам ускорение кодирования за счет использования gpu).
Хорошо, теперь мы должны научиться отрисовывать уже существующие Surface-ы которое мы создали в GLSurfaceMachine на Surface от MediaCodec-а. При этом важно помнить: Surface это объект который создает consumer-ом и прочитать что-то из него в общем случаи нельзя т.е нет условного метода getBitmap/readImage/…
Мы поступим следующим образом: на основе существующего GL контекста мы создадим новый который будем иметь общую с ним память, а потому мы сможем использовать переиспользовать там id-шники текстур которые мы создали ранее. Затем используя новый GL контекст и Surface от MediaCodec-а, мы создадим EGLSurface — внеэкранный буфер на котором мы так же сможем создать наш класс OpenGLScene. Затем при каждой отрисовке кадра мы попробуем параллельно записывать кадр на файл.
EGL означает интерфейс взаимодействия OpenGL API с оконной подсистемой платформы, работу с ним мы украдем из grafika. Конвейер (EncoderHelper) с MediaCodec-ом напрямую описывать тоже не буду, приведу лишь итоговую схему взаимодействия наших компонентов:
Источник