package jp.juggler.subwaytooter.view import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.SystemClock import android.util.AttributeSet import android.view.View import androidx.core.content.ContextCompat import jp.juggler.apng.ApngFrames import jp.juggler.subwaytooter.App1 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 @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) { private var url: String? = null private val tagDrawTarget: WeakReference by lazy { WeakReference(this) } // フレーム探索結果を格納する構造体を確保しておく private val mFrameFindResult = ApngFrames.FindFrameResult() // 最後に描画した時刻 private var tLastDraw = 0L // アニメーション開始時刻 private var tStart = 0L private val mPaint = Paint() private val rectSrc = Rect() private var errorDrawableCache: Drawable? = null private val emojiImageRect = EmojiImageRect( sizeMode = sizeMode, scale = scale, scaleRatio = 1f, descentRatio = 0f, maxEmojiWidth = maxEmojiWidth, // layout = { w, h -> // val lp = layoutParams // lp.width = w // lp.height = h // layoutParams = lp // requestLayout() // }, ) fun setEmoji( url: String?, initialAspect: Float?, defaultHeight: Int, ) { this.url = url mPaint.isFilterBitmap = true invalidate() 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 } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (drawImage(canvas)) return drawError(canvas) } private fun drawImage(canvas: Canvas): Boolean { val url = this.url if (url == null || url.isBlank()) return false // APNGデータの取得 val frames = App1.custom_emoji_cache.getFrames(tagDrawTarget, url) { postInvalidateOnAnimation() } ?: return false val now = SystemClock.elapsedRealtime() // アニメーション開始時刻を計算する if (tStart == 0L || now - tLastDraw >= 60000L) { tStart = now } tLastDraw = now // アニメーション開始時刻からの経過時間に応じたフレームを探索 frames.findFrame(mFrameFindResult, now - tStart) val b = mFrameFindResult.bitmap if (b == null || b.isRecycled) return false val srcWidth = b.width.toFloat() val srcHeight = b.height.toFloat() if (srcWidth < 1f || srcHeight < 1f) return false rectSrc.set(0, 0, b.width, b.height) val srcAspect = srcWidth / srcHeight val dstWidth = this.width.toFloat() val dstHeight = this.height.toFloat() if (dstWidth < 1f || dstHeight < 1f) return false emojiImageRect.updateRect( url = url, aspectArg = srcAspect, h = dstHeight, ) 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 } canvas.drawBitmap(b, rectSrc, emojiImageRect.rectDst, mPaint) // 少し後に描画しなおす val delay = mFrameFindResult.delay if (delay != Long.MAX_VALUE) { postInvalidateDelayed(delay) } 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() } } }