четверг, 27 октября 2011 г.

Android. ImageView с поддержкой SVG


English translation

Source Code

Эта моя стать изначально была опубликована на сайте The Code Project. Оригинал Codeproject.

Введение

Как вы знаете, Андроид не поддерживает формат SVG. Однако у него есть масса преимуществ. Во первых - масштабируемость. Отпадает необходимость держать картинки в разных разрешениях. Нет необходимости, к примеру, масштабировать битмэп с потерей качества. SVG может быть отмасштабирована в любое разрешение и качество картинки будет тем же. Во-вторых, SVG - это обычный xml-файл, что в итоге размер файла намного меньше, чем та же картинка в растровом формате. Более того, картинку сожно изменять "на лету". SVG файл можно открыть в обычном текстовом редакторе и помотреть, как картинка формируется. И так далее... Но, так как, к сожалению, Android не поддерживает SVG, то придется немного повозиться с NDK. Хорошая новоость заключается в том, что возни будет немного. У нас имеются готовые библиотеки с открытым исходным кодом для растеризации SVG.

В интернете достаточно уроков о том, как работать с NDK. Не будем повторяться. Единственно, немного полезных замечаний:

  • Во-первых, необходим эклипс. Загрузить можно здесь: [1].
  • Или, как альтернативу, можно использовать [2]. Раньше я предпочитал ее, но в последнее время она без бубна работает не очень...
  • После установки эклипса, в нем нужно установить CDT плагин.
  • ADT плагин [3].
  • После этого, добавляем Eclipse Sequoyah[4] плагин, чтобы дебажить c/c++ код.
  • Для пользователей Windows необходимо установить cygwin[5] (Добавляем Cygwin/bin в PATH. В эклипсе - cinfigure build path, и устанавливаем build command в что-то типа "bash C:\AndroidNDK\ndk-build").
  • Android SDK [6].
  • Android NDK. Используем CrystaX NDK[7]. (В текущей версии NDK (6) от Google Android NDK libsvg собрать не удалось)
  • В настройках эклипса указываем пути к NDK и SDKIn
 Сначала используем библиотеку android-libsvg [8]. Кроме того, она зависит от libsvg, libpng, libjpeg, libexpat и zlib. Ее преимущество в том, что она поддерживает почти все фичи SVG. Создаем папку android-libsvg где-нибудь в файловой системе и в консоли (или cygwin) выполняем “bzr branch lp:libsvg-android”. Bazaar загрузит исходники.

Ok. Теперь создаем новый проект “ImageViewSvg”. В контекстном меню проекта выбираем AndroidTools/Add Native support. К проекту добавится папка “jni”. Удаляем из нее все и копируем содержимое папки “jni” из android-libsvg . Обновляем папку jni нашего проекта(F5). Посмотрим на файл Android.mk в папке “jni”. Что означают некоторые переменные:


  • LOCAL_PATH := $(call my-dir) – my-dyr макрос устанавливает LOCAL_PATH переменную, которая указывает где находятся файлы исходников(в текущем каталоге)
  • include $(CLEAR_VARS) – Очищает вс локальные переменные
  • LOCAL_MODULE – Имя библиотеки
  • LOCAL_CFLAGS – Устанавливает флаги компилятора пути к "include" файлам
  • LIBJPEG_SOURCES, … - Список всех файлов исходников, для каждой библиотеки, которая будет использоваться
  • LOCAL_LDLIBS – Линки к дополнительным библиотекам
  • LOCAL_SRC_FILES – Список всех исходников для всех библиотек
  • BUILD_SHARED_LIBRARY – Линк к mk файлу, который построит "shared" библиотку 


Более подробно смотрите файлANDROID-MK.TXT в NDK.
Иногда в WIndows, после рестарта эклипса, он не может запустить ndk-build. Необходимо переустановить "build command" в крнфигурации.

Далее создаем com.toolkits.libsvgandroid и копируем туда SvgRaster.java из проекта libsvg-android . Все практически готово.


Для поддержки классом ImageView SVG формата, необходимо перегрузить в нем некоторые методы. Но еще хотелось бы использовать стандартный атрибут android:src как SVG файл и устанавливать его из стандартной папки“drawable” вместо папки “raw”. Вначале изменим конструктор. Для доступа к android:src атрибуту, добавляем attrs.xml в res/values:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="ImageViewSvg">
  <attr name="android:src"/>
 </declare-styleable>
</resources>
 
Посмотрим на исходники конструктора класса ImageView . Имеем следующий код:

Drawable d = 
   a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
    setImageDrawable(d);
}
 



Далее посмотрим на метод setImageBitmap . Он просто вызывает setImageDrawable. Т.о. мы можем использовать его, если у нас будет подходящий битмэп. Правда еще необходимо достать файл из “drawable”. Ну, здесь - как два пальца об асфальт - – достаем ID ресурса из android:src атрибута и читаем его в поток. Далее, при помощи, libandroidsvg парсим SVG:

В итоге конструктор выглядит так:

public ImageViewSvg(Context context, AttributeSet attrs, int defStyle) {

  // Let's try load supported by ImageView formats
  super(context, attrs, defStyle);
        
        if(this.getDrawable() == null)
        {
         //  Get defined attributes
            TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.ImageViewSvg, defStyle, 0);
                        
            // Getting a file name
            CharSequence cs = 
                a.getText(R.styleable.ImageViewSvg_android_src);
            String file = cs.toString();
            
            // Is it SVG file?
            if (file.endsWith(".svg")) {
             
             // Retrieve ID of the resource
                int id = 
                    a.getResourceId(
                    R.styleable.ImageViewSvg_android_src, -1);
                if(id != -1){
                try {
                  // Get the input stream for the raw resource
                  InputStream inStream = 
                                     getResources().openRawResource(id);
                  int size = inStream.available();
                  
                  // Read into the buffer
                  byte[] buffer = new byte[size];
                  inStream.read(buffer);
      inStream.close();
      
      // And make a string
      mSvgContent = 
      EncodingUtils.getString
       (buffer, "UTF-8");
      
      // Parse it
               mSvgId = SvgRaster.svgAndroidCreate();
               SvgRaster.svgAndroidParseBuffer
     (mSvgId, mSvgContent.toString());
               SvgRaster.svgAndroidSetAntialiasing(mSvgId, true);
                                                
               mIsSvg = true; 

      
     } catch (IOException e) {
      mIsSvg = false;
      e.printStackTrace();
     }                 
                }
            }
        }
}
 
Другая проблема в том, что в SVG не указан его размер(Не всегда, в некоторых указан желаемый. См. Пы.Сы.). Более тогоImageView параметры лэйаута могут быть установлены в wrap_content, fill_parent , или конкретные размеры. Используем метод onSizeChanged . Единственная проблема - атрибут wrap_content . В этом случае размер будет 0. Нуобходимо будет заменить wrap_content наfill_parent на лету. К сожалению, это ничего не даст. Если взглянуть на исходники, то будет видно, что родительски лэйаут вытаскивает параметры прямо из атрибутов и вызывает метод setLayoutParams . Перегрузим его:

@Override 
public void setLayoutParams(ViewGroup.LayoutParams params){
 if(mIsSvg)
 {
  // replace WRAP_CONTENT if needed
  if(params.width == ViewGroup.LayoutParams.WRAP_CONTENT
    && getSuggestedMinimumWidth() == 0)
   params.width = ViewGroup.LayoutParams.FILL_PARENT;
  if(params.height == ViewGroup.LayoutParams.WRAP_CONTENT
    && getSuggestedMinimumHeight() == 0)
   params.height = ViewGroup.LayoutParams.FILL_PARENT;
 }
 super.setLayoutParams(params);
}
 
Также onSizeChanged:

@Override 
public void onSizeChanged(int w, int h, int ow, int oh){
 if(mIsSvg){
  //Create the bitmap to raster svg to
   Canvas canvas = new Canvas();
  mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
  canvas.setBitmap(mBitmap);
  // Render SVG with use of libandroidsvg
  SvgRaster.svgAndroidRenderToArea(
   mSvgId, canvas,
   0, 0, canvas.getWidth(), canvas.getHeight()); 
  this.setImageBitmap(mBitmap);
 }
 else
  super.onSizeChanged(w, h, ow, oh);
}
 


И, в конце-концов, настала пора опробовать то, что получилось. Создадим такой лэйаут:

<?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:background="#AA0000"
  android:layout_height="fill_parent" 
  android:layout_width="fill_parent"
  android:layout_weight="1.0" 
  android:gravity="center"
  >
  <com.imageviewsvg.controls.ImageViewSvg
   android:src="@drawable/lion" 
   android:layout_width="100dip"
   android:layout_height="100dip" 
   android:id="@+id/svgview"
   android:layout_gravity="center" 
   />
 </LinearLayout>
 

Запускаемся:




Отладка

Для отладки с/с++ кода следуйте инструкциям Кароса Соунто: [9]. Сначала там не все ясно. Несколько советов:
  • При настройке C++ конфигурации, приложение действительно должно быть app_process, не обращаем внимания на жалобы эклипса, что такого файла нет, Он будет создан позже.
  • Необходимо каждый раз запускать ndk-gdb при запуске приложения. Иногда команда должна быть ndk-gdb –adb=/tools/adb –force.
  • Не забываем ставить "debuggable" в манифесте.

Другой подход. Anti Grain Geometry.

Это еще одна библиотека для растеризации SVG :[10]. Единственнаая дополнительная к ней библиотека - libexpat. Она уже есть в проекте. В папке jni создаем еще одну:



Копируем соответствующие файлы из исходников agg в gpc/include/src. Там, в папке examples есть папкаsvg_viewer. Копируем из нее все, за исключением svg_test в папкуaggsvg jni . Единственно, эта библиотека поддерживает только простые SVG. Необходимо будет дописывать парсер. В папке aggsvg-android , создаем файл aggsvgandroid.cpp. Пример парсит SVG из файловой системы. Для строки, добавляем следующий метод в класс parser :

void parser::parse(const char *chars, int length){

     char msg[1024];

     XML_Parser p = XML_ParserCreate(NULL);
     if(p == 0)
     {
      throw exception("Couldn't allocate memory for parser");
     }

     XML_SetParamEntityParsing(p, XML_PARAM_ENTITY_PARSING_ALWAYS);
     XML_UseForeignDTD(p, true);

     XML_SetUserData(p, this);
     XML_SetElementHandler(p, start_element, end_element);
     XML_SetCharacterDataHandler(p, content);

     int done = 0;
     std::string str = std::string(chars);
     std::istringstream inputString(str);

     while(true){
      if(done)
       break;
            size_t len = inputString.readsome(m_buf, buf_size);
            done = len < buf_size;
            if(!XML_Parse(p, m_buf, len, done))
            {
                sprintf(msg,
                    "%s at line %d\n",
                    XML_ErrorString(XML_GetErrorCode(p)),
                    (int)XML_GetCurrentLineNumber(p));
                throw exception(msg);
            }
     }
        XML_ParserFree(p);

        char* ts = m_title;
        while(*ts)
        {
            if(*ts < ' ') *ts = ' ';
            ++ts;
        }
    }
 

В конце файла Android.mk , добавляем секцию для построения
еще одной библиотеки. Все довольно просто. ПРосто очищаем переменные после построения первой
библиотеки и настраиваем их для построения второй.
Класс для растеризации с использованием AGG:

class SvgRasterizer{
 agg::svg::path_renderer m_path;
    double m_min_x;
    double m_min_y;
    double m_max_x;
    double m_max_y;
    double m_x;
    double m_y;
    pix_format_e pixformat;
 agg::rendering_buffer m_rbuf_window;

public:
 SvgRasterizer(pix_format_e format, uint32_t width, 
   uint32_t height, void *pixels) : \
  m_path(), \
  m_min_x(0.0), \
  m_min_y(0.0), \
  m_max_x(0.0), \
  m_max_y(0.0), \
  pixformat(format)
 {
  m_rbuf_window.attach((unsigned char*)pixels, width, height, 4*width);
 }

 void parse_svg(const char* svg, int length){
  // Create parser
  agg::svg::parser p(m_path);
  // Parse SVG
  p.parse(svg, length);
  // Make all polygons CCW-oriented
  m_path.arrange_orientations();
  // Get bounds of the image defined in SVG
        m_path.bounding_rect(&m_min_x, &m_min_y, &m_max_x, &m_max_y);
 }

 void rasterize_svg()
 {
  typedef agg::pixfmt_rgba32 pixfmt;
  typedef agg::renderer_base<pixfmt> renderer_base;
  typedef agg::renderer_scanline_aa_solid<renderer_base> renderer_solid;

        pixfmt pixf(m_rbuf_window);
        renderer_base rb(pixf);
        renderer_solid ren(rb);

        agg::rasterizer_scanline_aa<> ras;
        agg::scanline_p8 sl;
        agg::trans_affine mtx;

        double scl;
        // Calculate the scale the image to fit given bitmap
        if(m_max_y > m_max_x)
         scl = pixf.height()/m_max_y;
        else
         scl = pixf.width()/m_max_x;

        // Default gamma as is
        ras.gamma(agg::gamma_power(1.0));
        mtx *= agg::trans_affine_scaling(scl);

        m_path.expand(0.0);

        // Render image
        m_path.render(ras, sl, ren, mtx, rb.clip_box(), 1.0);

        ras.gamma(agg::gamma_none());
 }
};
 

В исходниках я добавил возможность протестировать обе библиотеки:




Заключение

Итак, имеется по меньшей мере два способа использования SVG в Андроиде. Основное преимущество libsvg-android в том, что она готова к использования, но более чем в три раза медленнее AGG, в которой сы вынуждены дорабатывать парсер. Помимо того, в AGG имеется масса возможностей для работы с изображениями. Я показал лишь как можно использовать ImageView в лэйауте, для программного использования необходимо будет еще перегрузить методы такие как setImageResource , к примеру.

На данный момент все, спасибо за внимание!


Ресурсы

  1. http://www.eclipse.org
  2. http://developer.motorola.com/docstools/motodevstudio
  3. http://developer.android.com/guide/developing/tools/adt.html
  4. http://www.eclipse.org/sequoyah/
  5. http://www.cygwin.com/
  6. http://developer.android.com/sdk/index.html
  7. http://www.crystax.net/en/android/ndk/6
  8. https://launchpad.net/libsvg-android
  9. http://www.eclipse.org/sequoyah/documentation/native_debug.php
  10. http://www.antigrain.com/



P.S. На Code Project вы также можете найти статью об использовании SVG как Drawable:
Drawable with SVG Support

Известные проблемы:

ibsvg-android не поддерживает представление цвета в виде rgb(255, 255, 255) (Inkscape). (По крайней мере на момент написания статьи.)
Проверьте файл и замените на fill: #ffffff

Возможно что-то еще. Проверяйте структуру SVG перед использованием.
Неподдреживаемые фичи будут выявляться только при отладке.


Комментариев нет:

Отправить комментарий