From 7621c531799ad9544ce36884a644749c92fd6708 Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 19 Sep 2019 13:26:42 +0900 Subject: [PATCH] add bitmap cache fopr SVG emoji --- .../subwaytooter/span/EmojiImageSpan.kt | 4 +- .../subwaytooter/span/NetworkEmojiSpan.kt | 30 ++- .../juggler/subwaytooter/span/SvgEmojiSpan.kt | 215 +++++++++++++----- 3 files changed, 175 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt index 46db0d48..6f836a00 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt @@ -3,11 +3,9 @@ package jp.juggler.subwaytooter.span import android.content.Context import android.graphics.* import android.graphics.drawable.Drawable +import android.text.style.ReplacementSpan import androidx.annotation.IntRange import androidx.core.content.ContextCompat -import android.text.style.ReplacementSpan -import jp.juggler.emoji.EmojiMap - import java.lang.ref.WeakReference class EmojiImageSpan( diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt index 001a8a78..7f85e076 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt @@ -16,7 +16,7 @@ import java.lang.ref.WeakReference class NetworkEmojiSpan internal constructor( private val url : String, private val scale : Float = 1f -) : ReplacementSpan(),AnimatableSpan { +) : ReplacementSpan(), AnimatableSpan { companion object { @@ -30,7 +30,6 @@ class NetworkEmojiSpan internal constructor( private val rect_src = Rect() private val rect_dst = RectF() - // フレーム探索結果を格納する構造体を確保しておく private val mFrameFindResult = ApngFrames.FindFrameResult() @@ -48,7 +47,6 @@ class NetworkEmojiSpan internal constructor( this.refDrawTarget = WeakReference(draw_target_tag) this.invalidate_callback = invalidate_callback } - override fun getSize( paint : Paint, @@ -88,7 +86,7 @@ class NetworkEmojiSpan internal constructor( // APNGデータの取得 val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) { - invalidate_callback.delayInvalidate(0) + invalidate_callback.delayInvalidate(0L) } ?: return val t = if(Pref.bpDisableEmojiAnimation(App1.pref)) @@ -106,33 +104,33 @@ class NetworkEmojiSpan internal constructor( } val srcWidth = b.width val srcHeight = b.height - if(srcWidth < 1 || srcHeight <1){ + if(srcWidth < 1 || srcHeight < 1) { log.e("draw: bitmap size is too small.") return } - rect_src.set(0, 0, srcWidth, srcHeight ) - + rect_src.set(0, 0, srcWidth, srcHeight) + // 絵文字の正方形のサイズ val dstSize = textPaint.textSize * scale_ratio * scale - + // ベースラインから上下方向にずらすオフセット val c_descent = dstSize * descent_ratio val transY = baseline - dstSize + c_descent // 絵文字のアスペクト比から描画範囲の幅と高さを決める - val dstWidth:Float - val dstHeight:Float + val dstWidth : Float + val dstHeight : Float val aspectSrc = srcWidth.toFloat() / srcHeight.toFloat() - if( aspectSrc >= 1f){ + if(aspectSrc >= 1f) { dstWidth = dstSize - dstHeight = dstSize /aspectSrc - }else{ + dstHeight = dstSize / aspectSrc + } else { dstHeight = dstSize dstWidth = dstSize * aspectSrc } - val dstX = (dstSize-dstWidth)/2f - val dstY = (dstSize-dstHeight)/2f - rect_dst.set(dstX,dstY,dstX+dstWidth,dstY+dstHeight) + val dstX = (dstSize - dstWidth) / 2f + val dstY = (dstSize - dstHeight) / 2f + rect_dst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight) canvas.save() canvas.translate(x, transY) diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt index 445b48b8..d4ab88dd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt @@ -2,9 +2,8 @@ package jp.juggler.subwaytooter.span import android.content.Context import android.content.res.AssetManager -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF +import android.graphics.* +import android.os.SystemClock import android.text.style.ReplacementSpan import androidx.annotation.IntRange import com.caverock.androidsvg.SVG @@ -13,7 +12,7 @@ import jp.juggler.util.LogCategory // 絵文字リソースの種類によって異なるスパンを作る fun EmojiMap.EmojiResource.createSpan(context : Context) = if(isSvg) { - SvgEmojiSpan(context, assetsName!!) + SvgEmojiSpan(context, assetsName !!) } else { EmojiImageSpan(context, drawableId) } @@ -21,37 +20,172 @@ fun EmojiMap.EmojiResource.createSpan(context : Context) = if(isSvg) { // SVG絵文字スパン class SvgEmojiSpan internal constructor( context : Context, - assetsName : String, + private val assetsName : String, private val scale : Float = 1f ) : ReplacementSpan() { companion object { internal val log = LogCategory("SvgEmojiSpan") - - private var assetsManager : AssetManager? = null private const val scale_ratio = 1.14f private const val descent_ratio = 0.211f - - // SVGの描画はBitmapを消費しないので、上限なしキャッシュ - private class CacheResult(val svg : SVG?) - private val cacheMap = HashMap() - private fun loadFromCache(assetsName : String) : SVG? { - assetsManager ?: return null - synchronized(cacheMap) { - val item = cacheMap[assetsName] - if(item != null) return item.svg - val svg = try { + private var assetsManager : AssetManager? = null + + private fun loadSvg(assetsName : String) : SVG? = + when(val assetsManager = assetsManager) { + null -> null + else -> try { SVG.getFromAsset(assetsManager, assetsName) } catch(ex : Throwable) { log.trace(ex) - log.e(ex, "getFromAsset failed.") null } - cacheMap[assetsName] = CacheResult(svg) - return svg + } + + class BitmapCacheKey( + var code : String = "", + var size : Int = 1 + ) : Comparable { + + override fun hashCode() : Int { + return code.hashCode() xor size + } + + override fun compareTo(other : BitmapCacheKey) : Int { + val i = code.compareTo(other.code) + if(i != 0) return i + return size.compareTo(other.size) + } + + override fun equals(other : Any?) : Boolean = + if(other is BitmapCacheKey) { + compareTo(other) == 0 + } else { + false + } + // don't use "when(other){ this -> …", it recursive call "equals()" + } + + class BitmapCacheValue( + val bitmap : Bitmap?, + var lastUsed : Long + ) + + private val bitmapCache = HashMap() + + // 時々キャッシュを掃除する + private var lastSweepCache = 0L + private const val sweepInterval = 30000L + private const val sweepExpire = 10000L + private const val sweepLimit1 = 512 // この個数を超えたら + private const val sweepLimit2 = 128 // この個数まで減らす + + private fun sweepCache(now : Long) { + val cacheSize = bitmapCache.size + if(now - lastSweepCache >= sweepInterval && cacheSize >= sweepLimit1) { + lastSweepCache = now + val list = bitmapCache.entries.sortedBy { it.value.lastUsed } + // 最低保持数より多い分が検査対象 + var mayRemove = cacheSize - sweepLimit2 + val it = list.iterator() + while(it.hasNext() && mayRemove > 0) { + val item = it.next() + // 最近作られたキャッシュは破棄しない + if(now - item.value.lastUsed <= sweepExpire) break + item.value.bitmap?.recycle() + bitmapCache.remove(item.key) + -- mayRemove + } + log.d("sweep. cache size $cacheSize=>${bitmapCache.size}") + } + } + + private val paint = Paint().apply { + isAntiAlias = true + isFilterBitmap = true + } + + private val rect_dst = RectF() + + private fun renderBitmap(bitmap : Bitmap, svg : SVG, dstSize : Float) { + try { + // the width in pixels, or -1 if there is no width available. + // the height in pixels, or -1 if there is no height available. + val src_w = svg.documentWidth + val src_h = svg.documentHeight + val srcAspect = if(src_w <= 0f || src_h <= 0f) { + // widthやheightの情報がない + 1f + } else { + src_w / src_h + } + + // 絵文字のアスペクト比から描画範囲の幅と高さを決める + val dstWidth : Float + val dstHeight : Float + if(srcAspect >= 1f) { + dstWidth = dstSize + dstHeight = dstSize / srcAspect + } else { + dstHeight = dstSize + dstWidth = dstSize * srcAspect + } + val dstX = (dstSize - dstWidth) / 2f + val dstY = (dstSize - dstHeight) / 2f + rect_dst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight) + + bitmap.eraseColor(Color.TRANSPARENT) + svg.renderToCanvas(Canvas(bitmap), rect_dst) + } catch(ex : Throwable) { + log.e(ex, "rendering failed.!") + } + } + + private val bitmapCacheKeyForSearch = BitmapCacheKey() + + private fun prepareBitmap(assetsName : String, dstSize : Float) : Bitmap? { + + val dstSizeInt = (dstSize + 0.995f).toInt() + synchronized(bitmapCache) { + val now = SystemClock.elapsedRealtime() + + // check bitmap cache + bitmapCacheKeyForSearch.code = assetsName + bitmapCacheKeyForSearch.size = dstSizeInt + val cached = bitmapCache[bitmapCacheKeyForSearch] + if(cached != null) { + val bitmap = cached.bitmap + if(bitmap != null) { + cached.lastUsed = now + return bitmap + } else if(now - cached.lastUsed < sweepExpire) { + return null + // if recently created, just return error cache + // don't update lastUsed. + } + // fall: retry error cache + } + + sweepCache(now) + + val bitmap : Bitmap? = when(val svg = loadSvg(assetsName)) { + null -> null + + else -> try { + Bitmap.createBitmap(dstSizeInt, dstSizeInt, Bitmap.Config.ARGB_8888) + ?.also { renderBitmap(it, svg, dstSize) } + } catch(ex : Throwable) { + log.e(ex, "bitmap allocation failed!") + null + } + } + + // create cache even if bitmap is null. + bitmapCache[BitmapCacheKey(assetsName, dstSizeInt)] = BitmapCacheValue(bitmap, now) + + return bitmap } } } @@ -60,11 +194,7 @@ class SvgEmojiSpan internal constructor( if(assetsManager == null) assetsManager = context.applicationContext.assets } - - private val rect_dst = RectF() - private val svg = loadFromCache(assetsName) - override fun getSize( paint : Paint, text : CharSequence, @@ -83,7 +213,7 @@ class SvgEmojiSpan internal constructor( } return size } - + override fun draw( canvas : Canvas, text : CharSequence, @@ -95,39 +225,14 @@ class SvgEmojiSpan internal constructor( bottom : Int, textPaint : Paint ) { - svg?:return - - val src_w = svg.documentWidth // the width in pixels, or -1 if there is no width available. - val src_h = svg.documentHeight // the height in pixels, or -1 if there is no height available. - val srcAspect = if( src_w <= 0f || src_h <=0f){ - // widthやheightの情報がない - 1f - }else{ - src_w / src_h - } - // 絵文字の正方形のサイズ val dstSize = textPaint.textSize * scale_ratio * scale + val bitmap = prepareBitmap(assetsName, dstSize) - // ベースラインから上下方向にずらすオフセット - val c_descent = dstSize * descent_ratio - val transY = baseline - dstSize + c_descent - - // 絵文字のアスペクト比から描画範囲の幅と高さを決める - val dstWidth : Float - val dstHeight : Float - if(srcAspect >= 1f) { - dstWidth = dstSize - dstHeight = dstSize / srcAspect - } else { - dstHeight = dstSize - dstWidth = dstSize * srcAspect + if(bitmap != null) { + val y = baseline - dstSize + dstSize * descent_ratio + rect_dst.set(x, y, x + bitmap.width, y + bitmap.height) + canvas.drawBitmap(bitmap, null, rect_dst, paint) } - val dstX = (dstSize - dstWidth) / 2f - val dstY = (dstSize - dstHeight) / 2f - - rect_dst.set(x+dstX, transY+dstY , x+dstX+ dstWidth, transY+dstY+ dstHeight ) - svg.renderToCanvas(canvas,rect_dst) } - }