理解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动画停止?
大概率是两个原因:
- StickerView的默认实现对
invalidateDrawable支持不足:很多自定义View(包括StickerView)没有重写invalidateDrawable方法,导致系统无法正确识别要重绘的区域,新的Gif帧没有被渲染出来,看起来就像动画停止了。 - 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
相关产品推荐
相关产品推荐

