0

0

Android自定义视图状态保存:避免直接序列化Drawable对象

花韻仙語

花韻仙語

发布时间:2025-10-13 12:39:53

|

725人浏览过

|

来源于php中文网

原创

android自定义视图状态保存:避免直接序列化drawable对象

在Android自定义视图中保存`Drawable`对象的状态时,直接将其序列化为`Parcelable`是不可行的,因为`Drawable`及其子类通常不实现`Parcelable`接口,会导致`ClassCastException`。正确的做法是避免直接保存`Drawable`实例,转而保存其资源ID或重建所需的数据,并在恢复状态时重新创建`Drawable`。

问题剖析:为何Drawable不应直接序列化

在Android开发中,当自定义视图需要在屏幕旋转、内存不足等情况下保存和恢复其内部状态时,通常会使用onSaveInstanceState()和onRestoreInstanceState()方法,并结合Parcelable接口来定义视图的保存状态类。然而,尝试直接将Drawable对象(如VectorDrawable、BitmapDrawable等)存储到Parcelable中时,会遇到java.lang.ClassCastException,提示Drawable类型无法转换为Parcelable。

这是因为Drawable是一个抽象基类,其具体实现类(例如VectorDrawable、ShapeDrawable、BitmapDrawable等)通常并未实现Parcelable接口。Parcelable接口要求对象能够被序列化和反序列化,以便在进程间或组件间传递数据。由于Drawable的复杂性及其与资源、上下文的紧密关联,Android框架并未提供一个通用的、安全的Drawable到Parcelable的转换机制。因此,直接尝试通过Parcel写入或读取Drawable实例会导致运行时类型转换异常。

常见误区与解决方案分析

开发者在遇到此问题时,常会尝试以下几种方法,但往往未能奏效:

  1. 直接将Drawable作为Parcelable写入/读取: 这是最直接的尝试,如原始问题所示,将Drawable字段直接放入SavedState中,并在writeToParcel和readFromParcel中使用out.writeParcelable()和source.readParcelable()。这会立即触发ClassCastException,因为Drawable对象并非Parcelable。

  2. 尝试转换为BitmapDrawable或Bitmap: 有些开发者会尝试将Drawable转换为BitmapDrawable,然后提取其内部的Bitmap对象,因为Bitmap是Parcelable的。

    // 尝试在writeToParcel中
    Bitmap bitmap = ((BitmapDrawable) picture).getBitmap();
    out.writeParcelable(bitmap, flags);
    
    // 尝试在readFromParcel中
    Bitmap bitmap = source.readParcelable(getClass().getClassLoader());
    picture = new BitmapDrawable(getResources(), bitmap);

    这种方法的问题在于:

    • 并非所有Drawable都能安全地转换为BitmapDrawable。例如,VectorDrawable是基于矢量路径而非像素的,强制转换会再次导致ClassCastException。
    • 即使成功转换为Bitmap,对于复杂的Drawable(如带状态的StateListDrawable、LevelListDrawable),这种转换会丢失其动态行为和元数据,只保存了某一时刻的像素快照。
    • Bitmap的序列化和反序列化涉及到大量的像素数据传输,可能导致性能问题和内存开销。

推荐方案:基于资源ID的Drawable状态管理

鉴于直接序列化Drawable的局限性,最稳健和推荐的方法是不保存Drawable实例本身,而是保存足以在恢复时重建Drawable的信息。对于从资源文件中加载的Drawable,这意味着保存其资源ID。

意兔-AI漫画相机
意兔-AI漫画相机

照片变漫画手绘,做周边好物

下载

核心思想

  • 在自定义视图中,如果Drawable是从res/drawable文件夹加载的,视图应该存储该Drawable的资源ID(例如R.drawable.my_image)。
  • 在onSaveInstanceState()中,将这个资源ID保存到Parcelable状态对象中。
  • 在onRestoreInstanceState()中,从状态对象中读取资源ID,然后使用getResources().getDrawable(id, context.getTheme())方法重新加载并创建Drawable实例。

示例代码

假设你的自定义视图中有一个Drawable字段picture,它通常通过一个资源ID来设置。

  1. 定义SavedState类: 修改SavedState类,用一个int类型的字段drawableResId来替代原来的Drawable picture。

    import android.graphics.drawable.Drawable;
    import android.os.Parcel;
    import android.os.Parcelable;
    import android.view.View;
    
    // 假设这是你的自定义视图类
    public class MyCustomView extends View {
        private Drawable picture;
        private Float degree = 0f;
        private int currentDrawableResId = 0; // 用于保存当前Drawable的资源ID
    
        // 构造函数等其他代码...
    
        public MyCustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            // 假设在初始化时,通过某个资源ID设置了picture
            // setPictureFromResource(R.drawable.default_image);
        }
    
        // 设置Drawable的方法,同时保存其资源ID
        public void setPictureFromResource(@DrawableRes int resId) {
            this.currentDrawableResId = resId;
            this.picture = getResources().getDrawable(resId, getContext().getTheme());
            invalidate();
        }
    
        // ... 其他视图逻辑 ...
    
        private static class SavedState extends BaseSavedState {
            int drawableResId; // 存储Drawable的资源ID
            Float degree = 0f;
    
            public SavedState(Parcelable superState) {
                super(superState);
            }
    
            public SavedState(Parcel source) {
                super(source);
                degree = source.readFloat();
                drawableResId = source.readInt(); // 从Parcel中读取资源ID
            }
    
            @Override
            public void writeToParcel(Parcel out, int flags) {
                super.writeToParcel(out, flags);
                out.writeFloat(degree);
                out.writeInt(drawableResId); // 将资源ID写入Parcel
            }
    
            public static final Parcelable.Creator<SavedState> CREATOR
                    = new Parcelable.Creator<SavedState>() {
                public SavedState createFromParcel(Parcel in) {
                    return new SavedState(in);
                }
    
                public SavedState[] newArray(int size) {
                    return new SavedState[size];
                }
            };
        }
    
        @Nullable
        @Override
        protected Parcelable onSaveInstanceState() {
            Parcelable superState = super.onSaveInstanceState();
            SavedState myState = new SavedState(superState);
            myState.degree = this.degree;
            myState.drawableResId = this.currentDrawableResId; // 保存当前Drawable的资源ID
            return myState;
        }
    
        @Override
        protected void onRestoreInstanceState(Parcelable state) {
            SavedState savedState = (SavedState) state;
            super.onRestoreInstanceState(savedState.getSuperState());
            this.degree = savedState.degree;
            // 使用保存的资源ID重新创建Drawable
            if (savedState.drawableResId != 0) { // 检查ID是否有效
                this.currentDrawableResId = savedState.drawableResId;
                this.picture = getResources().getDrawable(savedState.drawableResId, getContext().getTheme());
            }
            invalidate(); // 刷新视图
        }
    }

注意事项

  • currentDrawableResId的维护: 关键在于自定义视图本身需要有一个机制来存储当前Drawable的资源ID。当Drawable被设置时(例如通过setPictureFromResource()方法),这个ID也应该被更新。
  • getResources().getDrawable(): 在onRestoreInstanceState中,可以直接调用视图实例的getResources()方法来获取Resources对象,因为onRestoreInstanceState是一个实例方法,可以访问视图的上下文。为了兼容不同的API级别和主题,建议使用getResources().getDrawable(id, getContext().getTheme())。
  • 默认值处理: 如果drawableResId为0(表示没有设置Drawable或设置的是非资源Drawable),在恢复时应进行相应的处理,避免加载空ID。

其他情况下的Drawable状态管理

并非所有Drawable都来源于资源文件。对于以下情况,需要采取不同的策略:

  1. 动态生成的Drawable (例如ShapeDrawable, GradientDrawable): 这些Drawable是通过代码创建的,不依赖于资源ID。在这种情况下,你需要保存创建这些Drawable所需的所有属性(例如颜色、形状类型、尺寸、渐变方向等)。在onRestoreInstanceState中,根据这些属性重新构建Drawable。

    // 假设有一个动态生成的ShapeDrawable
    // SavedState中需要保存:int shapeType, int solidColor, int strokeWidth, int strokeColor, float cornerRadius;
    // onSaveInstanceState中:保存这些属性
    // onRestoreInstanceState中:
    // GradientDrawable shapeDrawable = new GradientDrawable();
    // shapeDrawable.setShape(savedState.shapeType);
    // shapeDrawable.setColor(savedState.solidColor);
    // ... 重新构建
  2. 非资源BitmapDrawable: 如果Drawable确实是一个BitmapDrawable,并且其内部的Bitmap数据是动态生成或加载的(而非来自资源),那么可以考虑直接保存Bitmap对象。Bitmap类实现了Parcelable接口,可以直接写入和读取。

    // SavedState中:Bitmap bitmap;
    // onSaveInstanceState中:
    // Bitmap bitmap = ((BitmapDrawable) picture).getBitmap();
    // myState.bitmap = bitmap;
    // onRestoreInstanceState中:
    // Bitmap restoredBitmap = savedState.bitmap;
    // if (restoredBitmap != null) {
    //     this.picture = new BitmapDrawable(getResources(), restoredBitmap);
    // }

    注意: 序列化Bitmap会占用较多内存和Parcel空间,可能影响性能,并有TransactionTooLargeException的风险。尽量避免在Parcelable中直接传递大Bitmap。如果Bitmap数据量大,更推荐将其保存到文件或数据库,然后只传递文件路径或ID。

总结

在Android自定义视图中保存Drawable状态的核心原则是:不直接序列化Drawable实例,而是保存重建它所需的最少信息。 对于资源Drawable,保存其资源ID是最佳实践;对于动态生成的Drawable,保存其构建属性;对于非资源的BitmapDrawable,虽然可以直接序列化Bitmap,但需注意性能和内存开销。通过这种方式,可以有效避免ClassCastException,并确保自定义视图在状态恢复时的正确性。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1051

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

615

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

335

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

235

2025.08.29

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1969

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

658

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2405

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

47

2026.01.19

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

49

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.4万人学习

Java 教程
Java 教程

共578课时 | 82.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号