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

相关推荐
BoomHe1 天前
Android AOSP13 原生 Launcher3 壁纸获取方式
android
Digitally1 天前
如何将联系人从 Android 转移到 Android
android
李小枫1 天前
webflux接收application/x-www-form-urlencoded参数
android·java·开发语言
爱丽_1 天前
MySQL `EXPLAIN`:看懂执行计划、判断索引是否生效与排错套路
android·数据库·mysql
NPE~1 天前
[App逆向]环境搭建下篇 — — 逆向源码+hook实战
android·javascript·python·教程·逆向·hook·逆向分析
yewq-cn1 天前
AOSP 下载
android
cch89181 天前
Laravel vs ThinkPHP:PHP框架终极对决
android·php·laravel
米码收割机1 天前
【Android】基于安卓app的汽车租赁管理系统(源码+部署方式+论文)[独一无二]
android·汽车
流星雨在线1 天前
安卓使用 Startup 管理三方 SDK 初始化
android·startup
jwn9991 天前
Laravel3.x:PHP框架的经典里程碑
android