android.animation.AnimationHandler instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 233011 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ AnimationHandler.mAnimationCallbacks
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~~~~~~~~~~~~~~~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ java.util.ArrayList instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 233002 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ ArrayList[0]
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ android.animation.ValueAnimator instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 232952 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ mListeners = null
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ ValueAnimator.mUpdateListeners
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~~~~~~~~~~~~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ java.util.ArrayList instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 232939 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ ArrayList[0]
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ com.xxlink.libskeleton.AnimationManager$$ExternalSyntheticLambda1 instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 232937 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ AnimationManager$$ExternalSyntheticLambda1.f$0
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ com.xxlink.libskeleton.AnimationManager instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: UNKNOWN
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 232936 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ AnimationManager.targetView
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ~~~~~~~~~~
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ com.xxlink.design.card.xxConstraintCardView instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: YES (View.mContext references a destroyed activity)
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Retaining 11.7 MB in 232923 objects
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ View not part of a window view hierarchy
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ View.mAttachInfo is null (view detached)
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ View.mWindowAttachCount = 1
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity xx.sdncontroller.
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ui.monitor.SdnControllerMonitorActivity with mDestroyed = true
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ ↓ View.mContext
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D ├─ androidx.appcompat.view.ContextThemeWrapper instance
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ Leaking: YES (xxConstraintCardView↑ is leaking and ContextThemeWrapper wraps an Activity with Activity.mDestroyed
2025-12-14 16:18:28.927 9328-9578 LeakCanary xx D │ true)class AnimationManager(
private val targetView: View,
private val config: SkeletonConfig = SkeletonDefaults.defaultConfig
) {
private var animator: ValueAnimator? = null
private val appliedDrawables = mutableListOf<Pair<View, Drawable>>()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// 用于监听布局变化,以便在宽高改变时重启动画
private var currentLayoutChangeListener: View.OnLayoutChangeListener? = null
/**
* 启动 Shimmer 动画
* 若已有动画运行,则先停止再启动新动画
*/
fun start() {
stop() // 清理旧状态
if (targetView.width == 0 || targetView.height == 0) {
targetView.post { setupAndStart() }
} else {
setupAndStart()
}
}
private fun setupAndStart() {
// 创建并添加布局变化监听器
val layoutChangeListener =
View.OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
val newWidth = right - left
val newHeight = bottom - top
val oldWidth = oldRight - oldLeft
val oldHeight = oldBottom - oldTop
if (newWidth != oldWidth || newHeight != oldHeight) {
restart()
}
}
targetView.addOnLayoutChangeListener(layoutChangeListener)
currentLayoutChangeListener = layoutChangeListener
// 立即启动一次
restart()
}
/**
* 停止当前动画并清理所有资源
*/
fun stop() {
stopAnimator()
currentLayoutChangeListener?.let { targetView.removeOnLayoutChangeListener(it) }
currentLayoutChangeListener = null
appliedDrawables.forEach { (view, drawable) ->
try {
view.overlay.remove(drawable)
} catch (e: Exception) {
Log.e("AnimationManager", "Failed to remove overlay from view", e)
}
}
appliedDrawables.clear()
}
/**
* 停止当前动画并立即以最新尺寸重新启动
*/
private fun restart() {
stopAnimator()
startAnimator()
}
/**
* 创建并启动新的 shimmer 动画
*/
private fun startAnimator() {
val animator = ValueAnimator.ofFloat(-1f, 2f).apply {
duration = config.shimmerDuration
repeatCount = ValueAnimator.INFINITE
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
updateShader(fraction)
invalidateAllDrawables()
}
}
this.animator = animator
animator.start()
// 应用统一 shimmer drawable
applyShimmerToTargetView()
}
/**
* 停止动画但不清除监听器
*/
private fun stopAnimator() {
animator?.cancel()
animator = null
}
/**
* 更新渐变着色器(Shader),实现移动的高亮光效
*/
private fun updateShader(offsetFraction: Float) {
val width = targetView.width.toFloat()
val height = targetView.height.toFloat()
if (width <= 0f || height <= 0f) return
val bandWidth = width * config.shimmerWidth
val angleRad = Math.toRadians(config.shimmerAngle.toDouble())
val cos = Math.cos(angleRad).toFloat()
val sin = Math.sin(angleRad).toFloat()
val diagonal = Math.sqrt((width * width + height * height).toDouble()).toFloat()
val offset = offsetFraction * diagonal
val startX = -diagonal * cos + offset
val startY = -diagonal * sin + offset
val endX = startX + bandWidth * cos
val endY = startY + bandWidth * sin
val colors = intArrayOf(
Color.TRANSPARENT,
ContextCompat.getColor(targetView.context, config.shimmerColorRes),
Color.TRANSPARENT
)
val positions = floatArrayOf(0f, 0.5f, 1f)
val shader = LinearGradient(startX, startY, endX, endY, colors, positions, Shader.TileMode.CLAMP)
paint.shader = shader
}
/**
* 触发所有 shimmer drawable 重绘,显示最新 shader 效果
*/
private fun invalidateAllDrawables() {
appliedDrawables.forEach { (_, drawable) -> drawable.invalidateSelf() }
}
/**
* 只创建一个 drawable,挂在最外层 targetView
* 并在 draw 时递归裁剪子 view 的可见区域
*/
private fun applyShimmerToTargetView() {
appliedDrawables.forEach { (view, drawable) -> view.overlay.remove(drawable) }
appliedDrawables.clear()
val unifiedDrawable = object : Drawable() {
private val rectF = RectF()
private val clipPath = Path()
override fun draw(canvas: Canvas) {
if (paint.shader == null) return
clipPath.reset()
collectVisibleChildPaths(targetView, clipPath)
canvas.save()
canvas.clipPath(clipPath)
rectF.set(bounds)
canvas.drawRect(rectF, paint)
canvas.restore()
}
private fun collectVisibleChildPaths(view: View, path: Path) {
if (view.visibility != View.VISIBLE || view.tag == "ignore_ani") return
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
collectVisibleChildPaths(view.getChildAt(i), path)
}
} else {
val location = IntArray(2)
view.getLocationOnScreen(location)
val rootLocation = IntArray(2)
targetView.getLocationOnScreen(rootLocation)
val left = (location[0] - rootLocation[0]).toFloat()
val top = (location[1] - rootLocation[1]).toFloat()
val right = left + view.width
val bottom = top + view.height
// 尝试从背景中提取圆角信息
val cornerRadius = ViewUtils.dp2px(targetView.context,config.cornerRadius).toFloat()
if (cornerRadius > 0f) {
path.addRoundRect(
left, top, right, bottom,
cornerRadius, cornerRadius,
Path.Direction.CW
)
} else {
path.addRect(left, top, right, bottom, Path.Direction.CW)
}
}
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}.apply { setBounds(0, 0, targetView.width, targetView.height) }
targetView.overlay.add(unifiedDrawable)
appliedDrawables.add(targetView to unifiedDrawable)
}
}fun ViewGroup.showSkeleton(config: SkeletonConfig = SkeletonDefaults.defaultConfig) {
hideSkeleton()
val resId = config.customLayoutRes
// 如果 resId 为空,直接返回
resId ?: return
val skeletonView = LayoutInflater.from(context).inflate(resId, this, false)
// 根据容器类型生成适配的布局参数
val layoutParams = generateOverlayLayoutParams()
// 防止点击/触摸事件穿透
setupTouchInterceptor(skeletonView)
val existingListener = getTag(R.id.tag_attach_listener) as? View.OnAttachStateChangeListener
if (existingListener == null) {
val attachListener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
hideSkeleton()
v.removeOnAttachStateChangeListener(this)
v.setTag(R.id.tag_attach_listener, null)
}
}
this.addOnAttachStateChangeListener(attachListener)
this.setTag(R.id.tag_attach_listener, attachListener)
}
addView(skeletonView, layoutParams)
val animation = AnimationManager(skeletonView as ViewGroup, config)
animation.start()
setTag(R.id.tag_skeleton_view, SkeletonView(skeletonView, animation))
}
private fun ViewGroup.generateOverlayLayoutParams(): ViewGroup.LayoutParams {
return when (this) {
is FrameLayout -> FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
).apply {
gravity = Gravity.START or Gravity.TOP
}
is LinearLayout -> LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
is RelativeLayout -> RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
).apply {
addRule(RelativeLayout.ALIGN_PARENT_START)
addRule(RelativeLayout.ALIGN_PARENT_TOP)
addRule(RelativeLayout.ALIGN_PARENT_END)
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
}
is ConstraintLayout -> ConstraintLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, // MATCH_CONSTRAINT
0 // MATCH_CONSTRAINT
).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
startToStart = ConstraintLayout.LayoutParams.PARENT_ID
endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
}
else -> ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
private fun setupTouchInterceptor(view: View) {
view.setOnTouchListener { _, _ ->
true
}
}
/**
* 隐藏 ViewGroup 的骨架屏,恢复原始子 View 状态
*/
fun ViewGroup.hideSkeleton() {
// 通过 tag 找到骨架 View,停止动画并移除
val skeletonTag = getTag(R.id.tag_skeleton_view) as? SkeletonView ?: return
val skeletonView = skeletonTag.view
val animationManager = skeletonTag.animationManager
// 停止所有动画
animationManager.stop()
// 从父布局中移除骨架 View
if (skeletonView.parent == this) {
removeView(skeletonView)
}
// 清理 Tag,避免重复添加或内存泄漏
setTag(R.id.tag_skeleton_view, null)
}
fun ViewGroup.skeleton(): SkeletonBuilder {
return SkeletonBuilder(this)
}这个为什么会泄露呢,明明如果detach了就会执行hide销毁动画啊
最新发布