2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.span
|
|
|
|
|
|
|
|
|
|
import android.graphics.Canvas
|
|
|
|
|
import android.graphics.Paint
|
|
|
|
|
import android.graphics.Rect
|
|
|
|
|
import android.graphics.RectF
|
2023-02-08 10:55:49 +01:00
|
|
|
|
import android.graphics.drawable.Drawable
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.text.style.ReplacementSpan
|
2020-09-11 11:19:09 +02:00
|
|
|
|
import androidx.annotation.IntRange
|
2023-02-21 16:17:29 +01:00
|
|
|
|
import androidx.collection.LruCache
|
2023-02-08 10:55:49 +01:00
|
|
|
|
import androidx.core.content.ContextCompat
|
2018-01-28 20:03:04 +01:00
|
|
|
|
import jp.juggler.apng.ApngFrames
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2023-02-08 10:55:49 +01:00
|
|
|
|
import jp.juggler.subwaytooter.R
|
2023-02-21 16:17:29 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
2021-11-06 04:00:29 +01:00
|
|
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
2023-02-08 10:55:49 +01:00
|
|
|
|
import jp.juggler.subwaytooter.pref.lazyContext
|
2023-02-21 16:17:29 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
2023-01-13 13:22:25 +01:00
|
|
|
|
import jp.juggler.util.log.LogCategory
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import java.lang.ref.WeakReference
|
2023-02-21 16:17:29 +01:00
|
|
|
|
import kotlin.math.min
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2023-02-21 16:17:29 +01:00
|
|
|
|
enum class EmojiSizeMode {
|
|
|
|
|
Square,
|
|
|
|
|
Wide,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun SavedAccount?.emojiSizeMode(): EmojiSizeMode {
|
|
|
|
|
val ti = this?.let { TootInstance.getCached(it) }
|
|
|
|
|
return when {
|
|
|
|
|
ti == null -> EmojiSizeMode.Square
|
|
|
|
|
ti.isMisskey || !ti.fedibirdCapabilities.isNullOrEmpty() -> EmojiSizeMode.Wide
|
|
|
|
|
else -> EmojiSizeMode.Square
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class NetworkEmojiSpan constructor(
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private val url: String,
|
2023-02-21 16:17:29 +01:00
|
|
|
|
private val sizeMode: EmojiSizeMode,
|
2023-01-13 13:22:25 +01:00
|
|
|
|
private val scale: Float = 1f,
|
2023-02-21 16:17:29 +01:00
|
|
|
|
private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
|
2019-09-19 06:26:42 +02:00
|
|
|
|
) : ReplacementSpan(), AnimatableSpan {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
internal val log = LogCategory("NetworkEmojiSpan")
|
|
|
|
|
private const val scaleRatio = 1.14f
|
|
|
|
|
private const val descentRatio = 0.211f
|
2023-02-21 16:17:29 +01:00
|
|
|
|
|
|
|
|
|
// 最大幅
|
|
|
|
|
var maxEmojiWidth = Float.MAX_VALUE
|
|
|
|
|
|
|
|
|
|
// ImageWidthCache
|
|
|
|
|
val imageAspectCache = LruCache<String, Float>(1024)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-08 10:55:49 +01:00
|
|
|
|
private val mPaint = Paint().apply { isFilterBitmap = true }
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private val rectSrc = Rect()
|
|
|
|
|
private val rectDst = RectF()
|
|
|
|
|
|
|
|
|
|
// フレーム探索結果を格納する構造体を確保しておく
|
|
|
|
|
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
|
|
|
|
|
|
|
|
|
private var invalidateCallback: AnimatableSpanInvalidator? = null
|
2023-02-08 10:55:49 +01:00
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private var refDrawTarget: WeakReference<Any>? = null
|
|
|
|
|
|
2023-02-08 10:55:49 +01:00
|
|
|
|
private var errorDrawableCache: Drawable? = null
|
|
|
|
|
|
2023-02-21 16:17:29 +01:00
|
|
|
|
private var lastWidth: Float? = null
|
|
|
|
|
private var transY = 0f
|
2023-02-21 20:20:24 +01:00
|
|
|
|
private var emojiHeight = 0f
|
|
|
|
|
private var emojiWidth = 0f
|
2023-02-21 16:17:29 +01:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* lastAspect に基づいて rectDst と transY を更新する
|
|
|
|
|
*/
|
|
|
|
|
private fun updateRect(aspectArg: Float? = null, textSize: Float, baseline: Float) {
|
|
|
|
|
// テキストサイズをスケーリングした基本高さ
|
2023-02-21 20:20:24 +01:00
|
|
|
|
this.emojiHeight = textSize * scaleRatio * scale
|
2023-02-21 16:17:29 +01:00
|
|
|
|
|
|
|
|
|
// ベースラインから上下方向にずらすオフセット
|
2023-02-21 20:20:24 +01:00
|
|
|
|
val cDescent = emojiHeight * descentRatio
|
|
|
|
|
this.transY = baseline - emojiHeight + cDescent
|
2023-02-21 16:17:29 +01:00
|
|
|
|
|
|
|
|
|
val aspect = when(aspectArg){
|
|
|
|
|
null ->{
|
|
|
|
|
imageAspectCache[url] ?: 1f
|
|
|
|
|
}
|
|
|
|
|
else->{
|
|
|
|
|
imageAspectCache.put(url,aspectArg)
|
|
|
|
|
aspectArg
|
|
|
|
|
}
|
|
|
|
|
}.takeIf { it>0f }?:1f
|
|
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
// 横長画像で、それを許可するモード
|
2023-02-21 20:20:24 +01:00
|
|
|
|
aspect > 1.36f && sizeMode == EmojiSizeMode.Wide -> {
|
2023-02-21 16:17:29 +01:00
|
|
|
|
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
|
2023-02-21 20:20:24 +01:00
|
|
|
|
val dstWidth = min(maxEmojiWidth, aspect * emojiHeight)
|
2023-02-21 16:17:29 +01:00
|
|
|
|
val dstHeight = dstWidth / aspect
|
|
|
|
|
val dstX = 0f
|
2023-02-21 20:20:24 +01:00
|
|
|
|
val dstY = (emojiHeight - dstHeight) / 2f
|
2023-02-21 16:17:29 +01:00
|
|
|
|
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
|
2023-02-21 20:20:24 +01:00
|
|
|
|
emojiWidth = dstWidth
|
2023-02-21 16:17:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
2023-02-21 20:20:24 +01:00
|
|
|
|
emojiWidth = emojiHeight
|
2023-02-21 16:17:29 +01:00
|
|
|
|
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
|
|
|
|
|
val dstWidth: Float
|
|
|
|
|
val dstHeight: Float
|
|
|
|
|
if (aspect >= 1f) {
|
2023-02-21 20:20:24 +01:00
|
|
|
|
dstWidth = emojiHeight
|
|
|
|
|
dstHeight = emojiHeight / aspect
|
2023-02-21 16:17:29 +01:00
|
|
|
|
} else {
|
2023-02-21 20:20:24 +01:00
|
|
|
|
dstHeight = emojiHeight
|
|
|
|
|
dstWidth = emojiHeight * aspect
|
2023-02-21 16:17:29 +01:00
|
|
|
|
}
|
2023-02-21 20:20:24 +01:00
|
|
|
|
val dstX = (emojiHeight - dstWidth) / 2f
|
|
|
|
|
val dstY = (emojiHeight - dstHeight) / 2f
|
2023-02-21 16:17:29 +01:00
|
|
|
|
rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 出力サイズが変化したならrequestLayout
|
|
|
|
|
val newWidth = rectDst.width()
|
|
|
|
|
if (lastWidth != null && lastWidth != newWidth) {
|
|
|
|
|
log.i("updateRect: width changed. $lastWidth → $newWidth")
|
|
|
|
|
invalidateCallback?.requestLayout()
|
|
|
|
|
}
|
|
|
|
|
lastWidth = newWidth
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
override fun setInvalidateCallback(
|
|
|
|
|
drawTargetTag: Any,
|
2023-01-13 13:22:25 +01:00
|
|
|
|
invalidateCallback: AnimatableSpanInvalidator,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
) {
|
|
|
|
|
this.refDrawTarget = WeakReference(drawTargetTag)
|
|
|
|
|
this.invalidateCallback = invalidateCallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getSize(
|
|
|
|
|
paint: Paint,
|
|
|
|
|
text: CharSequence,
|
|
|
|
|
@IntRange(from = 0) start: Int,
|
|
|
|
|
@IntRange(from = 0) end: Int,
|
2023-01-13 13:22:25 +01:00
|
|
|
|
fm: Paint.FontMetricsInt?,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
): Int {
|
2023-02-21 16:17:29 +01:00
|
|
|
|
updateRect(aspectArg = null, paint.textSize, baseline = 0f)
|
2023-02-21 20:20:24 +01:00
|
|
|
|
val height = (emojiHeight + 0.5f).toInt()
|
2021-06-20 15:12:25 +02:00
|
|
|
|
if (fm != null) {
|
2023-02-21 16:17:29 +01:00
|
|
|
|
val cDescent = (0.5f + height * descentRatio).toInt()
|
|
|
|
|
val cAscent = cDescent - height
|
2021-06-20 15:12:25 +02:00
|
|
|
|
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
|
|
|
|
|
}
|
2023-02-21 20:20:24 +01:00
|
|
|
|
return (emojiWidth + 0.5f).toInt()
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun draw(
|
|
|
|
|
canvas: Canvas,
|
|
|
|
|
text: CharSequence,
|
|
|
|
|
start: Int,
|
|
|
|
|
end: Int,
|
|
|
|
|
x: Float,
|
|
|
|
|
top: Int,
|
|
|
|
|
baseline: Int,
|
|
|
|
|
bottom: Int,
|
2023-01-13 13:22:25 +01:00
|
|
|
|
textPaint: Paint,
|
2021-06-20 15:12:25 +02:00
|
|
|
|
) {
|
2023-02-08 10:55:49 +01:00
|
|
|
|
if (drawFrame(canvas, x, baseline, textPaint)) return
|
|
|
|
|
drawError(canvas, x, baseline, textPaint)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun drawFrame(
|
|
|
|
|
canvas: Canvas,
|
|
|
|
|
x: Float,
|
|
|
|
|
baseline: Int,
|
|
|
|
|
textPaint: Paint,
|
|
|
|
|
): Boolean {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val invalidateCallback = this.invalidateCallback
|
|
|
|
|
if (invalidateCallback == null) {
|
|
|
|
|
log.e("draw: invalidate_callback is null.")
|
2023-02-08 10:55:49 +01:00
|
|
|
|
return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// APNGデータの取得
|
|
|
|
|
val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) {
|
|
|
|
|
invalidateCallback.delayInvalidate(0L)
|
2023-02-08 10:55:49 +01:00
|
|
|
|
} ?: return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
val t = when {
|
2023-02-04 21:52:26 +01:00
|
|
|
|
PrefB.bpDisableEmojiAnimation.value -> 0L
|
2021-06-20 15:12:25 +02:00
|
|
|
|
else -> invalidateCallback.timeFromStart
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// アニメーション開始時刻からの経過時間に応じたフレームを探索
|
|
|
|
|
frames.findFrame(mFrameFindResult, t)
|
|
|
|
|
|
|
|
|
|
val b = mFrameFindResult.bitmap
|
|
|
|
|
if (b == null || b.isRecycled) {
|
|
|
|
|
log.e("draw: bitmap is null or recycled.")
|
2023-02-08 10:55:49 +01:00
|
|
|
|
return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
val srcWidth = b.width
|
|
|
|
|
val srcHeight = b.height
|
|
|
|
|
if (srcWidth < 1 || srcHeight < 1) {
|
|
|
|
|
log.e("draw: bitmap size is too small.")
|
2023-02-08 10:55:49 +01:00
|
|
|
|
return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
rectSrc.set(0, 0, srcWidth, srcHeight)
|
2023-02-21 16:17:29 +01:00
|
|
|
|
updateRect(
|
|
|
|
|
aspectArg = srcWidth.toFloat() / srcHeight.toFloat(),
|
|
|
|
|
textPaint.textSize,
|
|
|
|
|
baseline.toFloat()
|
|
|
|
|
)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
canvas.save()
|
|
|
|
|
try {
|
|
|
|
|
canvas.translate(x, transY)
|
|
|
|
|
canvas.drawBitmap(b, rectSrc, rectDst, mPaint)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.w(ex, "drawBitmap failed.")
|
|
|
|
|
|
|
|
|
|
// 10月6日 18:18(アプリのバージョン: 378) Sony Xperia X Compact(F5321), Android 8.0
|
|
|
|
|
// 10月6日 11:35(アプリのバージョン: 380) Samsung Galaxy S7 Edge(hero2qltetmo), Android 8.0
|
|
|
|
|
// 10月2日 21:56(アプリのバージョン: 376) Google Pixel 3(blueline), Android 9
|
|
|
|
|
// java.lang.RuntimeException:
|
|
|
|
|
// at android.graphics.BaseCanvas.throwIfCannotDraw (BaseCanvas.java:55)
|
|
|
|
|
// at android.view.DisplayListCanvas.throwIfCannotDraw (DisplayListCanvas.java:226)
|
|
|
|
|
// at android.view.RecordingCanvas.drawBitmap (RecordingCanvas.java:123)
|
|
|
|
|
// at jp.juggler.subwaytooter.span.NetworkEmojiSpan.draw (NetworkEmojiSpan.kt:137)
|
|
|
|
|
} finally {
|
|
|
|
|
canvas.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 少し後に描画しなおす
|
|
|
|
|
val delay = mFrameFindResult.delay
|
2023-02-04 21:52:26 +01:00
|
|
|
|
if (delay != Long.MAX_VALUE && !PrefB.bpDisableEmojiAnimation.value) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
invalidateCallback.delayInvalidate(delay)
|
|
|
|
|
}
|
2023-02-08 10:55:49 +01:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun drawError(
|
|
|
|
|
canvas: Canvas,
|
|
|
|
|
x: Float,
|
|
|
|
|
baseline: Int,
|
|
|
|
|
textPaint: Paint,
|
|
|
|
|
) {
|
|
|
|
|
val drawable = errorDrawableCache
|
|
|
|
|
?: ContextCompat.getDrawable(lazyContext, errorDrawableId)
|
|
|
|
|
?.also { errorDrawableCache = it }
|
|
|
|
|
|
|
|
|
|
drawable ?: return
|
2023-02-21 16:17:29 +01:00
|
|
|
|
val srcWidth = drawable.intrinsicWidth.toFloat()
|
|
|
|
|
val srcHeight = drawable.intrinsicHeight.toFloat()
|
2023-02-08 10:55:49 +01:00
|
|
|
|
|
2023-02-21 16:17:29 +01:00
|
|
|
|
updateRect(
|
|
|
|
|
aspectArg = srcWidth / srcHeight,
|
|
|
|
|
textSize = textPaint.textSize,
|
|
|
|
|
baseline = baseline.toFloat()
|
|
|
|
|
)
|
2023-02-08 10:55:49 +01:00
|
|
|
|
|
|
|
|
|
canvas.save()
|
|
|
|
|
try {
|
|
|
|
|
canvas.translate(x, transY)
|
|
|
|
|
drawable.setBounds(
|
2023-02-21 16:17:29 +01:00
|
|
|
|
rectDst.left.toInt(),
|
|
|
|
|
rectDst.top.toInt(),
|
|
|
|
|
rectDst.right.plus(0.5f).toInt(),
|
|
|
|
|
rectDst.bottom.plus(0.5f).toInt(),
|
2023-02-08 10:55:49 +01:00
|
|
|
|
)
|
|
|
|
|
drawable.draw(canvas)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.w(ex, "drawBitmap failed.")
|
|
|
|
|
|
|
|
|
|
// 10月6日 18:18(アプリのバージョン: 378) Sony Xperia X Compact(F5321), Android 8.0
|
|
|
|
|
// 10月6日 11:35(アプリのバージョン: 380) Samsung Galaxy S7 Edge(hero2qltetmo), Android 8.0
|
|
|
|
|
// 10月2日 21:56(アプリのバージョン: 376) Google Pixel 3(blueline), Android 9
|
|
|
|
|
// java.lang.RuntimeException:
|
|
|
|
|
// at android.graphics.BaseCanvas.throwIfCannotDraw (BaseCanvas.java:55)
|
|
|
|
|
// at android.view.DisplayListCanvas.throwIfCannotDraw (DisplayListCanvas.java:226)
|
|
|
|
|
// at android.view.RecordingCanvas.drawBitmap (RecordingCanvas.java:123)
|
|
|
|
|
// at jp.juggler.subwaytooter.span.NetworkEmojiSpan.draw (NetworkEmojiSpan.kt:137)
|
|
|
|
|
} finally {
|
|
|
|
|
canvas.restore()
|
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|