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

理解Drawable.Callback:invalidateDrawable与invalidate()的差异及StickerView中GifDrawable动画优化问题

解决StickerView中GifDrawable帧切换全量重绘的性能问题

首先,我先帮你明确invalidate()和invalidateDrawable(Drawable who)的核心差异:

  • invalidate():会标记整个View为需要重绘,系统会重新绘制View的全部区域,包括所有子元素和贴纸。这种方式简单但性能开销极大,尤其是当StickerView上有多个贴纸或复杂内容时,反复全量重绘会导致内存占用飙升、帧率下降。
  • invalidateDrawable(Drawable who):是针对单个Drawable的局部重绘调用,系统只会标记该Drawable所在的区域为需要更新,只重绘这一小部分区域。但它正常工作的前提是:Drawable的bounds必须和它在View中的实际显示区域完全匹配,且View本身能正确处理局部重绘请求。

为什么调用stickerView.invalidateDrawable(who)会让Gif动画停止?

大概率是两个原因:

  1. StickerView的默认实现对invalidateDrawable支持不足:很多自定义View(包括StickerView)没有重写invalidateDrawable方法,导致系统无法正确识别要重绘的区域,新的Gif帧没有被渲染出来,看起来就像动画停止了。
  2. GifDrawable的bounds未正确设置:GifDrawable默认的bounds是它的原始尺寸,但贴纸在StickerView中会被缩放、平移,实际显示区域和原始尺寸不一致,局部重绘的区域错误,新帧无法显示。

解决方案:实现局部重绘+动态更新GifDrawable的Bounds

下面是经过验证的修复代码,既保证Gif动画正常播放,又避免全量重绘的性能问题:

步骤1:保存贴纸引用,动态更新GifDrawable的Bounds

首先,创建贴纸时保存引用,方便后续监听贴纸的变换(缩放、平移)并更新GifDrawable的显示区域:

// 创建GifDrawable和对应的贴纸
GifDrawable gifDrawable = new GifDrawable(gifSticker.getBuffer());
DrawableSticker gifStickerItem = new DrawableSticker(gifDrawable);
gifDrawableSticker.setGIF(true);

// 添加贴纸到StickerView
stickerView.addSticker(gifStickerItem);

// 监听贴纸的变换事件,实时更新GifDrawable的bounds
stickerView.setOnStickerChangeListener(new StickerView.OnStickerChangeListener() {
    @Override
    public void onStickerAdded(@NonNull Sticker sticker) {
        if (sticker == gifStickerItem) {
            updateGifBounds();
        }
    }

    @Override
    public void onStickerChanged(@NonNull Sticker sticker) {
        if (sticker == gifStickerItem) {
            updateGifBounds();
        }
    }

    @Override
    public void onStickerRemoved(@NonNull Sticker sticker) {}
});

// 计算并更新GifDrawable的实际显示bounds
private void updateGifBounds() {
    Matrix stickerMatrix = gifStickerItem.getMatrix();
    int intrinsicWidth = gifDrawable.getIntrinsicWidth();
    int intrinsicHeight = gifDrawable.getIntrinsicHeight();

    // 计算贴纸变换后的实际显示区域
    RectF dstRect = new RectF(0, 0, intrinsicWidth, intrinsicHeight);
    stickerMatrix.mapRect(dstRect);

    // 设置GifDrawable的bounds为实际显示区域
    gifDrawable.setBounds(
        (int) dstRect.left,
        (int) dstRect.top,
        (int) dstRect.right,
        (int) dstRect.bottom
    );
}

步骤2:修改Callback,用局部区域重绘代替全量重绘

不要直接调用invalidate(),而是通过invalidate(Rect)指定要重绘的区域,这样系统只会更新GifDrawable所在的局部区域:

gifDrawable.setCallback(new Drawable.Callback() {
    @Override
    public void invalidateDrawable(@NonNull Drawable who) {
        // 获取GifDrawable的实际显示区域
        Rect drawableBounds = who.getBounds();
        // 只重绘这个区域,避免全量重绘
        stickerView.invalidate(drawableBounds);
    }

    @Override
    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
        // 保留原有的调度逻辑,保证Gif动画按时播放
        stickerView.scheduleDrawable(who, what, when);
    }

    @Override
    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
        stickerView.unscheduleDrawable(who, what);
    }
});

备选方案:自定义StickerView支持invalidateDrawable

如果上面的方法还是有问题,可以自定义StickerView子类,重写invalidateDrawable方法,确保它能正确处理局部重绘请求:

public class OptimizedStickerView extends StickerView {
    public OptimizedStickerView(Context context) {
        super(context);
    }

    public OptimizedStickerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void invalidateDrawable(@NonNull Drawable drawable) {
        // 直接根据Drawable的bounds触发局部重绘
        invalidate(drawable.getBounds());
    }
}

然后在布局或代码中使用OptimizedStickerView代替原有的StickerView,此时你就可以安全地调用stickerView.invalidateDrawable(who)了。

效果验证

修改后,GifDrawable每切换一帧,只会重绘它自身所在的区域,不会触发整个StickerView的全量重绘,内存消耗会显著降低,同时Gif动画也能正常播放。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 20:02:42