2019-09-14 22:09:52 +02:00
|
|
|
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
|
2019-09-14 22:09:52 +02:00
|
|
|
import android.text.style.ReplacementSpan
|
|
|
|
import androidx.annotation.IntRange
|
|
|
|
import com.caverock.androidsvg.SVG
|
2021-02-22 22:33:54 +01:00
|
|
|
import jp.juggler.emoji.UnicodeEmoji
|
2019-09-14 22:09:52 +02:00
|
|
|
import jp.juggler.util.LogCategory
|
|
|
|
|
|
|
|
// 絵文字リソースの種類によって異なるスパンを作る
|
2021-02-22 22:33:54 +01:00
|
|
|
fun UnicodeEmoji.createSpan(context : Context, scale:Float=1f) = if(isSvg) {
|
2020-01-04 15:17:19 +01:00
|
|
|
SvgEmojiSpan(context, assetsName !!,scale=scale)
|
2019-09-14 22:09:52 +02:00
|
|
|
} else {
|
2020-01-04 15:17:19 +01:00
|
|
|
EmojiImageSpan(context, drawableId,scale = scale)
|
2019-09-14 22:09:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// SVG絵文字スパン
|
|
|
|
class SvgEmojiSpan internal constructor(
|
|
|
|
context : Context,
|
2019-09-19 06:26:42 +02:00
|
|
|
private val assetsName : String,
|
2019-09-14 22:09:52 +02:00
|
|
|
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 {
|
2019-09-14 22:09:52 +02:00
|
|
|
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
|
2019-09-14 22:09:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
2019-09-14 22:09:52 +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-14 22:09:52 +02:00
|
|
|
|
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)
|
2019-09-14 22:09:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|