package expo.modules.gaodemap.map.overlays import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Rect import android.os.Handler import android.os.Looper import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.graphics.createBitmap import expo.modules.gaodemap.map.companion.IconBitmapCache import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.text.StringBuilder import androidx.core.view.isEmpty import androidx.core.graphics.get import androidx.core.view.isNotEmpty internal data class MarkerBitmapSnapshot( val keyPart: String, val width: Int, val height: Int, ) { val fullCacheKey: String get() = "$keyPart|${width}x${height}" } internal object MarkerBitmapRenderer { fun hasPendingAsyncImageContent(view: View?): Boolean { view ?: return false if (view.visibility != View.VISIBLE) return false if (view is ImageView) { val resolvedWidth = view.measuredWidth.takeIf { it > 0 } ?: view.width val resolvedHeight = view.measuredHeight.takeIf { it > 0 } ?: view.height if (resolvedWidth > 0 && resolvedHeight > 0 && view.drawable == null) { return true } } if (view is ViewGroup) { for (index in 0 until view.childCount) { if (hasPendingAsyncImageContent(view.getChildAt(index))) { return true } } } return false } fun resolveSnapshot( container: ViewGroup, contentWidth: Int, contentHeight: Int, cacheKey: String?, ): MarkerBitmapSnapshot? { if (container.isEmpty()) { return null } val child = container.getChildAt(0) ?: return null if (hasPendingAsyncImageContent(child)) { return null } val contentView = resolveRenderableContentView(child) val contentBounds = computeContentBounds(child) val measuredWidth = contentBounds?.width() ?: contentView?.measuredWidth ?: child.measuredWidth val measuredHeight = contentBounds?.height() ?: contentView?.measuredHeight ?: child.measuredHeight val finalWidth = if (measuredWidth > 0) measuredWidth else contentWidth val finalHeight = if (measuredHeight > 0) measuredHeight else contentHeight if (finalWidth <= 0 || finalHeight <= 0) { return null } // 业务层已经显式传入稳定 cacheKey 时,直接信任它。 // 否则点击、重排或 drawable 实例抖动仍会让 key 变化,导致 children marker 反复重建。 val keyPart = cacheKey?.takeIf { it.isNotBlank() } ?: computeViewFingerprint(container) return MarkerBitmapSnapshot( keyPart = keyPart, width = finalWidth, height = finalHeight, ) } fun createBitmap( container: ViewGroup, snapshot: MarkerBitmapSnapshot, contentWidth: Int, contentHeight: Int, mainHandler: Handler, ): Bitmap? { IconBitmapCache.get(snapshot.fullCacheKey)?.let { return it } val bitmap = if (Looper.myLooper() == Looper.getMainLooper()) { renderViewToBitmapInternal( container = container, finalWidth = snapshot.width, finalHeight = snapshot.height, contentWidth = contentWidth, contentHeight = contentHeight, ) } else { val latch = CountDownLatch(1) var result: Bitmap? = null mainHandler.post { try { result = renderViewToBitmapInternal( container = container, finalWidth = snapshot.width, finalHeight = snapshot.height, contentWidth = contentWidth, contentHeight = contentHeight, ) } finally { latch.countDown() } } try { latch.await(200, TimeUnit.MILLISECONDS) } catch (_: InterruptedException) { } result } bitmap?.let { IconBitmapCache.put(snapshot.fullCacheKey, it) } return bitmap } fun computeContentBounds(view: View?): Rect? { view ?: return null if (view.visibility != View.VISIBLE) return null var resolvedBounds: Rect? = null if (view is ViewGroup && view.isNotEmpty()) { for (i in 0 until view.childCount) { val child = view.getChildAt(i) ?: continue val childBounds = computeContentBounds(child) ?: continue val shiftedBounds = Rect(childBounds) shiftedBounds.offset(child.left, child.top) resolvedBounds = if (resolvedBounds == null) { shiftedBounds } else { Rect(resolvedBounds).apply { union(shiftedBounds) } } } } val hasOwnVisualBounds = view.background != null || view.paddingLeft != 0 || view.paddingTop != 0 || view.paddingRight != 0 || view.paddingBottom != 0 || view !is ViewGroup val ownWidth = view.measuredWidth.takeIf { it > 0 } ?: view.width val ownHeight = view.measuredHeight.takeIf { it > 0 } ?: view.height if (hasOwnVisualBounds && ownWidth > 0 && ownHeight > 0) { val ownBounds = Rect(0, 0, ownWidth, ownHeight) resolvedBounds = if (resolvedBounds == null) { ownBounds } else { Rect(resolvedBounds).apply { union(ownBounds) } } } return resolvedBounds } fun resolveRenderableContentView(view: View?): View? { var current = view ?: return null while (current is ViewGroup && current.childCount == 1) { val next = current.getChildAt(0) ?: break current = next } return current } fun computeViewFingerprint(view: View?): String { if (view == null) return "null" val sb = StringBuilder() val tag = view.tag if (tag != null) { sb.append("tag=").append(tag.toString()).append(";") return sb.toString() } val contentDesc = view.contentDescription if (!contentDesc.isNullOrEmpty()) { sb.append("cdesc=").append(contentDesc.toString()).append(";") return sb.toString() } fun appendFor(v: View) { sb.append(v.javaClass.simpleName) when (v) { is TextView -> { val text = v.text?.toString() ?: "" if (text.isNotEmpty()) { sb.append("[text=").append(text).append("]") } } is ImageView -> { val resId = v.tag if (resId is Int && resId != 0) { sb.append("[imgRes=").append(resId).append("]") } else { val drawable = v.drawable if (drawable != null) { val constantState = drawable.constantState if (constantState != null) { sb.append("[drawableState=").append(constantState.hashCode()).append("]") } else { sb .append("[drawable=") .append(drawable.javaClass.simpleName) .append(':') .append(drawable.intrinsicWidth) .append('x') .append(drawable.intrinsicHeight) .append("]") } } } } } sb.append(";") if (v is ViewGroup) { for (i in 0 until v.childCount) { appendFor(v.getChildAt(i)) } } } appendFor(view) return sb.toString().take(1024) } private fun renderViewToBitmapInternal( container: ViewGroup, finalWidth: Int, finalHeight: Int, contentWidth: Int, contentHeight: Int, ): Bitmap? { try { val childView = container.getChildAt(0) ?: return null if (childView.width != finalWidth || childView.height != finalHeight) { if (childView.width == 0 || childView.height == 0) { return null } val widthSpec = View.MeasureSpec.makeMeasureSpec(finalWidth, View.MeasureSpec.EXACTLY) val heightSpec = View.MeasureSpec.makeMeasureSpec(finalHeight, View.MeasureSpec.EXACTLY) childView.measure(widthSpec, heightSpec) childView.layout(0, 0, finalWidth, finalHeight) } else if (childView.left != 0 || childView.top != 0) { childView.layout(0, 0, finalWidth, finalHeight) } val bitmap = createBitmap(finalWidth, finalHeight) val canvas = Canvas(bitmap) childView.draw(canvas) val shouldTrimTransparentPadding = contentWidth <= 0 && contentHeight <= 0 return if (shouldTrimTransparentPadding) trimTransparentPadding(bitmap) else bitmap } catch (_: Exception) { return null } } private fun trimTransparentPadding(bitmap: Bitmap): Bitmap { if (bitmap.width <= 1 || bitmap.height <= 1) { return bitmap } if (hasOpaquePixelsOnAllBitmapEdges(bitmap)) { return bitmap } var minX = bitmap.width var minY = bitmap.height var maxX = -1 var maxY = -1 for (y in 0 until bitmap.height) { for (x in 0 until bitmap.width) { if (Color.alpha(bitmap[x, y]) != 0) { if (x < minX) minX = x if (y < minY) minY = y if (x > maxX) maxX = x if (y > maxY) maxY = y } } } if (maxX < minX || maxY < minY) { return bitmap } val trimmedWidth = maxX - minX + 1 val trimmedHeight = maxY - minY + 1 if (trimmedWidth == bitmap.width && trimmedHeight == bitmap.height) { return bitmap } return Bitmap.createBitmap(bitmap, minX, minY, trimmedWidth, trimmedHeight) } private fun hasOpaquePixelsOnAllBitmapEdges(bitmap: Bitmap): Boolean { var topEdgeHasPixel = false var bottomEdgeHasPixel = false var leftEdgeHasPixel = false var rightEdgeHasPixel = false for (x in 0 until bitmap.width) { if (!topEdgeHasPixel && Color.alpha(bitmap[x, 0]) != 0) { topEdgeHasPixel = true } if (!bottomEdgeHasPixel && Color.alpha(bitmap[x, bitmap.height - 1]) != 0) { bottomEdgeHasPixel = true } if (topEdgeHasPixel && bottomEdgeHasPixel) { break } } for (y in 0 until bitmap.height) { if (!leftEdgeHasPixel && Color.alpha(bitmap[0, y]) != 0) { leftEdgeHasPixel = true } if (!rightEdgeHasPixel && Color.alpha(bitmap[bitmap.width - 1, y]) != 0) { rightEdgeHasPixel = true } if (leftEdgeHasPixel && rightEdgeHasPixel) { break } } return topEdgeHasPixel && bottomEdgeHasPixel && leftEdgeHasPixel && rightEdgeHasPixel } }