Wednesday, October 26, 2011

Android. ImageView with SVG Support


Русский перевод


Source Code


This my article was originally published on The Code Project web site. You can see it at Codeproject.

Introduction

As you know, Android doesn’t support SVG format. But benefits of SVG are obvious. First, it’s scalable. You don’t need to have pictures in different resolutions, no need to scale, for example, bitmap image with a quality loss. SVG image can be scaled to any resolution and the quality will be the same. Second, SVG is an ordinary XML file, so its size can be much lesser than the raster format file of the same picture. Moreover, you can change a picture on the fly due to this feature. You can open an SVG file in an ordinary text editor and look how it's composed. And so on… But as Android doesn’t deal with SVG, it will imply some native coding. The good news is that there won’t be a lot of native coding. There are some open source libraries for parsing and rasterizing SVG format.

There are a lot of tutorials how to begin the native development for the Android platform, so I won’t repeat all of them here. I will just give some useful tips.

  • First, you need the Eclipse IDE. You can download it at [1].
  • Or as an alternative, you can use Motodev Studio [2]. I prefer the second one as it has some delicious features. BTW, you can install Motodev Studio as a plugin for the Eclipse IDE. I couldn’t setup it on OpenSUSE, but as the plugin it works fine.
  • Once Eclipse is installed, add CDT plugin to it.
  • Add Android plugin [3].
  • After that, add Eclipse Sequoyah[4] plugin needed for the native debugging. But make sure you installed CDT before Sequoyah. As the Sequoyah project claims, it installs all dependencies it didn’t really install CDT in my case. While installing Sequoyah, make sure you unchecked “Group Items by Category” and checked “Sequoyah Android Native Support”.
  • Also Windows users will need cygwin[5] (Add the Cygwin/bin path to your system. Also configure build path - set build command to something like "bash C:\AndroidNDK\ndk-build"). While installing it, setup development tools.
  • Download Android SDK [6].
  • And at last, you will need Android NDK. Download CrystaX NDK[7]. It has support for C++ exceptions, RTTI and Standard C++ Library. (Current(6) Google Android NDK supports them but can't build libsvg)
  • In Eclipse preferences, set Android SDK & NDK locations. That’s all for now. 
 For the first approach, I will use android-libsvg [8] library. Really it depends on libsvg, libpng, libjpeg, libexpat and zlib. But at this moment, it has support for almost all features of SVG format. To get its sources, create android-libsvg folder somewhere in a file system and go to this folder in console (in cygwin for windows users) and run “bzr branch lp:libsvg-android” command. Bazaar will download sources to this folder.

Ok. Create a new Android project “ImageViewSvg”. Now right click on the project and go to AndroidTools/Add Native support. It will create “jni” folder in the project. Delete all stuff from it and copy contents of “jni” folder of android-libsvg project. Refresh jni folder in the project. Let’s look at Android.mk file in “jni” folder. I'll explain some variables:


  • LOCAL_PATH := $(call my-dir) – my-dyr macro sets LOCAL_PATH variable used to locate source files into current directory
  • include $(CLEAR_VARS) – clears all local variables
  • LOCAL_MODULE – the name of the library
  • LOCAL_CFLAGS – sets compiler flags and some include directories
  • LIBJPEG_SOURCES, … - the list of sources files for each library that will be used
  • LOCAL_LDLIBS – links to additional libraries
  • LOCAL_SRC_FILES – the list of all source files to be compiled, here it contains all sources for all libraries
  • BUILD_SHARED_LIBRARY – link to mk file to build shared library 


For more information, please see ANDROID-MK.TXT in NDK.
Sometimes on Windows, after restarting IDE, it can’t run ndk-build. Then right click the project and go to “Build Path/Configure Build Path” and change “Build command” to something like “bash /cygdrive/c/ndk/ndk-build”.

Next create com.toolkits.libsvgandroid package and copy there SvgRaster.java from the libsvg-android project. Preparations are over.


To make ImageView class to support SVG format, it’s enough to inherit from it and override some methods. But I want it to set standard android:src attribute as SVG file and to be able to pick the file from standard “drawable” folder vs. “raw” folder. At the beginning, let’s make all logic to be done in the constructor. To get access to the android:src attribute, add attrs.xml file to the res/values folder:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="ImageViewSvg">
  <attr name="android:src"/>
 </declare-styleable>
</resources>
 
Let’s look at ImageView class constructor sources. It contains the following code:

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



And look at setImageBitmap method. It just calls setImageDrawable. So we can use it in our constructor if we have the corresponding bitmap. But what about getting file from “drawable” folder. Nothing supernatural – get resource ID from the android:src attribute and read raw file into an input stream. Next, libandroidsvg gives us a way to parse SVG file:

So, the constructor will look like this:

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();
     }                 
                }
            }
        }
}
 
Another problem is that SVG doesn’t have a size(Not always, some have desired ones. See P.S.). It’s scalable and it’s all. Moreover ImageView layout parameters can be set to wrap_content, fill_parent or we can set predefined size of the image. But when a layout is required, Android sets the size and we can override onSizeChanged method. The only problem is wrap_content attribute. In this case, the size will be 0. The idea is to replace wrap_content with fill_parent on the fly. But doing it in the constructor will give nothing. If you debug through source codes, you will see that parent layout drags layout parameters from attributes directly and calls setLayoutParams method. Let’s override it:

@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);
}
 
And 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);
}
 


And at last, it’s the time to try it. Create the following layout:

<?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>
 

And run:




Debugging Native Code

To debug native code, you can follow the instructions given by Carlos Souto at Sequoyah Project [9]. But during the first time, some things are unclear. So there are several tips:
  • While configuring C++ debug configuration, the application really must be app_process, don’t care Eclipse says the program doesn’t exist, it will be created later.
  • You should run ndk-gdb each time you run debugging. Sometimes the command should be ndk-gdb –adb=/tools/adb –force.
  • Don’t forget to set "debuggable" in the manifest.

Second Approach. Anti Grain Geometry.

There is another library where you can use to rasterize SVG files. It’s Anti Grain Geometry[10]. The only extra library that will be needed is libexpat. We already have it in our project. In the jni folder, create folders like this:



Copy the corresponding files from agg sources folder into gpc/include/src folders. There in an examples folder where you can find svg_viewer folder. Copy all files except svg_test into aggsvg jni folder. It will parse and rasterize SVG with use of AGG. But it has only basic support for SVG and can’t parse complicated stuff. You should extend the parser by yourself. In aggsvg-android folder, create aggsvgandroid.cpp file. The example parses SVG from a file system. To parse a string, add the following method to a parser class:

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;
        }
    }
 

At the end of Android.mk file, add section to build another
library. It’s pretty simple. Just clear variables after the first
library build and set them to build another library. And here is the
class to rasterize with use of 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());
 }
};
 

And in sources, I’ve added an ability to test both approaches:




Conclusion

So, there are at last two ways to show SVG file in Android. The main benefit of libsvg-android is that it is ready to use, but it is more than three times slower than AGG, with which you should extend SVG parser by your own. Also with AGG, you get extra features for the image processing. I’ve just used ImageView in the layout, but to use it programmatically you, of cause, should override more methods, such as setImageResource for example.

That’s all. Thanks!


Resources

  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. Also there is the article in Code Project with some additional improvements for SVG usage:
Drawable with SVG Support

Known issues:

ibsvg-android doesn't support rgb(255, 255, 255) color representation (used by Inkscape). (At least at the moment I wrote the article.)
Check if there are occurences of such representation and use fill: #ffffff

Also, there may be some other unsupported features unsupported by libsvg-android.
Check the structure of the .svg file you're using.


Tuesday, October 25, 2011

Animated Splash


An Advanced Splash Screen for Android App

Русский перевод


This my article was originally published on The Code Project web site. You can see it at Codeproject.

Source Code

Introduction


     Everyone wants his/her application to be beautiful and attractive for an eye of a user. And there are a lot of applications, at least desktop applications, mostly games that use splash screens. It’s nice and, moreover, while the splash screen is working, you can initialize your application. Many tutorials exist explaining how to begin Android programming and I won't repeat them here. You can find them all over the internet. So I will show only the programming stuff.


The Beginning


Create a new Android Eclipse project with the following settings:

Project name : AdvancedSplashDemo
Build target: I've set it to Android 2.1
Application name: Advanced Splash Demo
Package name: Advanced Splash Demo
Create Activity: MainActivity – it will be the application itself 
 

So, as we don’t need a splash screen after it’s done, the first thought is to use another activity that will start the main activity and silently die after that. Let’s create a layout for the splash – it will be a linear layout and an Image View inside it. Create a new Android XML file "splash.xml" in appfolder/res/layout folder. Don’t make it fill parent, as we want it to be really as splash screen. The image view has to wrap a content too:

 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/TheSplashLayout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center" >

    <ImageView
        android:id="@+id/SplashImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" >
    </ImageView>
</LinearLayout>
 

Here the gravity attribute value is set to "center" to make the splash to be at the center of the screen. Add some picture to the appfolder/res/drawable folder and press F5 on the project. I’ve added lnxins.png and as you can see, set it as the image view source.

So far, let’s look at the application manifest. It has now just one “.MainActivity” activity set as a launcher. We’ll set it as default category and add another splash activity with a splash layout and will set it as the launcher. Open the manifest and open an application tab. For the main activity, change an Android intent category to default. Near application nodes press an “Add…” button, choose create a new element at top level and double click on Activity. For new activity, click on a “Name*” hyperlink and enter “SplashScreen” class. In sources, a new class will be added for the splash activity. Next, press the “Add…” button again for the SplashScreen node and add the intent filter. Again, for just added intent filter, add an action and a category.

Set the action to android.intent.action.MAIN and the category to android.intent.category.LAUNCHER. So the Splash screen activity will be run first. The manifest should look like the following:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.yourname.main"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MainActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>    
        <activity android:name="SplashScreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"></action>
                <category android:name="android.intent.category.LAUNCHER"></category>
            </intent-filter>
        </activity>
    </application>
</manifest> 
 

A Little Coding

Open SplashScreen.java class. Now it has overridden onCreate method only. Override onTouchEvent method to give the user a possibility to close splash screen at every moment. And don’t forget synchronization or you will have random crashes. Here’s the class code:

public class SplashScreen extends Activity {
    
    /**
     * The thread to process splash screen events
     */
    private Thread mSplashThread;    

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Splash screen view
        setContentView(R.layout.splash);
        
        final SplashScreen sPlashScreen = this;   
        
        // The thread to wait for splash screen events
        mSplashThread =  new Thread(){
            @Override
            public void run(){
                try {
                    synchronized(this){
                        // Wait given period of time or exit on touch
                        wait(5000);
                    }
                }
                catch(InterruptedException ex){                    
                }

                finish();
                
                // Run next activity
                Intent intent = new Intent();
                intent.setClass(sPlashScreen, MainActivity.class);
                startActivity(intent);
                stop();                    
            }
        };
        
        mSplashThread.start();        
    }
        
    /**
     * Processes splash screen touch events
     */
    @Override
    public boolean onTouchEvent(MotionEvent evt)
    {
        if(evt.getAction() == MotionEvent.ACTION_DOWN)
        {
            synchronized(mSplashThread){
                mSplashThread.notifyAll();
            }
        }
        return true;
    }    
}


A Little Beautification

First, let’s make the splash screen transparent. In appfolder/res/values, add new Android XML file styles.xml and add to it a transparent theme:

<resources>
    <style name="Theme.Transparent" parent="android:Theme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:backgroundDimEnabled">false</item>
    </style>    
</resources>



Here are some explanations: as you can see, the style’s parent is android:Theme so we could apply it to our activity. And as you can see, the attribute's names are clear and you can understand what they mean.
Next, we’ll apply this theme to our splash. In the manifest file for the splash activity, set a "theme" attribute to the just created theme:

<activity 
    android:name="SplashScreen"
    android:theme="@style/Theme.Transparent"            
>
    <intent-filter>
        <action android:name="android.intent.action.MAIN"></action>
        <category android:name="android.intent.category.LAUNCHER"></category>
    </intent-filter>
</activity> 


Let’s suppose we’re developing a game application. And gamers don’t like when something distracts them from the game process. Most of them prefer fullscreen mode. So, set the fullscreen theme for the main activity:

<activity android:name=".MainActivity"
          android:label="@string/app_name"
          android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
          >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>


Run it. Looks better. Now we’ll make it fade-in and fade-out. Create in appfolder/res folder new folder "anim" and add to it two Android XML files – appear.xml and disappear.xml. They will be alpha animations.

Appear.xml



<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:interpolator="@android:anim/accelerate_interpolator"
        android:fromAlpha="0.0" android:toAlpha="1.0"
        android:duration="800"
    />
</set>


Disappear.xml



<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:interpolator="@android:anim/decelerate_interpolator"
        android:fromAlpha="1.0" android:toAlpha="0.0"
        android:duration="800"
    />
</set> 

In these animations, they just change the alpha channel of an object from fromAlpha value to toAlpha value for a given period of time. Now add new style in styles.xml:

<style name="Animations" parent="@android:Animation" />
    <style name="Animations.SplashScreen">
        <item name="android:windowEnterAnimation">@anim/appear</item>
        <item name="android:windowExitAnimation">@anim/disappear</item> 
    </style>
</style>

So, on the window, enter the “appear” animation will be performed and on the window exit the “disappear” animation will be performed. Add this style to Theme.Transparent theme:

<style name="Theme.Transparent" parent="android:Theme">
        ………
  <item name="android:windowAnimationStyle">
      @style/Animations.SplashScreen
  </item>
</style>


Ok, it’s time to run it again. Now it looks nice. And more…

Don’t Shoot at a Programmer, He Draws As He Can…

Let’s create an animated splash screen. As an artist, I’m not very good so I used Gimp’s Script-Fu to generate a set of frames for animations. First, remove android:src attribute in splash.xml. Then, in the drawable folder, create flag.xml:

<?xml version="1.0" encoding="utf-8"?>
<animation-list     
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@+id/flaganim"
    android:oneshot="false"
    >
    <item android:drawable="@drawable/f03" android:duration="100" />
    <item android:drawable="@drawable/f04" android:duration="100" />
    <item android:drawable="@drawable/f05" android:duration="100" />
    <item android:drawable="@drawable/f06" android:duration="100" />
    <item android:drawable="@drawable/f07" android:duration="100" />
    <item android:drawable="@drawable/f08" android:duration="100" />
    <item android:drawable="@drawable/f09" android:duration="100" />
    <item android:drawable="@drawable/f10" android:duration="100" />    
</animation-list>

Here is a set of frames and “oneshot” attribute says to loop them. To run animation, we need to change the splash screen class code. To the onCreate method, add the following:

final ImageView splashImageView = 
        (ImageView) findViewById(R.id.SplashImageView);
 splashImageView.setBackgroundResource(R.drawable.flag);
 final AnimationDrawable frameAnimation = 
              (AnimationDrawable)splashImageView.getBackground(); 


We've set the animation for the splash, but here’s a little problem. We can’t start it from the onCreate method. The animation must be started from GUI thread. For this, we’ll use a “post” method of the ImageView class. It will add our runnable to a message queue and when GUI thread will be available, it will start it:

splashImageView.post(new Runnable(){
            @Override
            public void run() {
                frameAnimation.start();                
            }            
        });


And here we are:


That’s all. Have fun with Android programming.

Thanks!