Используем векторные изображения SVG в приложениях Android, или как убить фрагментацию экранов и не потерять в качестве (плюсы, минусы, особенности)

    Достаточно долгое время мы занимаемся разработкой детских приложений под Android, постепенно постигая множество нюансов этой платформы. Есть одни грабли, которые подстерегают нас в каждом приложении, – это фрагментация экранов. Если делать одно изображение только под телефон маленького размера, то на планшете оно выглядит мягко говоря “не очень”. А если делать изображение высокого разрешения для планшетов и пытаться использовать его на телефонах, то с очень большой вероятность приложение вывалится с OutOfMemory.

    Приходится готовить несколько экземляров одного и того же изображения под разные экраны. Еще сильнее облака сгущает новый монстр Galaxy Nexus 10 с безумным разрешением 2560х1600.



    В общем, неплохо бы что-то изменить, решили мы. А что если использовать в приложениях не растровые изображения, а векторные? Такие изображения легко масштабируются под разные разрешения экранов, при этом не теряя в качестве. Можно использовать всего одно изображение под разные разрешения и размеры.

    Сказано — сделано. Итак, под катом история внедрения векторных изображений в одно из наших приложений. В статье мы поделимся опытом и особенностями использования векторных изображений в формате SVG в приложениях Android.

    Немного погуглив, выяснили, что векторные изображения для web и приложений обычно используются в формате SVG. С данным форматом работают векторные редакторы Adobe Illustrator и Inkscape. Вооружившись Inkscape, нарезали пробных картинок и принялись искать способы их загрузки и отображения в приложении Android.
    Разбираться с устройством формата SVG и писать свой парсер не хотелось — наверняка же люди сталкивались с этим и до нас! Что ж, гуглим «android svg».
    В итоге есть:

    Берем самый популярный — SVG-Android (он, кстати, переехал на Github, но новых коммитов там нет). Подключаем библиотеку, векторное изображение помещаем в res/raw, загружаем её и устанавливаем ее во вьюшку:
    SVG svg = SVGParser.getSVGFromResource(getResources(), R.raw.filename);
    Drawable drawable = svg.createPictureDrawable();
    imageView.setImageDrawable(drawable);
    

    Загружаем тестовый проект с изображениями — всё отлично! Подключаем наши изображения — пусто. Как оказалось, данная библиотека поддерживает только формат SVG basic 1.1, который не поддерживается Inkspace, а рождается только в Adobe Illustrator.

    Пробуем вторую библиотеку SVG-Android-2, которая является форком первого проекта и ушла чуть-чуть дальше.
    Она уже понимает Inkscape, а также поддерживает другие фишки этого формата, о чем можно почитать тут. Здесь всё пошло проще, картинки загрузились и выглядели шикарно и на телефоне, и на планшете. На нем мы и остановились.

    Пример SVG-изображения и неадаптированного по размеру под планшет PNG-изображения на планшете.
    (просмотреть изображение в оригинальном размере 1280х800)

    Первое — SVG (10 Кб), второе — PNG (22 Кб). Второе изображение имеет размытый контур и ступенчатый градиент

    Масштабирование изображений


    Изображения масштабируются только с сохранением пропорций. Поэтому использовать их в качестве фона, который может немного менять пропорции на разных разрешениях, не получится. В этом случае мы обычно делаем какое-то абстрактное изображение в PNG, которое довольно легко меняет свои пропорции для разных экранов.

    Не забываем для SVG устанавливать свойство аdjustViewBounds в значение true, иначе изображение может рассчитывать свои границы не так, как вы задумали.

    Размер изображений с тенями и подсветками


    Некоторые элементы в нашем приложении изначально были отрисованы с небольшими тенями и подстветками — например, этот смайлик имеет серую подсветку сзади. Но это приводит к колоссальному увеличению размера файла SVG. 118 Кб против 1 Кб без этой подсветки. Чем больше размер файла — тем больше времени надо на его загрузку в программе, поэтому мы решили отказаться от этого эффекта.


    Изорбражения с тенью и без: 118 Кб vs 1 Кб

    Подсветку можно отключить или в графическом редакторе, или же прямым редактированием SVG-файла — удаляем тэг <image /> с огромным содержимым.

    Отображение градиентов


    На некоторых изображениях вдруг обнаружились черные пятна вместо фона. Оказалось, что градиент не поддерживается!
    Проблема с градиентами решилась удалением лишних тэгов из svg (описано далее в статье). Но в принципе, и с этим можно было бы жить и в наших простых изображениях заменить градиент однородной заливкой, если бы не другой нюанс — значительное время загрузки изображений.

    Вот как это выглядело на экране: слева — черное небо в виде градиента, справа — корректная картинка.


    Время загрузки изображений


    В приложении нужно было по 6 изображений на одной странице ViewPager, а поскольку они подгружаются в процессе прокрутки (если не кэшировать), интерфейс заметно дергался при скроллинге. Этого очень не хотелось, и было решено загружать все изображения при старте приложения. Получили время инициализации порядка 8 секунд, что было слишком долго.

    В проблеме решили разобраться. Выкачали исходники проекта SVG-Android-2 и стали искать, что именно так тормозит. Оказалось, что в классе SVGParser XML-файл изображения парсится дважды: первый раз он собирает информацию о дополнительных атрибутах, которые используются при втором проходе. И, что самое интересное, — анализируется лишь атрибут xlink:href, который является некоторым подобием гиперссылок внутри самого документа. В наших проблемных изображениях как раз были такие ссылки, и вели они никуда. После того, как мы избавились от данных ссылок, отредактировав код SVG в некоторых изображениях, градиент стал корректно отображаться. Более того, убрав этот предварительный проход и немного оптимизирорав процесс загрузки, мы смогли уменьшить скорость загрузки с 8 секунд до 1,8-2. Следует заметить, что это соизмеримо с PNG среднего размера — загрузка этих же изображений в память заняла 1,7 секунд.

    Ниже приведено сравнение загрузки 35 файлов в формате SVG и PNG.
    SVG PNG(~500x500)
    Размер, КБ 327 943
    Время загрузки, с 1,9 1,7


    Прозрачность и цветовые фильтры


    Часто в играх мы используем полупрозрачные картинки для неактивных элементов. В этом проекте помимо прозрачности нужны были цветовые фильтры для генерации элементов в играх, то есть чтобы один элемент можно было использовать один раз, но, раскрашивая его по-разному, мы получали бы разные элементы.

    Оказалось, что ни alpha, ни colorFilter мы применить не сможем, т.к. библиотека загружает не типичные bitmapDrawable, а pictureDrawable, и в исходниках Android мы видим пустые методы для этого класса:
    @Override
    public void setColorFilter(ColorFilter colorFilter) {}
    @Override
    public void setAlpha(int alpha) {}
    

    До этого с классом pictureDrawable никогда не сталкивались, и это было большой неожиданностью.

    Опять покопавшись в исходниках библиотеки, мы нашли в классе SVGHandler поле fillPaint типа Paint, которым рисуются все компоненты. Если до загрузки элемента ему установить colorFilter, то он будет работать как положено. Нас это вполне устраивало, поэтому мы чуть-чуть изменили метод загрузки SVG, добавив возможность передавать туда цвет фильтра, который при необходимости устанавливается перед загрузкой изображения. Теперь изображения загружались так:
    SVG svg = SVGParser.getSVGFromResource(getResources(), rawSvgId, filterColor);
    

    А в самом SVGHandler появился такой метод:
    public void setFilterColor(int filterColor) {
            fillPaint.setColorFilter(new PorterDuffColorFilter(filterColor, Mode.MULTIPLY));
     }
    

    В итоге мы смогли получать из одной картинки сколько угодно изображений разных оттенков.

    Также можно установить и Alpha для fillPaint, но в играх это свойство требуется в динамической форме (нажали на элемент — сделался полупрозрачным), и подгружать каждый раз новое изображение неудобно. Поэтому этот эффект заменили масштабированием (нажали — элемент уменьшился).

    Нюанс с принудительной обработкой GPU


    После запуска приложения к нам стали такие поступать ошибки:
    java.lang.UnsupportedOperationException
          at android.view.GLES20Canvas.drawPicture(GLES20Canvas.java:895)
          at android.graphics.drawable.PictureDrawable.draw(PictureDrawable.java:73)
    

    Оказалось, что если на устройстве включена настройка “Принудительная обработка GPU” (Developer options — Force GPU Rendering), то наше приложение валится, т.к. метод drawPicture() у Canvas не поддерживает аппаратное ускорение. Об этом можно почитать на android developer.
    Причем простое указание в манифесте android:hardwareAccelerated=«false» проблему не решает — пользовательская галочка в настройках имеет более высокий приоритет.

    Было найдено довольно простое решение: для всех view, которые работают с нашими pictureDrawable, полученными из SVG, отключить аппаратное ускорение.
    Так как функция аппаратного ускорения появилась в Аndroid 3.0 (api 11), то для работы с этим функционалом пришлось изменить target sdk нашего проекта с 8 на 11. И, конечно же, надо помнить про обратную совместимость — на более ранних платформах этих методов нет.
    public static void setSoftwareLayerType(View view) {
           try {
             view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
           } catch (NoSuchMethodError e) {
               //Do nothing - this happens on API < 11
           }
       }
    


    Выводы


    Давайте подведем краткий итог работы с векторными изображениями в формате SVG в Android.

    Плюсы:
    • Один огромный плюс, из которого следуют все остальные, — это одна векторная картинка.
    • Так как картинка векторная, она отлично отображается на всех размерах экранов.
    • Размер SVG-картинок мал.
    • Одна картинка используется несколько раз для разных разрешений.
    • Сокращается процесс подготовки изображений для приложения.

    Минусы:
    • Картинки масштабируются только пропорционально.
    • Не поддерживается прозрачность.
    • Графику нужно упрощать — чем больше векторных элементов, тем больше весит файл.Нежелательно использовать тени и свечения, так как это в разы увеличивает размер SVG-файлов.

    В результате экспериментов с SVG родилось приложение для детей “Учим формы и фигуры”. Ознакомится с приложением можно в Google Play:
    play.google.com/store/apps/details?id=com.whisperarts.kids.forms
    Количество получившихся изображений:
    • PNG — 3 (сплэшскрин и 2 фона для меню);
    • SVG-элементов — 97;
    • Размер приложения 3,5 Мб.

    В сравнении с почти похожим по функционалу нашим приложением “Учим цвета” (размер которого 8 Мб) выигрыш более 50% налицо.

    Для себя мы приняли решение использовать SVG-изображения в наших приложениях, так как это существенно ускоряет процесс разработки и адаптации картинок под разные разрешения экранов, а также существенно уменьшает вес приложения.

    Надеемся, опыт, которым мы поделились в статье, поможет вам также пересмотреть процесс подготовки изображений для приложений и задуматься над использованием формата SVG.

    PS: Если вы уже использовали SVG в своих проектах или по другому обходили проблемы, с которыми столкнулись мы при использовании SVG — пишите в комментариях. Будем рады услышать Ваш опыт.
    WebCanape
    Приводим клиентов малому бизнесу. Быстро и много.
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 23

      +3
      >>Оказалось, что градиент не поддерживается!
      При этом выше у вас нарисован скат с градиентом.
      И какие-то минусы у вас неоднозначные.
      >>Картинки масштабируются только пропорционально.
      Почему?
      >>Не поддерживается прозрачность.
      Это еще почему? Сам по себе svg поддерживает прозрачность. Проблема конкретного выбранного отрисовщика? Или прозрачность в загруженных PNG файлах?
      >>Нежелательно использовать тени и сечения, так как это в разы увеличивает размер SVG-файлов.
      Погодите, что-то не сходится. Во первых для свечения можно было применять градиент, во-вторых — фильтры.

      Как дела с производительностью, не просела?
        +2
        Проблема с градиентом была решена, об этом в статье рассказывается. Скат показывает уже результат

        Проблема масштабирования и альфа — это проблема данной конкретной реализации библиотеки, но так как альтернатив нет — мы использовали её.

        Спасибо за подсказку по поводу свечений. Мы описали то, с чем столкнулись. Художник сделал нам изображение с тенью в таком виде, и мы с ним работали

        На производительности использование svg практически не сказалась. Проблема долгой загрузки и её решение описаны в статье
          0
          Дефолтное свечение в иллюстраторе — растровая картинка.
      • НЛО прилетело и опубликовало эту надпись здесь
          +1
          Спасибо за подсказки, будем пробовать. О результатах напишем. Как я написал в комментрии выше, у нас изображения пришли готовые от художника, и использовали то, что имели.
          +5
          Какое устройство? Если у вас 4 гигалошади под капотом, то загрузка и ресайзы кучи PNG размерами в 3-4 мегапикселя будут идти те же несколько секунд.

          P.S. Правки SVG парсера неплохо бы выложить на github, все-таки ведь GPL лицензия.
            0
            А разве GPL совместима с гугловским маркетом?
            +1
            Спасибо за статью, может когда пригодится.

            А вы изменения после оптимизации не коммитнули? Не перенесли проект на github?
              +1
              Это был наш первый опыт с этой библиотекой, и изменения носят очень локальный характер для сугубо наших целей. Но мы и дальше будем использовать эту библиотеку, и тогда уже будем рады поделиться нашими наработками.
                +4
                Локальные изменения можно не выкладывать только если вы не распространяете приложение или компонент. Похоже в вашем случае факт распространения имеет место быть. А соответственно нужно или влить изменения в исходный проект, или выложить изменённую версию.
                P.S. Заранее извиняюсь, если что-то напутал.
              +1
              Здорово, надо будет ваш метод попробовать.
              Сам я уже пробовал использовать AndEngineSVGTextureRegionExtension, вроде бы как получалось, но так много фишек не реализовано.
              Я рисовал сам графику в Inkscape, но потом еще немного оптимизировал код svg (там есть много мусора, который ни на что не влияет). Более простые элементы типа кнопок, бордюров так вообще мне было проще написать ручками :)
                +1
                Я начал подходить к этой же задаче несколько с другой стороны — генерировать ресурсы из SVG при сборке. Это, конечно, ограничивает область применения (во время выполнения программы нельзя изменять размеры/цвета/...), но зато и проблем с отображением меньше, так как для растеризации используется достаточно мощная библиотека Batik.

                Проект доступен на https://github.com/kriomant/buketan
                Пока возможностей не слишком много, но если кого заинтересует, то продолжу развитие.
                  0
                  Интересовался подобной темой, но отрисовка SVG занимает больше времени и сжирает больше ресурсорв (батарея садится быстрее). Имхо, проще брать изображения высокого разрешения, а при первом запуске ресайзить и сохранять на SD под активное разрешение.
                    –1
                    Может взять SVG-изображение, отрендерить в png (чтобы в качестве не терять) под текущее устройство и сохранить на sd? Или это сложно? Я сам в этой теме не селен.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      +1
                      Речь шла именно о формате svg basic 1.1, именно его понимает первая версия библиотеки и кантинки из inkscape, увы, не отображались. Во второй это исправили и всё пошло хорошо.

                      Вы правильно говорите — все изображения от художника пришли именно из Adobe Illustrator. Обязательно учтем ваши советы по поводу подсветки на будущее, и будем экспериментировать!

                      Попоробовали добавить этот тэг — к сожалению, не помогло. Видимо это пока не поддерживается на уровне библиотеки. Возможно попробуем сами в будущем расширить этот функционал. Спасибо за подсказки!
                      +2
                      Как вы думаете, а почему Google ввел именно механизм разделения ресурсов для разных DPI? Почему они (в Google) не написали библиотеку изначально в фреймворке которая берет все из SVG? Скорее всего от того что это меньшее зло (производительность выше). И наверное возможности у PNG как формата передачи изображения, все же выше =), потом еще найнпач, очень удобно.

                      А если вам приходится масштабировать элементы интерфейса, так это скорее проблема дизайна приложения.

                      В любом случае, спасибо за «наводку».
                        0
                        есть еще вариант использовать иконочные шрифты, это вроде как проще и производительнее
                        • НЛО прилетело и опубликовало эту надпись здесь
                          +1
                          делали что то подобное, у svg формата есть проблема:
                          — это слабый набор инструментария/библиотек, код буквально приходится «допиливать», это усложняет внесение каких-либо правок. Например:
                          желаете трансформации по применять — писать скрипт,
                          мусор после экспорта — чистить, одними скриптами не обойтись,
                          а качество и количество библиотек оставляет желать лучшего.
                          Процесс оптимизации можно слегка автоматизировать — мы используем это: svgo.
                            +1
                            Когда только изучал разработку под Android, сильно удивился, что нет штатного средства для работы с векторной графикой (при наличии неудобного спецэфичного редактора интерфейса). И, видимо, ничего не изменилось.

                            Автору и всем остальным, кто выкладывал ссылки на инструменты, с которыми приходилась работать, огромное спасибо! Сейчас все больше поглядываю на Android в плане разработки.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                +2
                                Спасибо за подсказку с Build.VERSION — что-то совсем забыл про это.

                                По поводу target sdk — у нас детское приложение, и нативных элементов там практически нет. Так что это не критично, а вот охватить как можно больше устройств — это предпочтительно, поэтому min у нас даже на 7 стоит.

                                Исходников нам нисколько не жалко, что вы! Просто как я писал выше — изменения довольно специфичны для нас. Ну кому еще может понадобится загружать изображение с заранее наложенным цветовым фильтром? (в статье кстати описано, как мы это сделали). Вот если бы делать такое с уже загруженными… Вот как сделаем — зальем!

                                Ну а по поводу ускорения — класс SVGParser, метод parse. Убираем первый проход парсера:
                                //			IDHandler idHandler = new IDHandler();
                                //			xr.setContentHandler(idHandler);
                                //			xr.parse(new InputSource(cin.getCopy()));
                                //			svgHandler.idXml = idHandler.idXml;
                                

                                Но тут надо быть осторожным. Это сейчас он работает только с xlink(я писал в статье), я боюсь, что дальше библиотека будет расширена для поддержки других фич.

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

                              Самое читаемое