Android使用矢量图(SVG, VectorDrawable)实践篇
先放上两篇Android SVG使用相关的文章作为备忘。
问题背景
虽然老早就知道Android支持SVG资源,但是因为缺乏使用场景,所以这方面一直没有实践过。因而当我看到甲方的接口返回了一串这个东西时,一时间我是懵逼的:
"svg": "<path d=\"M364.4,55.1...364.4,55.1z\"></path>
<path d=\"M218.6,130.1h-47.4.1-...1z\"></path>"
服务器返回的svg对象是一串xml格式的文本,这是个什么鬼呢?
下面是一个完整的SVG文件内容示例:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1527589197284" class="icon" style=""
viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="2045"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<defs><style type="text/css"></style></defs>
<path d="M235.097303 ...133018Z" p-id="2046"></path>
<path d="M399.609756 ... 902.017094Z" p-id="2047"></path>
<path d="M924.097562 ... 902.017094Z" p-id="2048"></path>
</svg>
通过对比可以看出来,服务器返回的xml,其实就是SVG文件中的<path>...</path>
部分。我试了一下,虽然返回的信息不完整,但这段xml套入一个完整的SVG标签后,是可以正常加载出来图片的。
那要如何在手机上加载这串xml呢?
寻找解决方案
查了些Android 加载SVG的相关资料后我发现,这类文章绝大多数都在描述“加载本地SVG文件”这一使用场景(当然这也是最常见的场景),对我的窘境并没有什么帮助。不过收获还是有的,我发现StackOverFlow上早在15年就有这么一个问题:
android:load svg file from web and show it on image view
里面赞数最高的回答,描述了如何用我们的老朋友Glide加载一个SVG文件的Url。答者还很贴心的更新了新版本Glide (v4)的相关内容:
Update: For newer version please checkout the Glide Samples (https://github.com/bumptech/glide/tree/master/samples/svg)
点进去一看,非常简洁明了的范例,只需要在工程里集成AndroidSVG库,然后把范例搬进工程就能用了:
1.集成AndroidSVG
这个库是加载SVG的核心库,如果想抛开glide,单纯加载SVG的话,有这个库就够了。
2.拷贝源码
下面贴上源码:
SvgDecoder.java
/**
* Decodes an SVG internal representation from an {@link InputStream}.
*/
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
svg.setDocumentWidth(width);
svg.setDocumentHeight(height);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}
这个类的作用是把glide通过url加载的资源转成Svg类型,转换的过程依赖AndroidSvg库提供的方法,很简单。转换出的Svg对象,可以设置渲染的像素密度、文件宽高、viewbox宽高等参数。在这里我们需要将文件宽高设置成我们加载图片的imageView的宽高,以保证图片的正常显示。
SvgDrawableTranscoder.java
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}
很简单,调用Svg类自带的方法,完成Svg -> PictureDrawable的转换
SvgSoftwareLayerSetter.java
/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}
注意注释内容: Listener which updates the {@link ImageView} to be software rendered, because {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on a hardware backed {@link android.graphics.Canvas Canvas}.
3.SvgModule.java在AppGlideModule进行注册
依托Glide的Generated API特性,在工程的AppGlideModule类中注册上面的组件:
@GlideModule
public class SvgModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}
// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}
注册SvgDrawableTranscoder,告诉Glide由SVG转成PictureDrawable依靠SvgDrawableTranscoder类;
注册SvgDecoder,告诉Glide由InputStream转成SVG依靠SvgDecoder类。
*参考Glide官方文档Generated API配置AppGlideModule
4.加载图片
RequestBuilder<PictureDrawable> requestBuilder = GlideApp.with(context)
.as(PictureDrawable.class)
.transition(withCrossFade())
.listener(new SvgSoftwareLayerSetter());
requestBuilder.load(svg).into(view);
到这一步,就可以通过Url直接加载SVG图片了。
自定义ModelLoader,实现从xml载入SVG
虽然现在可以用Glide直接加载SVG文件的Url了,但我这边需要的是从xml直接加载。好在Glide已经足够强大,可以让我们充分自定义图片加载的过程:
1.自定义ModelLoader
public class MTSvgModelLoader implements ModelLoader<MTSVGItem, InputStream> {
@Nullable
@Override
public LoadData<InputStream> buildLoadData(@NonNull MTSVGItem mtigqsvgItem, int width, int height, @NonNull Options options) {
Key diskCacheKey = new ObjectKey(mtigqsvgItem.getFullSVG());
return new LoadData<>(diskCacheKey, new MTSvgFetcher(mtigqsvgItem.getFullSVG()));
}
@Override
public boolean handles(@NonNull MTSVGItem mtigqsvgItem) {
return true;
}
}
MTSVGItem是我希望Glide加载的对象,而InputStream是输入出的对象。
2.自定义DataFetcher
public class MTSvgFetcher implements DataFetcher<InputStream> {
private final String model;
public MTSvgFetcher(String model) {
this.model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) {
InputStream stream = new ByteArrayInputStream(model.getBytes(StandardCharsets.UTF_8));
callback.onDataReady(stream);
}
@Override
public void cleanup() {
}
@Override
public void cancel() {
}
@NonNull
@Override
public Class getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.LOCAL;
}
}
DataFatcher负责切实的获取到图片的数据,通常这里要进行本地的文件读取或者下载图片的操作,但是这里我们只要把SVG的xml转换成InputStream返回就行了。
3.自定义ModelLoaderFactory
public class MTSvgModelLoaderFactory implements ModelLoaderFactory<MTSVGItem, InputStream> {
@NonNull
@Override
public ModelLoader<MTSVGItem, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new MTSvgModelLoader();
}
@Override
public void teardown() {
}
}
glide注册组件注册的是ModelLoaderFactory,因此还需要一包装一下...
4.注册ModelLoaderFactory
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder())
.append(MTSVGItem.class, InputStream.class, new MTSvgModelLoaderFactory());
到这一步,Glide就可以从我自定义的对象MTSVGItem加载出来SVG图片了。
总...结
Glide我也用了很久了,这次是第一次做自定义ModelLoader的尝试。好在Glide v4提供了清晰的文档,整个过程非常的平滑愉快。
虽说作为一个没有理想的搬砖工人,没什么深入研究技术的动力,但是加深对Glide这种常用工具的了解,毫无疑问可以增加搬砖的效率。不错不错,善莫大焉~