Drawable 与 Bitmap 的区别、互转与自定义

Bitmap 是什么

Bitmap 的字面意思是"位图",它是图片被解码后,在内存中的表现形式。它映射了每个像素的颜色信息,所以可以直接被系统用于渲染。

可以用一张图来形象地描述它:

Drawable 是什么

Drawable 不像 Bitmap,它不是一个图片信息的存储工具,它是绘制工具。

Drawable 可调用 Canvas 进行绘制,就像 View 一样。不过 View 负责测量、布局和绘制,Drawable 只负责绘制。Drawable 内部存储的是绘制操作或绘制规则,Bitmap 存储的是像素信息。

使用 Drawable 只需这样:

kotlin 复制代码
// 创建Drawable对象
private val drawable = ColorDrawable(Color.parseColor("#FF4081"))
private val size = 150.dp


// 使用前,需要调用setBounds方法设置Drawable的边界
drawable.setBounds(
    (width / 2f - size / 2f).toInt(),
    (height / 2f - size / 2f).toInt(),
    (width / 2f + size / 2f).toInt(),
    (height / 2f + size / 2f).toInt()
)
drawable.draw(canvas)

运行效果:

Drawable 和 Bitmap 的互转

其实 Drawable 和 Bitmap 的互转,只是用其中一个实例创建另一个实例,两者并没有真正的转换。以官方代码为例,BitmapDrawable 的代码是这样的:

kotlin 复制代码
public inline fun Bitmap.toDrawable(resources: Resources): BitmapDrawable =
    // 创建BitmapDrawable对象 
    BitmapDrawable(resources, this)

BitmapDrawable 在实际绘制时,只是将传入的 Bitmap 对象,使用 Canvas 绘制出来。

DrawableBitmap 的代码是这样的:

kotlin 复制代码
public fun Drawable.toBitmap(
    @Px width: Int = intrinsicWidth,
    @Px height: Int = intrinsicHeight,
    config: Config? = null
): Bitmap {
    if (this is BitmapDrawable) {
        if (bitmap == null) {
            // This is slightly better than returning an empty, zero-size bitmap.
            throw IllegalArgumentException("bitmap is null")
        }
        if (config == null || bitmap.config == config) {
            // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
            // involves allocation and two jumps into native code so we perform the check ourselves.
            if (width == bitmap.width && height == bitmap.height) {
                return bitmap
            }
            return Bitmap.createScaledBitmap(bitmap, width, height, true)
        }
    }

    val (oldLeft, oldTop, oldRight, oldBottom) = bounds

    val bitmap = Bitmap.createBitmap(width, height, config ?: Config.ARGB_8888)
    setBounds(0, 0, width, height)
    draw(Canvas(bitmap))

    setBounds(oldLeft, oldTop, oldRight, oldBottom)
    return bitmap
}

如果当前是 BitmapDrawable,直接返回内部存储的 Bitmap 对象,否则将当前 Drawable 绘制在新建的 Bitmap 对象上并返回。

这就是转换的本质。

自定义 Drawable

我们先来看看如何自定义 Drawable,比如我们要创建网格线的 Drawable

需要继承自 Drawable 抽象类,然后实现各个抽象方法。

kotlin 复制代码
class MeshDrawable : Drawable(){
    override fun draw(canvas: Canvas) {
        TODO("Not yet implemented")
    }

    override fun setAlpha(alpha: Int) {
        TODO("Not yet implemented")
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        TODO("Not yet implemented")
    }

    @Deprecated("Deprecated in Java")
    override fun getOpacity(): Int {
        TODO("Not yet implemented")
    }
}

draw 方法是绘制的核心逻辑,setAlpha 方法用于设置透明度,setColorFilter 方法用于设置颜色滤镜,getOpacity 方法用于返回透明度信息。

对于设置透明度,只需将传入的透明度设置到内部的 Paint 对象上即可。

kotlin 复制代码
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

override fun setAlpha(alpha: Int) {
    paint.alpha = alpha
    invalidateSelf() // 通知系统重绘
}

在更新了 Paint 的属性后,需要调用 invalidateSelf 方法通知 Drawable 的持有者进行重绘,这样才能应用到最新的透明度。

同理,颜色滤镜也是一样。

kotlin 复制代码
override fun setColorFilter(colorFilter: ColorFilter?) {
    paint.colorFilter = colorFilter
    // 同样调用invalidateSelf方法
    invalidateSelf()
}

因为网格线之间存在间隙,永远不会完全覆盖下层内容,所以属于半透明(TRANSLUCENT)类型,需要与下层图像进行混合。

kotlin 复制代码
@Deprecated("Deprecated in Java",
    ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat")
)
override fun getOpacity(): Int {
    return PixelFormat.TRANSLUCENT
}

最后,来绘制网格线。和自定义 View 绘制一样,代码如下:

kotlin 复制代码
class MeshDrawable(
    private val meshSize: Float = 50f,
    private val meshColor: Int = Color.GRAY,
) : Drawable() {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = meshColor
        strokeWidth = 2f
    }

    override fun draw(canvas: Canvas) {
        val bounds = bounds

        // 画垂直线
        var x = bounds.left.toFloat()
        while (x <= bounds.right) {
            canvas.drawLine(x, bounds.top.toFloat(), x, bounds.bottom.toFloat(), paint)
            x += meshSize
        }

        // 画水平线
        var y = bounds.top.toFloat()
        while (y <= bounds.bottom) {
            canvas.drawLine(bounds.left.toFloat(), y, bounds.right.toFloat(), y, paint)
            y += meshSize
        }
    }

    ...
}

使用:

kotlin 复制代码
private val drawable = MeshDrawable(meshSize = 50.dp)
override fun onDraw(canvas: Canvas) {
    drawable.setBounds(
        0,
        0,
        width,
        height
    )
    drawable.draw(canvas)
}

运行效果:

知道了 Drawable 的用法,我们自然会引出一个问题,它的使用场景是什么?

首先,它可以动态绘制图像,相较于静态图片来说更加灵活,就比如我们当前的示例,我们可以指定它的格子大小,网格线范围,以及网格线的粗细和颜色。

但它的核心价值在于作为独立的绘制模块,可在不同的自定义 View 之间共享绘制代码。

就比如,我们要在消息列表中展示一个小尺寸的头像(SimpleAvatarView),还要在个人资料页中展示一个大尺寸、带有光环的头像(DetailedAvatarView),我们就可以将核心的绘制逻辑(将方形头像裁剪为圆形),封装到一个自定义 AvatarDrawable 中。

对于 SimpleAvatarView 来说,只需直接使用这个 AvatarDrawableDetailedAvatarView 只需在 AvatarDrawable 的基础上,额外绘制一个光环即可。

这样,就实现了复用,可以避免代码重复,还可以减少不必要的 View 嵌套以提升性能。

没有自定义 Bitmap

我们并不能自定义 Bitmap,因为类声明被 final 修饰了。

其实也很容易想明白,Bitmap 的职责是纯粹的数据存储,并不需要像 Drawable 那样为其添加额外的绘制行为。

相关推荐
程序员江同学3 小时前
Kotlin 技术月报 | 2025 年 8 月
android·kotlin
nju永远得不到的男人4 小时前
关于virtual camera
android
雨白6 小时前
Android 自定义 View:属性动画和硬件加速
android
hellokai7 小时前
React Native新架构源码分析
android·前端·react native
真西西7 小时前
Koin:Kotlin轻量级依赖注入框架
android·kotlin
CYRUS_STUDIO10 小时前
手把手教你改造 AAR:解包、注入逻辑、重打包,一条龙玩转第三方 SDK!
android·逆向
CYRUS_STUDIO10 小时前
Android 源码如何导入 Android Studio?踩坑与解决方案详解
android·android studio·源码阅读
前端赵哈哈11 小时前
初学者入门:Android 实现 Tab 点击切换(TabLayout + ViewPager2)
android·java·android studio
一条上岸小咸鱼15 小时前
Kotlin 控制流(二):返回和跳转
android·kotlin