You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中使用AnimatedImageDrawable在Canvas上正确绘制GIF的方法

我来帮你搞定这个问题!你当前的代码只绘制了GIF的第一帧就结束了,根本没给后续帧刷新画布的机会——AnimatedImageDrawable本身不会自动触发SurfaceView重绘,得我们主动配合它的动画节奏来更新画布。下面是完整的实现方案,Kotlin和Java版本都有:

Kotlin 实现方案

我们需要给AnimatedImageDrawable注册动画回调,让它在每一帧准备好时通知SurfaceView刷新画布,同时遵循SurfaceView的生命周期管理资源:

class GifSurfaceView(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), SurfaceHolder.Callback {
    private var victoryGif: AnimatedImageDrawable? = null
    private var holder: SurfaceHolder? = null
    
    // 监听GIF帧更新的回调
    private val animationCallback = object : AnimatedImageDrawable.AnimationCallback() {
        override fun onAnimationFrame(drawable: Drawable?) {
            // 每一帧就绪时,触发画布重绘
            drawCurrentFrame()
        }
    }

    init {
        holder = this.holder
        holder?.addCallback(this)
        // 初始化GIF资源
        try {
            val victorySource = ImageDecoder.createSource(context.assets, "victory.gif")
            victoryGif = ImageDecoder.decodeDrawable(victorySource) as AnimatedImageDrawable
            victoryGif?.registerAnimationCallback(animationCallback)
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // Surface创建完成后启动GIF动画
        victoryGif?.start()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Surface销毁时清理资源,防止内存泄漏
        victoryGif?.stop()
        victoryGif?.unregisterAnimationCallback(animationCallback)
        victoryGif = null
    }

    private fun drawCurrentFrame() {
        val canvas = holder?.lockCanvas() ?: return
        try {
            // 清空画布(可选,根据你的背景需求调整)
            canvas.drawColor(Color.BLACK)
            victoryGif?.let { gif ->
                // 计算缩放比例,让GIF居中显示
                val scale = min(canvas.width.toFloat() / gif.intrinsicWidth, canvas.height.toFloat() / gif.intrinsicHeight)
                val left = (canvas.width - gif.intrinsicWidth * scale) / 2
                val top = (canvas.height - gif.intrinsicHeight * scale) / 2
                // 设置GIF的绘制区域
                gif.setBounds(left.toInt(), top.toInt(), (left + gif.intrinsicWidth * scale).toInt(), (top + gif.intrinsicHeight * scale).toInt())
                // 绘制当前帧
                gif.draw(canvas)
            }
        } finally {
            // 必须解锁并提交画布,否则无法显示绘制内容
            holder?.unlockCanvasAndPost(canvas)
        }
    }
}
Java 实现方案

如果习惯用Java开发,对应的实现逻辑完全一致:

public class GifSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private AnimatedImageDrawable victoryGif;
    private SurfaceHolder holder;
    
    private final AnimatedImageDrawable.AnimationCallback animationCallback = new AnimatedImageDrawable.AnimationCallback() {
        @Override
        public void onAnimationFrame(@Nullable Drawable drawable) {
            drawCurrentFrame();
        }
    };

    public GifSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        holder = getHolder();
        holder.addCallback(this);
        // 初始化GIF资源
        try {
            ImageDecoder.Source victorySource = ImageDecoder.createSource(context.getAssets(), "victory.gif");
            victoryGif = (AnimatedImageDrawable) ImageDecoder.decodeDrawable(victorySource);
            victoryGif.registerAnimationCallback(animationCallback);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        if (victoryGif != null) {
            victoryGif.start();
        }
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        if (victoryGif != null) {
            victoryGif.stop();
            victoryGif.unregisterAnimationCallback(animationCallback);
            victoryGif = null;
        }
    }

    private void drawCurrentFrame() {
        Canvas canvas = holder.lockCanvas();
        if (canvas == null) return;
        try {
            canvas.drawColor(Color.BLACK);
            if (victoryGif != null) {
                float scale = Math.min((float) canvas.getWidth() / victoryGif.getIntrinsicWidth(),
                        (float) canvas.getHeight() / victoryGif.getIntrinsicHeight());
                int left = (int) ((canvas.getWidth() - victoryGif.getIntrinsicWidth() * scale) / 2);
                int top = (int) ((canvas.getHeight() - victoryGif.getIntrinsicHeight() * scale) / 2);
                victoryGif.setBounds(left, top,
                        (int) (left + victoryGif.getIntrinsicWidth() * scale),
                        (int) (top + victoryGif.getIntrinsicHeight() * scale));
                victoryGif.draw(canvas);
            }
        } finally {
            holder.unlockCanvasAndPost(canvas);
        }
    }
}
关键要点解释
  • 为什么原代码只显示第一帧?因为你只调用了一次draw(),后续动画帧没有机会被绘制到画布上。
  • 为什么用AnimationCallback?它能精准匹配GIF每帧的更新时机,比定时任务更可靠,避免卡顿或帧丢失。
  • 一定要管理Surface生命周期:在surfaceCreated启动动画,surfaceDestroyed停止并释放资源,防止内存泄漏。

内容的提问来源于stack exchange,提问作者himmels

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 23:08:12