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 那样为其添加额外的绘制行为。

相关推荐
范特西林13 小时前
实战演练——从零实现一个高性能 Binder 服务
android
范特西林14 小时前
代码的生成:AIDL 编译器与 Parcel 的序列化艺术
android
范特西林14 小时前
深入内核:Binder 驱动的内存管理与事务调度
android
范特西林15 小时前
解剖麻雀:Binder 通信的整体架构全景图
android
范特西林15 小时前
破冰之旅:为什么 Android 选择了 Binder?
android
奔跑中的蜗牛66616 小时前
一次播放器架构升级:Android 直播间 ANR 下降 60%
android
测试工坊18 小时前
Android 视频播放卡顿检测——帧率之外的第二战场
android
Kapaseker20 小时前
一杯美式深入理解 data class
android·kotlin
鹏多多20 小时前
Flutter使用screenshot进行截屏和截长图以及分享保存的全流程指南
android·前端·flutter
Carson带你学Android20 小时前
OpenClaw移动端要来了?Android官宣AI原生支持App Functions
android