SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt

234 lines
6.8 KiB
Kotlin
Raw Normal View History

package jp.juggler.subwaytooter.span
import android.content.Context
import android.content.res.AssetManager
2019-09-19 06:26:42 +02:00
import android.graphics.*
import android.os.SystemClock
import android.text.style.ReplacementSpan
import androidx.annotation.IntRange
import com.caverock.androidsvg.SVG
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.util.LogCategory
// 絵文字リソースの種類によって異なるスパンを作る
fun UnicodeEmoji.createSpan(context : Context, scale:Float=1f) = if(isSvg) {
SvgEmojiSpan(context, assetsName !!,scale=scale)
} else {
EmojiImageSpan(context, drawableId,scale = scale)
}
// SVG絵文字スパン
class SvgEmojiSpan internal constructor(
context : Context,
2019-09-19 06:26:42 +02:00
private val assetsName : String,
private val scale : Float = 1f
) : ReplacementSpan() {
companion object {
internal val log = LogCategory("SvgEmojiSpan")
private const val scale_ratio = 1.14f
private const val descent_ratio = 0.211f
2019-09-19 06:26:42 +02:00
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)
null
}
2019-09-19 06:26:42 +02:00
}
2019-09-19 06:35:47 +02:00
class BitmapCacheKey(var code : String = "", var size : Int = 1) :
Comparable<BitmapCacheKey> {
2019-09-19 06:26:42 +02:00
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()"
}
2019-09-19 06:35:47 +02:00
class BitmapCacheValue(val bitmap : Bitmap?, var lastUsed : Long)
2019-09-19 06:26:42 +02:00
private val bitmapCache = HashMap<BitmapCacheKey, BitmapCacheValue>()
// 時々キャッシュを掃除する
private const val sweepInterval = 30000L
private const val sweepExpire = 10000L
2019-09-19 06:35:47 +02:00
private const val sweepLimit1 = 64 // この個数を超えたら
private const val sweepLimit2 = 32 // この個数まで減らす
private var lastSweepTime = 0L
2019-09-19 06:26:42 +02:00
private fun sweepCache(now : Long) {
val cacheSize = bitmapCache.size
2019-09-19 06:35:47 +02:00
if(now - lastSweepTime >= sweepInterval && cacheSize >= sweepLimit1) {
lastSweepTime = now
2019-09-19 06:26:42 +02:00
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) {
2019-09-19 06:35:47 +02:00
log.trace(ex, "bitmap allocation failed!")
2019-09-19 06:26:42 +02:00
null
}
}
// create cache even if bitmap is null.
bitmapCache[BitmapCacheKey(assetsName, dstSizeInt)] = BitmapCacheValue(bitmap, now)
return bitmap
}
}
}
init {
if(assetsManager == null)
assetsManager = context.applicationContext.assets
}
override fun getSize(
paint : Paint,
text : CharSequence,
@IntRange(from = 0) start : Int,
@IntRange(from = 0) end : Int,
fm : Paint.FontMetricsInt?
) : Int {
val size = (paint.textSize * scale_ratio * scale + 0.5f).toInt()
if(fm != null) {
val c_descent = (0.5f + size * descent_ratio).toInt()
val c_ascent = c_descent - size
if(fm.ascent > c_ascent) fm.ascent = c_ascent
if(fm.top > c_ascent) fm.top = c_ascent
if(fm.descent < c_descent) fm.descent = c_descent
if(fm.bottom < c_descent) fm.bottom = c_descent
}
return size
}
2019-09-19 06:26:42 +02:00
override fun draw(
canvas : Canvas,
text : CharSequence,
start : Int,
end : Int,
x : Float,
top : Int,
baseline : Int,
bottom : Int,
textPaint : Paint
) {
// 絵文字の正方形のサイズ
val dstSize = textPaint.textSize * scale_ratio * scale
2019-09-19 06:26:42 +02:00
val bitmap = prepareBitmap(assetsName, dstSize)
2019-09-19 06:26:42 +02:00
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)
}
}
}