Android Glideを拡張する
Android用画像読み込みライブラリGlideでapngを扱えるようにしようとしたところ
うまくいかなくて解決するのに時間がかかってしまったので、解決方法を書きます。
svgを扱えるようにしてみる
com.caverock:androidsvg-aar:1.4というライブラリをGlideで使えるようにします。
まずGlideModuleを作成します。
ドキュメント通りにAppGlideModuleを継承します。
ちなみにですがModuleという名前からわかるように、
ここからGlideのインスタンスの設定をおこなっているようです。
@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
}
override fun isManifestParsingEnabled(): Boolean {
return false
}
}
InputStreamをSVGに変換する処理を追加する
svgの処理を追加する
Glideはappend, prependで任意の画像処理を追加することができます。
appendとprependの違いとしては、Glideは画像のDecoder処理を
Chain of Responsibility状の処理になっていて、
前のDecoder判定処理に失敗したら次のDecoder処理に回されていくような仕組みになっています。
そのため真っ先に処理させたい場合はprepend,後から処理させたい場合はappendにします。
prependは雑に処理してしまうとGlideでデフォルトで入ってる画像のDecoderの処理を邪魔してしまうため、
それら処理を上書きする意図がなければappendにします。
SVGはGlideの標準処理では全て処理できない扱いになるのでappendにします。
処理の内容としてはInputStreamを受け取って, SvgDecoderでSVGクラスに変換しています。
@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry
.append(InputStream::class.java, SVG::class.java, SvgDecoder())
}
override fun isManifestParsingEnabled(): Boolean {
return false
}
}
次にSvgDecoderクラスを任意のパッケージに作成します。
handlersはこの画像がこのDecoderで処理できる画像なのかを判定しています。
trueを返した場合は判定可能。falseを返した場合は判定不能という扱いになります。
先ほどのappend, prependの話になりますが、
問答無用でtrueを返すようにするとこの後にappendをしてもそのappendしたDecoderは処理されないと言った現象が発生するようになってしまいます。
decodeは第一引数の入力(InputStream)から得られた入力値から、
第二引数の出力(SVG)を出力する処理を記述しています。
private const val SVG_HEADER: Int = 0x3C737667
private const val SVG_HEADER_STARTS_WITH_XML = 0x3c3f786d
class SvgDecoder : ResourceDecoder<InputStream, SVG>{
override fun handles(source: InputStream, options: Options): Boolean {
val buffer = ByteArray(8)
val cnt = source.read(buffer)
if (cnt < 8) {
Log.d("SvgDecoder", "svgではない")
return false
}
Log.d("SvgDecoder", "svgだった")
val header = ByteBuffer.wrap(buffer).int
return header == SVG_HEADER || header == SVG_HEADER_STARTS_WITH_XML
}
override fun decode(
source: InputStream,
width: Int,
height: Int,
options: Options
): Resource<SVG>? {
return try {
val svg = SVG.getFromInputStream(source)
SimpleResource(svg)
}catch(e: SVGParseException){
Log.e("SvgDecoder", "error", e)
null
}
}
}
SVGをDrawableにする
SvgDecoderをappendすることによって、
InputStreamからSVGを得られるようになりましたが、
Androidでは直接SVGを扱うことができないので、
Drawableに変換する処理を追加します。
公式ではPictureDrawableを使用していますがPictureDrawableはリサイズや複雑なことをすることができないので、一度BitmapDrawableに変換しています。
package あなたのパッケージ名
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.PictureDrawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.SimpleResource
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
import com.caverock.androidsvg.SVG
class SvgBitmapTransCoder(val context: Context): ResourceTranscoder<SVG, BitmapDrawable>{
override fun transcode(toTranscode: Resource<SVG>, options: Options): Resource<BitmapDrawable> {
val svg = toTranscode.get()
val picture: Picture = svg.renderToPicture()
val drawable = PictureDrawable(picture)
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawPicture(drawable.picture)
return SimpleResource(BitmapDrawable(context.resources, bitmap))
}
}
GlideModuleに追加してください。
※省略しています。
registry.register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context))
apngを扱えるようにする
以下のようなライブラリを使うことで簡単にapngの対応を行うことができますが、
このライブラリの処理系を見てみると明らかGlideの標準処理より非効率な処理を行なっている上に、
壊れたGifを読み込むとアプリがクラッシュするといったことがあるため、
なるべくライブラリへの依存は少なくしたいところです。
そこでこのライブラリのapngの部分だけをGlideにうまく組み込んでみたいと思います。
apng対応の厄介なところ
svg対応はHeaderで判定することができるのですが、
apngはヘッダーがpngになってしまうので、png == apng判定をしてしまうと、
pngを処理できなくなってしまうので注意が必要です。(ライブラリ次第)
build.gradleにライブラリを追加する
バージョンはライブラリを参照してください。
dependencies {
implementation 'com.github.penfeizhou.android.animation:apng:${VERSION}'
}
apngのDecoderを追加する。
ソースコードの内容は参考にしたライブラリの丸パクリです。
判定処理でまずヘッダーがPNGであるかを判定しています。
この判定を入れることで、余分なデータを読み込む必要がないので、
オーバーヘッドを少なくすることができます。
最終的にAPNGParserのisAPNG判定をすることで、
pngの処理を潰してしまわないようにしています。
ちなみにByteBufferにしている理由としては、
Glideのソースコードを見たところpngなどの処理はまずByteBufferを通して処理をしているためだったと思います(うろ覚え)
const val PNG: Long = 0x89504E47
class ByteBufferApngDecoder : ResourceDecoder<ByteBuffer, FrameSeqDecoder<*, *>> {
override fun decode(
source: ByteBuffer,
width: Int,
height: Int,
options: Options
): Resource<FrameSeqDecoder<*, *>> {
val loader: Loader = object : ByteBufferLoader() {
override fun getByteBuffer(): ByteBuffer {
source.position(0)
return source
}
}
val decoder = APNGDecoder(loader, null)
return FrameSeqDecoderResource(decoder, source.limit())
}
override fun handles(source: ByteBuffer, options: Options): Boolean {
Log.d("ByteBufferApngDecoder", "apng decoder on decode")
val byteBufferArray = ByteArray(8)
source.get(byteBufferArray, 0, 4)
val header = ByteBuffer.wrap(byteBufferArray).long ushr 32
if (header != PNG) {
Log.d("ByteBufferApngDecoder", "is not png:${header.toHexString()}")
return false
}
val result = APNGParser.isAPNG(ByteBufferReader(source))
Log.d("ByteBufferApngDecoder", "handlers isApng:$result")
return result
}
private class FrameSeqDecoderResource(
private val decoder: FrameSeqDecoder<*, *>,
private val size: Int
) : Resource<FrameSeqDecoder<*, *>> {
override fun getResourceClass(): Class<FrameSeqDecoder<*, *>> {
return FrameSeqDecoder::class.java
}
override fun get(): FrameSeqDecoder<*, *> {
return decoder
}
override fun getSize(): Int {
return size
}
override fun recycle() {
decoder.stop()
}
}
}
Glide Moduleに追加する
最終的なGlideModuleです。
本当はClassに切り出すべきですが、ベタ書きをしてしまいました。
SVGの時の違いとしては、
ソースがさらに汚い- transcode処理でリソースの解放処理を行なっている
- appendではなくprepandを使用している
特にappendではなくprependを使用している理由としては、
GlideのpngのDecoderより先に判定をする必要があったからです。
@GlideModule(glideName = "GlideApp")
class MiGlideModule : AppGlideModule(){
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry
.prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, ByteBufferApngDecoder())
.register(FrameSeqDecoder::class.java, Drawable::class.java, object : ResourceTranscoder<FrameSeqDecoder<*, *>, Drawable> {
override fun transcode(
toTranscode: Resource<FrameSeqDecoder<*, *>>,
options: Options
): Resource<Drawable> {
val apngDrawable = APNGDrawable(toTranscode.get() as APNGDecoder)
apngDrawable.setAutoPlay(false)
return object : DrawableResource<Drawable>(apngDrawable) {
override fun getResourceClass(): Class<Drawable> {
return Drawable::class.java
}
override fun getSize(): Int {
return apngDrawable.memorySize
}
override fun recycle() {
apngDrawable.stop()
}
}
}
})
.register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context))
.append(InputStream::class.java, SVG::class.java, SvgDecoder())
}
override fun isManifestParsingEnabled(): Boolean {
return false
}
}
以上です。
Discussion
Wow,thank for your share!