package com.example.screentest
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Matrix
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewOutlineProvider
import android.view.animation.AnticipateOvershootInterpolator
import androidx.core.animation.doOnEnd
import androidx.core.content.withStyledAttributes
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
/**
* @author liuzhen
* 流光动效
* [R.styleable.ShimmerView_src] loading drawable
* [R.styleable.ShimmerView_shimmerType]see[ShimmerType]
* [R.styleable.ShimmerView_radius] 圆角
*/
open class ShimmerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "ShimmerView"
const val SHIMMER_DURATION = 2000L
}
private val shimmerColors = intArrayOf(
Color.TRANSPARENT,
resources.getColor(R.color.color_shimmer2, context.theme),
resources.getColor(R.color.color_shimmer1, context.theme),
resources.getColor(R.color.color_shimmer2, context.theme),
Color.TRANSPARENT,
)
private val positions = floatArrayOf(0f, 0.35f, 0.5f, 0.65f, 1f)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val lightRectF = RectF()
private val drawRectF = RectF()
private val mShaderMatrix = Matrix()
private var diffX = 0f
private var isStart = false
private var isInit = false
private var lightWidth = 0f
private var extraWidth = 0f
private var mHeight = 0f
private var mRotate = 30f
private var shimmerType = ShimmerType.BANNER.type
private val shimmerAnimator = ValueAnimator().apply {
duration = SHIMMER_DURATION
repeatCount = ValueAnimator.INFINITE
interpolator = AnticipateOvershootInterpolator(0.3f)
addUpdateListener { anim ->
val num = anim.animatedValue as Float
diffX = num
updateGradient()
}
doOnEnd {
isStart = false
diffX = lightWidth
updateGradient()
}
}
init {
context.withStyledAttributes(attrs, R.styleable.ShimmerView) {
val shimmerType = getInt(R.styleable.ShimmerView_shimmerType, ShimmerType.BANNER.type)
val radius = getDimension(R.styleable.ShimmerView_radius, 0f)
init(shimmerType, radius)
}
}
fun init(type: Int, radius: Float) {
shimmerType = type
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, measuredWidth, measuredHeight, radius)
}
}
}
fun startShimmer() {
if (shimmerAnimator.isStarted || shimmerAnimator.isRunning || isStart) return
Log.i(TAG, "startShimmer isInit=$isInit")
if (isInit) {
isStart = true
shimmerAnimator.setFloatValues(-(lightWidth + extraWidth), width + lightWidth)
shimmerAnimator.start()
}
}
fun stopShimmer() {
Log.i(TAG, "stopShimmer")
shimmerAnimator.cancel()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 根据旋转角度计算合适的绘制区域宽度
val rotationRadians = Math.toRadians(mRotate.toDouble())
val cosValue = abs(cos(rotationRadians))
val sinValue = abs(sin(rotationRadians))
// 计算旋转后需要的额外宽度,确保覆盖整个可见区域
extraWidth = (h * sinValue / cosValue).toFloat()
lightWidth = w / if (shimmerType == ShimmerType.ITEM.type) 1.2f else 4f
mHeight = h.toFloat()
isInit = true
log("onSizeChanged type=$shimmerType,width=$w,height=$h,lightWidth=$lightWidth,extraWidth=$extraWidth")
if (!isStart) startShimmer()
}
private fun updateGradient() {
mShaderMatrix.reset()
lightRectF.set(diffX, 0f, diffX + lightWidth, mHeight)
// 设置可见区域,避免多余消耗
drawRectF.set(
max(0f, diffX - extraWidth), 0f,
min(width.toFloat(), lightRectF.right + extraWidth), mHeight
)
paint.shader = LinearGradient(
lightRectF.left, lightRectF.top,
lightRectF.right, lightRectF.top,
shimmerColors, positions,
Shader.TileMode.CLAMP
).apply {
mShaderMatrix.setRotate(mRotate, lightRectF.centerX(), lightRectF.centerY())
setLocalMatrix(mShaderMatrix)
}
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (!isStart) return
canvas.drawRect(drawRectF, paint)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startShimmer()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.i(TAG, "onDetachedFromWindow type=$shimmerType")
stopShimmer()
}
private fun log(str: String) = Log.i(TAG, str)
}
enum class ShimmerType(val type: Int) {
/** 流光宽度较小 */
BANNER(0),
/** 流光宽度较大 */
ITEM(1),
}