Android. ImageView 支持 SVG






4.83/5 (19投票s)
在 ImageView 控件中使用 SVG 的技术。
引言
如您所知,Android 不支持 SVG 格式。但 SVG 的好处是显而易见的。首先,它是可伸缩的。您无需拥有不同分辨率的图片,也无需对位图图像进行拉伸(例如,会损失质量)。SVG 图像可以缩放到任何分辨率,并且质量保持不变。其次,SVG 是一个普通的 XML 文件,因此其大小可能比同等图片的栅格格式文件要小得多。此外,由于这个特性,您还可以动态更改图片。您可以在普通的文本编辑器中打开 SVG 文件,查看其构成。等等……但由于 Android 不处理 SVG,所以需要一些原生编码。好消息是,原生编码的工作量不会很大。有一些开源库可以用于解析和栅格化 SVG 格式。
必备组件
有很多关于如何在 Android 平台进行原生开发的教程,所以这里我就不再赘述了。我只会提供一些有用的技巧。
- 首先,您需要 Eclipse IDE。您可以在 [ 1 ] 下载。
- 或者,您也可以选择使用 Motodev Studio [ 2 ]。我更喜欢后者,因为它有一些非常棒的功能。顺便说一句,您可以将 Motodev Studio 作为插件安装到 Eclipse IDE 中。我没能在 OpenSUSE 上成功配置它,但作为插件,它运行得很好。
- 安装好 Eclipse 后,添加 CDT 插件。
- 添加 Android 插件 [ 3 ]。
- 之后,添加 Eclipse Sequoyah [ 4 ] 插件,这是原生调试所必需的。但请确保您在安装 Sequoyah 之前安装了 CDT。Sequoyah 项目声称会安装所有依赖项,但实际上并没有安装 CDT。在安装 Sequoyah 时,请确保取消勾选“按类别分组项”并勾选“Sequoyah Android 原生支持”。
- Windows 用户还需要 cygwin [ 5 ](将 *Cygwin/bin* 路径添加到您的系统中)。安装时,请设置开发工具。
- 下载 Android SDK [ 6 ]。
- 最后,您还需要 Android NDK。下载 CrystaX NDK [ 7 ]。它支持 C++ 异常、RTTI 和标准 C++ 库。
- 在 Eclipse 偏好设置中,设置 Android SDK 和 NDK 的位置。现在就这些了。
第一种方法
对于第一个方法,我将使用 android-libsvg [ 8 ] 库。它实际上依赖于 libsvg、libpng、libjpeg、libexpat 和 zlib。但目前,它支持 SVG 格式的几乎所有功能。要获取其源代码,请在文件系统中创建一个名为 *android-libsvg* 的文件夹,然后在控制台(Windows 用户请在 cygwin 中)进入该文件夹,并运行“bzr branch lp:libsvg-android
”命令。Bazaar 会将源代码下载到该文件夹。
开始
好的。创建一个新的 Android 项目“ImageViewSvg
”。现在右键单击项目,然后选择 AndroidTools/Add Native support。它会在项目中创建一个“jni”文件夹。删除其中的所有内容,并将 *libsvg-android* 项目的“jni”文件夹中的内容复制过来。刷新项目中的 *jni* 文件夹。让我们来看看“jni”文件夹中的 *Android.mk* 文件。我将解释一些变量:
- LOCAL_PATH := $(call my-dir) – my-dir 宏设置 LOCAL_PATH 变量,用于将源文件定位到当前目录
- include $(CLEAR_VARS) – 清除所有局部变量
- LOCAL_MODULE – 库的名称
- LOCAL_CFLAGS – 设置编译器标志和一些包含目录
- LIBJPEG_SOURCES, … – 将要使用的每个库的源文件列表
- LOCAL_LDLIBS – 链接到附加库
- LOCAL_SRC_FILES – 要编译的所有源文件列表,这里包含所有库的所有源文件
- BUILD_SHARED_LIBRARY – 链接到 mk 文件以构建共享库
更多信息,请参阅 NDK 中的 *ANDROID-MK.TXT*。
在 Windows 上,有时在重启 IDE 后,它可能无法运行 ndk-build。然后右键单击项目,选择“Build Path/Configure Build Path”,并将“Build command”更改为类似“bash /cygdrive/c/ndk/ndk-build”的内容。
接下来,创建 *com.toolkits.libsvgandroid* 包,并将 *libsvg-android* 项目中的 *SvgRaster.java* 复制到其中。准备工作就绪。
ImageViewSvg 类
要使 `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”文件夹获取文件呢?没什么特别的——从 `android:src` 属性获取资源 ID,然后将原始文件读入输入流。接下来,*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`,或者我们可以设置图像的预定义大小。但是,当需要布局时,Android 会设置大小,我们可以重写 `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>
然后运行。

调试原生代码
要调试原生代码,您可以按照 Carlos Souto 在 Sequoyah 项目 [ 9 ] 中提供的说明进行操作。但初次使用时,有些地方可能不清楚。因此,有一些技巧:
- 配置 C++ 调试配置时,应用程序实际上必须是 `app_process`,不要在意 Eclipse 说程序不存在,它稍后会创建。
- 每次运行调试时都应该运行 ndk-gdb。有时命令应该是 ndk-gdb –adb=
/tools/adb –force。 - 不要忘记在清单文件中设置“debuggable”。
第二种方法。Anti Grain Geometry。
还有一个库可以用于栅格化 SVG 文件。那就是 Anti Grain Geometry [ 10 ]。唯一需要的额外库是 `libexpat`。我们在项目中已经有了。在 *jni* 文件夹中,创建如下文件夹:

将相应的 *agg* 源文件夹中的文件复制到 *gpc/include/src* 文件夹中。那里有一个 *examples* 文件夹,您可以在其中找到 *svg_viewer* 文件夹。复制除 *svg_test* 之外的所有文件到 *aggsvg jni* 文件夹。它将使用 AGG 来解析和栅格化 SVG。但它只支持基本的 SVG 功能,无法解析复杂的内容。您需要自己扩展解析器。在 *aggsvg-android* 文件夹中,创建一个 *aggsvgandroid.cpp* 文件。示例从文件系统中解析 SVG。要解析 *string*,请向 *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());
}
};
在源代码中,我添加了测试这两种方法的功能:

结论
所以,至少有两种方法可以在 Android 中显示 SVG 文件。 `libsvg-android` 的主要优点是它开箱即用,但它比 AGG 慢三倍多。而使用 AGG,您需要自己扩展 SVG 解析器。另外,使用 AGG,您还可以获得图像处理的额外功能。我只是在布局中使用了 `ImageView`,但要以编程方式使用它,您当然应该重写更多方法,例如 `setImageResource`。
就是这些了。谢谢!
资源
- http://www.eclipse.org
- http://developer.motorola.com/docstools/motodevstudio
- https://developer.android.com.cn/guide/developing/tools/adt.html
- http://www.eclipse.org/sequoyah/
- http://www.cygwin.com/
- https://developer.android.com.cn/sdk/index.html
- http://www.crystax.net/android/ndk-r4.php?lang=en
- https://launchpad.net/libsvg-android
- http://www.eclipse.org/sequoyah/documentation/native_debug.php
- http://www.antigrain.com/
历史
- 2010 年 11 月 25 日:首次发布