add bitmap cache fopr SVG emoji
This commit is contained in:
parent
9b91aaa002
commit
7621c53179
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue