package jp.juggler.subwaytooter.span import android.content.Context import android.content.res.AssetManager 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.log.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, 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 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.e(ex, "SVG.getFromAsset failed.") null } } 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 const val sweepInterval = 30000L private const val sweepExpire = 10000L private const val sweepLimit1 = 64 // この個数を超えたら private const val sweepLimit2 = 32 // この個数まで減らす private var lastSweepTime = 0L private fun sweepCache(now: Long) { val cacheSize = bitmapCache.size if (now - lastSweepTime >= sweepInterval && cacheSize >= sweepLimit1) { lastSweepTime = 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 } } } 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 cDescent = (0.5f + size * descent_ratio).toInt() val cAscent = cDescent - size if (fm.ascent > cAscent) fm.ascent = cAscent if (fm.top > cAscent) fm.top = cAscent if (fm.descent < cDescent) fm.descent = cDescent if (fm.bottom < cDescent) fm.bottom = cDescent } return size } 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 val bitmap = prepareBitmap(assetsName, dstSize) 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) } } }