返回

Jetpack Compose中为BitmapDescriptor添加颜色滤镜

Android

Jetpack Compose 中给 BitmapDescriptor/Drawable 应用颜色滤镜

你在用 Jetpack Compose 画 Google 地图上的折线 (Polyline) 时,想给末端的 BitmapDescriptor 加个和折线一样的颜色,这可咋整?你发现以前用 Android Views 的那一套,像 setColorFilter,在 Compose 里不好使了,对吧?别急,咱来捋捋这事儿。

咋就不能直接用老办法了?

问题的关键在于,BitmapDescriptorFactory.fromResource 返回的 BitmapDescriptor 对象,它本身并不直接提供修改颜色的方法。它不像 Android Views 里的 Drawable 那样有 setColorFilter。 而且在Compose环境下, 使用旧有的方式处理位图与新的组合系统并不兼容。

几招搞定颜色滤镜

既然老路不通,咱就得找新路子。下面有几种办法,都能给你的 BitmapDescriptor 染上色:

1. 使用 ImageBitmap.imageResourceCanvas

这种方法,先把你的 Drawable 资源转成 ImageBitmap,然后利用 Compose 提供的 Canvas 来操作。

原理和步骤:
  1. 加载 Drawable: 通过 ImageBitmap.imageResource 把资源文件(比如你的箭头 R.drawable.arrow)加载成 ImageBitmap
  2. 创建新画布和位图: 新建一个和ImageBitmap等大的MutableImageBitmap。并创建基于新位图的Canvas
  3. 设置颜色滤镜: 利用 DrawScopedrawImage 方法, 并设置colorFilter 参数进行绘制,这个方法可以让你在绘制图片时加上颜色滤镜。
  4. 转成 BitmapDescriptor:MutableImageBitmap 转为 Bitmap,最后通过BitmapDescriptorFactory.fromBitmap() 创建 BitmapDescriptor
代码示例:
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.res.imageResource
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import android.graphics.Bitmap
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.asImageBitmap

fun getColoredBitmapDescriptor(resourceId: Int, color: Color, context: Context): BitmapDescriptor {
    val imageBitmap = ImageBitmap.imageResource(context.resources, resourceId)
    val mutableBitmap = Bitmap.createBitmap(
            imageBitmap.width,
            imageBitmap.height,
            Bitmap.Config.ARGB_8888
    ).asImageBitmap()

    val newCanvas = Canvas(mutableBitmap)
    val paint = Paint().apply {
        colorFilter = ColorFilter.tint(color)
    }

    newCanvas.drawImage(imageBitmap,  paint)

    return BitmapDescriptorFactory.fromBitmap(mutableBitmap.asAndroidBitmap())
}

// 在你的 Compose 代码里这样用:
val arrowBitmapDescriptor: BitmapDescriptor by remember(polylineColor) {
   mutableStateOf(getColoredBitmapDescriptor(R.drawable.arrow, polylineColor, context))
}

Polyline(
    points = polylinePoints,
    endCap = CustomCap(arrowBitmapDescriptor, 30f),
    color = polylineColor // 假设 polylineColor 是你想要的颜色
)

改进-扩展函数

你可以通过编写ImageBitmap的扩展函数的方式简化流程:

 fun ImageBitmap.tint(color: Color): BitmapDescriptor {
  val tintedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).asImageBitmap()

  Canvas(tintedBitmap).drawImage(
      this,
      androidx.compose.ui.graphics.Paint().apply {
       colorFilter = ColorFilter.tint(color)
      }
  )

  return BitmapDescriptorFactory.fromBitmap(tintedBitmap.asAndroidBitmap())
 }

//使用
 val originalBitmapDescriptor = ImageBitmap.imageResource(context.resources, R.drawable.arrow)
    val arrowBitmapDescriptor: BitmapDescriptor by remember(polylineColor) {
     mutableStateOf(originalBitmapDescriptor.tint(polylineColor))
 }

2. 直接操作 Bitmap 像素 (进阶)

如果你对性能有极致追求,可以考虑更底层的操作.直接摆弄Bitmap的像素值。

原理:

每个像素的颜色,实际上是用一个整数表示的,通常是 ARGB 格式(Alpha, Red, Green, Blue)。我们可以遍历 Bitmap 的每个像素,修改它的颜色值,实现颜色滤镜效果。

代码示例:
import android.graphics.Bitmap
import android.graphics.Color
import androidx.compose.ui.graphics.Color as ComposeColor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.BitmapDescriptor

fun colorizeBitmap(bitmap: Bitmap, color: ComposeColor): BitmapDescriptor {
    val width = bitmap.width
    val height = bitmap.height
    val pixels = IntArray(width * height)
    bitmap.getPixels(pixels, 0, width, 0, 0, width, height)

    val targetColor = color.toArgb()
    val targetRed = Color.red(targetColor)
    val targetGreen = Color.green(targetColor)
    val targetBlue = Color.blue(targetColor)

    for (i in pixels.indices) {
        val pixel = pixels[i]

        //简单地将原始像素的RGB与目标颜色的RGB相乘
        val red = (Color.red(pixel) * targetRed) / 255
        val green = (Color.green(pixel) * targetGreen) / 255
        val blue = (Color.blue(pixel) * targetBlue) / 255

        pixels[i] = Color.argb(Color.alpha(pixel), red, green, blue)
    }

    val newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    newBitmap.setPixels(pixels, 0, width, 0, 0, width, height)

    return BitmapDescriptorFactory.fromBitmap(newBitmap)
}

// 用法:
// 先从资源创建一个普通的 Bitmap
val originalBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.arrow)
val coloredArrow = colorizeBitmap(originalBitmap, polylineColor) // polylineColor 是你的目标颜色

注意:

直接操作像素比较耗费计算资源,适合小图片。大图片可能导致卡顿。而且不同的色彩混合模式算法不同. 上面的示例只是一个简单的颜色混合模式。

3. 利用现成的库(如果条件允许)

有些第三方库已经封装好了这些操作。如果你不介意引入新库,可以试试. 比如 Coil 库:

//添加依赖(build.gradle.kts (Module :app))
implementation("io.coil-kt:coil-compose:2.5.0") //检查最新版本!

//由于库本身不能直接输出BitmapDescriptor,仍然需要手动进行转换.
//需要和第一种或第二种方式结合

然后使用其提供的rememberAsyncImagePainter配合colorFilter:
Coil 自身无法处理将ImagePainter 转为 BitmapDescriptor
所以这种方法更多的是简化了图片加载部分,然后仍然使用方法1 或 2 中的方法将其转换.

方案选择

  • 如果简单起见,推荐用第一种,代码可读性比较好, 也比较符合 Compose 的设计思想。
  • 如果你对性能要求高,而且处理的是小图标,可以试试第二种。
  • 如果能用库,且库本身有支持,当然是更省事的.
    以上三种方法,都能实现给 BitmapDescriptor 加颜色滤镜的需求。挑一个你顺手的用吧!