惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
D
DataBreaches.Net
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The GitHub Blog
The GitHub Blog
Blog — PlanetScale
Blog — PlanetScale
Microsoft Security Blog
Microsoft Security Blog
A
About on SuperTechFans
Vercel News
Vercel News
L
LangChain Blog
B
Blog RSS Feed
Y
Y Combinator Blog
IT之家
IT之家
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
V
V2EX
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
有赞技术团队
有赞技术团队
D
Docker
V
Visual Studio Blog
aimingoo的专栏
aimingoo的专栏
Last Week in AI
Last Week in AI
月光博客
月光博客

博客园 - 翻滚的咸鱼

Ops权限行为记录源码解析 TaskView方案框架原理 视频解码器问题 常用命令 引导记录 性能优化 View共享动效 常用脚本文件 Launcher 卡片框架多模块集成 Launcher 桌面源码笔记二 Launcher 桌面源码笔记一 氛围灯动态屏保取色方案二 氛围灯动态屏保取色方案一 自定义最近任务管理器 anr问题 TV RecyclerView 焦点处理笔记 多类型适配器 多屏下字体自动取色 阻尼拉伸方案 图片区域点击处理 RecyclerView设置默认焦点跟多页面焦点抢占 TV RecyclerView 滑动后操作保持落焦在左侧第一个View Fragment翻盖动效 当个View下,使用Drawable入场退场动画
扫光动效
翻滚的咸鱼 · 2026-04-23 · via 博客园 - 翻滚的咸鱼

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度后,在起始位置绘制