首页 > Java > java教程 > 正文

Android自定义视图:如何正确保存包含Drawable的状态

花韻仙語
发布: 2025-10-12 12:32:38
原创
863人浏览过

Android自定义视图:如何正确保存包含Drawable的状态

android自定义视图中,直接将drawable对象序列化为parcelable是不可行的,会导致classcastexception。本文将深入探讨这一问题,并提供有效的替代方案,指导开发者如何通过保存drawable的资源id或其他可序列化数据来正确地恢复视图状态,确保应用在配置变更后能保持一致的用户界面。

Drawable对象序列化的挑战

在Android开发中,自定义视图经常需要保存其内部状态,以便在配置变更(如屏幕旋转)后能够恢复。Android提供了Parcelable接口,允许对象在进程间或配置变更时进行高效序列化和反序列化。通常,我们会创建一个继承自BaseSavedState的内部类来封装需要保存的视图状态。

然而,当尝试直接保存Drawable对象时,开发者会遇到一个常见的问题:Drawable类本身并未实现Parcelable接口,也不是所有Drawable的子类都可直接序列化。例如,VectorDrawable等特定类型的Drawable对象,它们通常依赖于Resources上下文来创建和渲染,直接将其写入Parcel会导致运行时错误,如java.lang.ClassCastException: android.graphics.drawable.VectorDrawable cannot be cast to android.os.Parcelable。

直接序列化Drawable的尝试与失败

为了说明这一问题,我们来看一个典型的错误尝试。假设一个自定义视图需要保存一个Drawable对象和一个浮点数degree:

// 错误的SavedState实现示例
private static class SavedState extends BaseSavedState {
    Drawable picture; // 问题所在:Drawable不是Parcelable
    Float degree = 0f;

    public SavedState(Parcelable superState) {
        super(superState);
    }

    public SavedState(Parcel source) {
        super(source);
        degree = source.readFloat();
        // 尝试读取Drawable,会抛出ClassCastException
        picture = source.readParcelable(getClass().getClassLoader());
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        super.writeToParcel(out, flags);
        out.writeFloat(degree);
        // 尝试写入Drawable,如果picture不是Parcelable,会抛出ClassCastException
        out.writeParcelable((Parcelable) picture, flags);
    }

    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];
        }
    };
}
登录后复制

当视图尝试通过onSaveInstanceState()和onRestoreInstanceState()方法保存和恢复状态时,如果picture是一个VectorDrawable,上述代码将在writeToParcel或readParcelable时引发ClassCastException。

尝试将Drawable转换为BitmapDrawable

有开发者可能会尝试将Drawable转换为BitmapDrawable,因为Bitmap实现了Parcelable接口。然而,这种方法并非总是有效:

// 错误的转换尝试
protected Parcelable onSaveInstanceState() {
    // ... 其他代码
    Bitmap bitmap = ((BitmapDrawable) picture).getBitmap(); // 问题:VectorDrawable不能直接转换为BitmapDrawable
    out.writeParcelable(bitmap, flags);
    // ... 其他代码
}
登录后复制

如果原始的Drawable是VectorDrawable而不是BitmapDrawable,这种转换会再次导致ClassCastException。VectorDrawable和BitmapDrawable是Drawable的不同子类,它们之间没有直接的继承或转换关系。

推荐方案:保存Drawable的资源ID

鉴于Drawable对象本身难以直接序列化,最安全和推荐的做法是不要保存Drawable对象本身,而是保存能够重建该Drawable对象所需的数据。对于从资源文件(如res/drawable)加载的Drawable,其资源ID(int类型)是重建它的关键信息。

存了个图
存了个图

视频图片解析/字幕/剪辑,视频高清保存/图片源图提取

存了个图 17
查看详情 存了个图

原理阐述

Android的Resources系统在应用启动时加载资源,并根据设备配置进行管理。Drawable对象通常是通过Context.getResources().getDrawable(int id)方法从资源ID创建的。这个资源ID是一个简单的整数,可以很容易地序列化。在恢复状态时,我们可以利用保存的资源ID和当前的Context来重新创建Drawable对象。

实现细节

  1. 在SavedState中保存资源ID: 将SavedState类中的Drawable字段替换为int类型的资源ID字段。
  2. 在onSaveInstanceState()中获取并保存ID: 在自定义视图中,当Drawable被设置时,如果它是从资源加载的,就应该将其资源ID存储在一个成员变量中,然后在onSaveInstanceState()中将这个ID保存到SavedState。
  3. 在onRestoreInstanceState()中重建Drawable: 从SavedState中读取资源ID,然后使用getContext().getResources().getDrawable(id, getContext().getTheme())(或ContextCompat.getDrawable(getContext(), id))方法重新创建Drawable对象。

示例代码:使用资源ID保存自定义视图状态

以下是修改后的SavedState和自定义视图状态保存/恢复的示例代码:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; // 推荐使用ContextCompat兼容旧版本API

public class MyCustomView extends View {

    private Drawable picture;
    private float degree = 0f;
    private int drawableResId = 0; // 保存Drawable的资源ID

    public MyCustomView(Context context) {
        super(context);
        init(null);
    }

    public MyCustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public MyCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(@Nullable AttributeSet attrs) {
        // 假设这里初始化了picture,例如从XML属性或默认资源加载
        // 为了演示,我们手动设置一个默认的Drawable
        // 注意:在实际应用中,你需要确保drawableResId在Drawable被设置时同步更新
        setPictureFromResource(R.drawable.ic_launcher_foreground); // 假设这是一个VectorDrawable
        // 其他初始化逻辑...
    }

    // 设置Drawable的方法,同时保存其资源ID
    public void setPictureFromResource(int resId) {
        this.drawableResId = resId;
        this.picture = ContextCompat.getDrawable(getContext(), resId);
        if (this.picture != null) {
            this.picture.setBounds(0, 0, getWidth(), getHeight()); // 设置边界,根据实际需求调整
        }
        invalidate(); // 刷新视图
    }

    // 设置Drawable的方法,如果不是来自资源,则drawableResId可以设置为0
    public void setPicture(Drawable drawable) {
        this.picture = drawable;
        this.drawableResId = 0; // 非资源Drawable不保存ID
        if (this.picture != null) {
            this.picture.setBounds(0, 0, getWidth(), getHeight());
        }
        invalidate();
    }

    public void setDegree(float degree) {
        this.degree = degree;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.rotate(degree, getWidth() / 2f, getHeight() / 2f);
        if (picture != null) {
            picture.draw(canvas);
        }
        canvas.restore();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState myState = new SavedState(superState);
        myState.degree = this.degree;
        myState.drawableResId = this.drawableResId; // 保存资源ID
        return myState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        // 检查状态类型,避免ClassCastException
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        this.degree = savedState.degree;
        this.drawableResId = savedState.drawableResId;

        // 根据保存的资源ID重新创建Drawable
        if (this.drawableResId != 0) {
            this.picture = ContextCompat.getDrawable(getContext(), this.drawableResId);
            if (this.picture != null) {
                this.picture.setBounds(0, 0, getWidth(), getHeight()); // 重新设置边界
            }
        } else {
            this.picture = null; // 如果没有ID,则清除Drawable
        }
        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(); // 读取资源ID
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(degree);
            out.writeInt(drawableResId); // 写入资源ID
        }

        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];
            }
        };
    }
}
登录后复制

在上述代码中,MyCustomView通过drawableResId字段来追踪当前Drawable的资源ID。在onSaveInstanceState中,这个ID被写入Parcel;在onRestoreInstanceState中,它被读取并用于通过ContextCompat.getDrawable()方法重新加载Drawable。

注意事项与最佳实践

  1. 非资源Drawable的处理: 如果你的Drawable不是从资源加载的(例如,它是动态创建的ShapeDrawable或GradientDrawable),那么保存资源ID的方法就不适用。在这种情况下,你需要保存创建该Drawable所需的所有参数(如颜色、形状类型、尺寸、渐变属性等),然后在恢复时根据这些参数重新构建Drawable。
  2. 保持SavedState轻量: Parcelable的目的是高效传输数据。避免在SavedState中保存大型或复杂的对象图。只保存恢复视图状态所必需的最小数据集。
  3. Context的可用性: 在onRestoreInstanceState中,View的Context是可用的,因此可以安全地调用getContext().getResources().getDrawable()。
  4. API兼容性: 使用ContextCompat.getDrawable(context, resId)可以确保在不同Android版本上的兼容性,因为它会自动处理旧版API中getDrawable方法的废弃问题,并支持主题(theme)属性。

总结

在Android自定义视图中保存包含Drawable的状态时,直接序列化Drawable对象是行不通的。核心思想是不要保存Drawable对象本身,而是保存能够重建Drawable所需的数据。对于从资源加载的Drawable,最有效和推荐的方法是保存其资源ID。通过这种方式,可以在配置变更后,利用资源系统重新创建Drawable,从而确保视图状态的正确恢复和应用界面的稳定性。

以上就是Android自定义视图:如何正确保存包含Drawable的状态的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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