shimmer

使用 LinearGradient 绘制渐变区域,然后旋转角度,可以根据情况跳转扫光区域的大小

针对大量扫光动效同时进行时,需要对绘制进行优化,否则过渡消耗CPU性能

drawRectF.set(
    max(0f, diffX - extraWidth), 0f,
    min(width.toFloat(), lightRectF.right + extraWidth), mHeight
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (!isStart) return
canvas.drawRect(drawRectF, paint)
}
 

只绘制扫光区域,并且对不可见区域不做绘制,暂停时不做绘制

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),
}
View Code
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.screentest.ShimmerView
        android:id="@+id/shimmer_view1"
        android:layout_width="match_parent"
        android:layout_height="350dp"
        android:layout_marginHorizontal="50dp"
        android:layout_marginTop="20dp"
        android:background="@android:color/background_dark"
        app:radius="24dp"
        app:shimmerType="banner" />

    <com.example.screentest.ShimmerView
        android:id="@+id/shimmer_view2"
        android:layout_width="530dp"
        android:layout_height="300dp"
        android:layout_marginHorizontal="50dp"
        android:layout_marginTop="400dp"
        android:background="@android:color/background_dark"
        app:radius="24dp"
        app:shimmerType="item" />

</FrameLayout>
activity_test
<declare-styleable name="ShimmerView">
        <!-- Sets a drawable as the content of this ImageView. -->
        <attr name="src" format="reference|color" />
        <!-- com.example.screentest.ShimmerType -->
        <attr name="shimmerType" format="enum">
            <enum name="banner" value="0" />
            <enum name="item" value="1" />
        </attr>
        <attr name="radius" format="dimension" />
</declare-styleable>
attrs

image

非旋转状态下起始位置绘制

image

旋转30度后,在起始位置绘制

 

posted on 2026-04-23 17:11  翻滚的咸鱼  阅读(12)  评论(0)    收藏  举报