add bitmap cache fopr SVG emoji

This commit is contained in:
tateisu 2019-09-19 13:26:42 +09:00
parent 9b91aaa002
commit 7621c53179
3 changed files with 175 additions and 74 deletions

View File

@ -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(

View File

@ -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)

View File

@ -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<String, CacheResult>()
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<BitmapCacheKey> {
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<BitmapCacheKey, BitmapCacheValue>()
// 時々キャッシュを掃除する
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)
}
}