2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.view
|
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
import android.annotation.SuppressLint
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.graphics.Canvas
|
|
|
|
|
import android.graphics.Paint
|
|
|
|
|
import android.graphics.Rect
|
2023-02-22 16:04:00 +01:00
|
|
|
|
import android.graphics.drawable.Drawable
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.os.SystemClock
|
|
|
|
|
import android.util.AttributeSet
|
|
|
|
|
import android.view.View
|
2023-02-22 16:04:00 +01:00
|
|
|
|
import androidx.core.content.ContextCompat
|
2018-01-28 20:03:04 +01:00
|
|
|
|
import jp.juggler.apng.ApngFrames
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2023-02-22 16:04:00 +01:00
|
|
|
|
import jp.juggler.subwaytooter.R
|
|
|
|
|
import jp.juggler.subwaytooter.pref.lazyContext
|
|
|
|
|
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
|
|
|
|
import jp.juggler.subwaytooter.util.EmojiImageRect
|
|
|
|
|
import jp.juggler.subwaytooter.util.EmojiSizeMode
|
|
|
|
|
import java.lang.ref.WeakReference
|
2018-01-28 20:03:04 +01:00
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
@SuppressLint("ViewConstructor")
|
|
|
|
|
class NetworkEmojiView(
|
|
|
|
|
context: Context,
|
|
|
|
|
attrs: AttributeSet? = null,
|
|
|
|
|
defStyleAttr: Int = 0,
|
|
|
|
|
sizeMode: EmojiSizeMode,
|
|
|
|
|
maxEmojiWidth: Float,
|
|
|
|
|
scale: Float = 1f,
|
|
|
|
|
private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
|
|
|
|
|
) : View(context, attrs, defStyleAttr) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
private var url: String? = null
|
|
|
|
|
|
|
|
|
|
private val tagDrawTarget: WeakReference<Any> by lazy { WeakReference(this) }
|
|
|
|
|
|
|
|
|
|
// フレーム探索結果を格納する構造体を確保しておく
|
|
|
|
|
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
|
|
|
|
|
|
|
|
|
// 最後に描画した時刻
|
|
|
|
|
private var tLastDraw = 0L
|
|
|
|
|
|
|
|
|
|
// アニメーション開始時刻
|
|
|
|
|
private var tStart = 0L
|
|
|
|
|
|
|
|
|
|
private val mPaint = Paint()
|
2023-02-22 16:04:00 +01:00
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
private val rectSrc = Rect()
|
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
private var errorDrawableCache: Drawable? = null
|
|
|
|
|
|
|
|
|
|
private val emojiImageRect = EmojiImageRect(
|
|
|
|
|
sizeMode = sizeMode,
|
|
|
|
|
scale = scale,
|
|
|
|
|
scaleRatio = 1f,
|
|
|
|
|
descentRatio = 0f,
|
|
|
|
|
maxEmojiWidth = maxEmojiWidth,
|
2023-02-26 00:15:57 +01:00
|
|
|
|
// layout = { w, h ->
|
|
|
|
|
// val lp = layoutParams
|
|
|
|
|
// lp.width = w
|
|
|
|
|
// lp.height = h
|
|
|
|
|
// layoutParams = lp
|
|
|
|
|
// requestLayout()
|
|
|
|
|
// },
|
2023-02-22 16:04:00 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fun setEmoji(
|
|
|
|
|
url: String?,
|
|
|
|
|
initialAspect: Float?,
|
|
|
|
|
defaultHeight: Int,
|
|
|
|
|
) {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
this.url = url
|
|
|
|
|
mPaint.isFilterBitmap = true
|
|
|
|
|
invalidate()
|
2023-02-22 22:59:35 +01:00
|
|
|
|
emojiImageRect.updateRect(
|
|
|
|
|
url = url ?: "",
|
|
|
|
|
aspectArg = initialAspect,
|
|
|
|
|
h = defaultHeight.toFloat(),
|
|
|
|
|
)
|
|
|
|
|
val lp = layoutParams
|
|
|
|
|
lp.width = (emojiImageRect.emojiWidth + 0.5f).toInt()
|
|
|
|
|
lp.height = (emojiImageRect.emojiHeight + 0.5f).toInt()
|
|
|
|
|
layoutParams = lp
|
2021-06-20 15:12:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDraw(canvas: Canvas) {
|
|
|
|
|
super.onDraw(canvas)
|
2023-02-22 16:04:00 +01:00
|
|
|
|
if (drawImage(canvas)) return
|
|
|
|
|
drawError(canvas)
|
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
private fun drawImage(canvas: Canvas): Boolean {
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val url = this.url
|
2023-02-22 16:04:00 +01:00
|
|
|
|
if (url == null || url.isBlank()) return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
// APNGデータの取得
|
|
|
|
|
val frames = App1.custom_emoji_cache.getFrames(tagDrawTarget, url) {
|
|
|
|
|
postInvalidateOnAnimation()
|
2023-02-22 16:04:00 +01:00
|
|
|
|
} ?: return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
|
|
|
|
|
|
// アニメーション開始時刻を計算する
|
|
|
|
|
if (tStart == 0L || now - tLastDraw >= 60000L) {
|
|
|
|
|
tStart = now
|
|
|
|
|
}
|
|
|
|
|
tLastDraw = now
|
|
|
|
|
|
|
|
|
|
// アニメーション開始時刻からの経過時間に応じたフレームを探索
|
|
|
|
|
frames.findFrame(mFrameFindResult, now - tStart)
|
|
|
|
|
|
|
|
|
|
val b = mFrameFindResult.bitmap
|
2023-02-22 16:04:00 +01:00
|
|
|
|
if (b == null || b.isRecycled) return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
val srcWidth = b.width.toFloat()
|
|
|
|
|
val srcHeight = b.height.toFloat()
|
2023-02-22 16:04:00 +01:00
|
|
|
|
if (srcWidth < 1f || srcHeight < 1f) return false
|
|
|
|
|
rectSrc.set(0, 0, b.width, b.height)
|
|
|
|
|
|
2021-06-20 15:12:25 +02:00
|
|
|
|
val srcAspect = srcWidth / srcHeight
|
|
|
|
|
|
|
|
|
|
val dstWidth = this.width.toFloat()
|
|
|
|
|
val dstHeight = this.height.toFloat()
|
2023-02-22 16:04:00 +01:00
|
|
|
|
if (dstWidth < 1f || dstHeight < 1f) return false
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
emojiImageRect.updateRect(
|
|
|
|
|
url = url,
|
|
|
|
|
aspectArg = srcAspect,
|
|
|
|
|
h = dstHeight,
|
|
|
|
|
)
|
2023-02-26 00:15:57 +01:00
|
|
|
|
val width = (emojiImageRect.emojiWidth + 0.5f).toInt()
|
|
|
|
|
if (width != layoutParams.width) {
|
|
|
|
|
val lp = layoutParams
|
|
|
|
|
lp.width = width
|
|
|
|
|
lp.height = (emojiImageRect.emojiHeight + 0.5f).toInt()
|
|
|
|
|
layoutParams = lp
|
|
|
|
|
}
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
2023-02-22 16:04:00 +01:00
|
|
|
|
canvas.drawBitmap(b, rectSrc, emojiImageRect.rectDst, mPaint)
|
2021-06-20 15:12:25 +02:00
|
|
|
|
|
|
|
|
|
// 少し後に描画しなおす
|
|
|
|
|
val delay = mFrameFindResult.delay
|
|
|
|
|
if (delay != Long.MAX_VALUE) {
|
|
|
|
|
postInvalidateDelayed(delay)
|
|
|
|
|
}
|
2023-02-22 16:04:00 +01:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun drawError(canvas: Canvas) {
|
|
|
|
|
val drawable = errorDrawableCache
|
|
|
|
|
?: ContextCompat.getDrawable(lazyContext, errorDrawableId)
|
|
|
|
|
?.also { errorDrawableCache = it }
|
|
|
|
|
|
|
|
|
|
drawable ?: return
|
|
|
|
|
val srcWidth = drawable.intrinsicWidth.toFloat()
|
|
|
|
|
val srcHeight = drawable.intrinsicHeight.toFloat()
|
|
|
|
|
if (srcWidth < 1f || srcHeight < 1f) return
|
|
|
|
|
|
|
|
|
|
val dstWidth = this.width.toFloat()
|
|
|
|
|
val dstHeight = this.height.toFloat()
|
|
|
|
|
if (dstWidth < 1f || dstHeight < 1f) return
|
|
|
|
|
|
|
|
|
|
emojiImageRect.updateRect(
|
|
|
|
|
url = "",
|
|
|
|
|
aspectArg = srcWidth / srcHeight,
|
|
|
|
|
h = dstHeight,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
canvas.save()
|
|
|
|
|
try {
|
|
|
|
|
canvas.translate(x, emojiImageRect.transY)
|
|
|
|
|
drawable.setBounds(
|
|
|
|
|
emojiImageRect.rectDst.left.toInt(),
|
|
|
|
|
emojiImageRect.rectDst.top.toInt(),
|
|
|
|
|
emojiImageRect.rectDst.right.plus(0.5f).toInt(),
|
|
|
|
|
emojiImageRect.rectDst.bottom.plus(0.5f).toInt(),
|
|
|
|
|
)
|
|
|
|
|
drawable.draw(canvas)
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
NetworkEmojiSpan.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
|
|
|
|
}
|