安卓ImageView setImageMatrix对ShapeDrawable失效?原因与解决办法
2025-04-30 00:21:56
搞定 Android ImageView 的 setImageMatrix 不生效问题
你是不是也遇到了 ImageView
设置了 scaleType="matrix"
,然后兴冲冲地调用 setImageMatrix(matrix)
,结果发现图片纹丝不动?无论是缩放、平移还是镜像,统统失效,就像下面这位朋友遇到的情况:
// 期望缩放图片
val m = Matrix()
m.setScale(0.5f, 0.5f)
// m.setTranslate(30f, 30f) // 平移也不好使
imageView.setImageMatrix(m) // 调用了但没效果
imageView.invalidate() // 刷新也没用
// XML 设置
<ImageView
android:id="@+id/sun"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="matrix" // 明确指定了 matrix
android:src="@drawable/sun" /> // 源是个 ShapeDrawable (oval)
用的图片资源还是个 ShapeDrawable
:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/bright_sun" />
</shape>
尝试了在 onPreDraw
里设置,在点击事件里设置,各种组合都试了,就是没反应。别急,这事儿挺常见的,咱们来分析分析是咋回事,再看看怎么解决。
为什么 setImageMatrix
失效了?
问题的关键往往不在于 setImageMatrix
这个方法本身,也不在于 scaleType="matrix"
这个设置,而在于你给 ImageView
设置的那个 Drawable
是什么类型。
核心症结:Drawable 不配合
ImageView
的 setImageMatrix
方法,其设计初衷主要是为了配合那些具有“内禀尺寸”(intrinsic dimensions)的 Drawable
,最典型的就是 BitmapDrawable
。当你给 ImageView
一个 BitmapDrawable
,并且设置 scaleType="matrix"
时,ImageView
在绘制阶段会:
- 获取你通过
setImageMatrix
设置的Matrix
。 - 将这个
Matrix
应用(concat)到Canvas
上。 - 然后让
BitmapDrawable
在这个变换后的Canvas
上绘制自己。
这样一来,Bitmap
就按照你期望的 Matrix
进行了变换(缩放、平移、旋转、镜像等)。
但是!当你使用 ShapeDrawable
或者某些其他类型的 Drawable
(比如 ColorDrawable
)时,情况就不同了。ShapeDrawable
这类 Drawable
通常没有明确的“原始图片尺寸”。它们绘制自己的逻辑,更多是依赖于 View
通过 setBounds()
方法告诉它的可用绘制区域有多大。它在自己的 draw(Canvas canvas)
方法里,直接在给定的 bounds
内绘制形状,不太会去主动关心 ImageView
可能通过 setImageMatrix
传过来的那个 Matrix
小纸条。
简单说:setImageMatrix
设定了一个变换规则,但 ShapeDrawable
这哥们儿画图时,没按这个规则来,直接按自己的理解(主要看 bounds
)画完了事。ImageView
本身并没有强制所有 Drawable
都必须遵守这个 Matrix
。
scaleType
的影响
你设置 scaleType="matrix"
是绝对正确的,这步操作告诉了 ImageView
:“老大,待会儿绘图的时候,记得用我(通过 setImageMatrix
)给你的那个 Matrix 哦!”。如果 scaleType
不是 matrix
(比如是 fitCenter
),那 ImageView
会用内置的逻辑去计算一个 Matrix
来让 Drawable
适应视图,你再调用 setImageMatrix
设置的 Matrix
就会被覆盖或忽略。
所以,scaleType="matrix"
是必要条件,但不是充分条件。还需要 Drawable
类型本身支持或者说能响应这个 Matrix
。
解决方案
知道了问题根源,解决起来就思路清晰了。下面提供几种可行的方法,各有优劣,可以根据你的具体需求选择。
方案一:将 ShapeDrawable
转为 BitmapDrawable
这是最直接也最符合 setImageMatrix
设计意图的方法。既然 ShapeDrawable
不听话,咱就把它变成听话的 BitmapDrawable
。
原理:
先将 ShapeDrawable
绘制到一个临时的 Bitmap
上,然后用这个 Bitmap
创建一个 BitmapDrawable
。BitmapDrawable
是 setImageMatrix
的“老朋友”了,能很好地配合工作。
操作步骤与代码示例 (Kotlin):
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.ShapeDrawable
import androidx.core.content.ContextCompat
// 假设你的 ImageView 实例叫 imageView, ShapeDrawable 资源 ID 是 R.drawable.sun
// 1. 获取 ShapeDrawable
val shapeDrawable: Drawable? = ContextCompat.getDrawable(context, R.drawable.sun)
if (shapeDrawable != null) {
// 2. 决定 Bitmap 的尺寸 (可以根据 ImageView 的尺寸或预设值)
// 这里用 ImageView 的尺寸为例,注意在获取尺寸前确保 View 已经测量布局完成
// 更稳妥的做法可能是在 onSizeChanged 或使用 ViewTreeObserver 获取准确尺寸
// 或者,直接指定一个固定尺寸
val width = imageView.width.takeIf { it > 0 } ?: 100 // fallback size
val height = imageView.height.takeIf { it > 0 } ?: 100 // fallback size
if (width > 0 && height > 0) {
// 3. 创建一个 Bitmap
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// 4. 创建一个基于该 Bitmap 的 Canvas
val canvas = Canvas(bitmap)
// 5. 设置 ShapeDrawable 的绘制边界
// 非常重要:ShapeDrawable 需要知道在哪里绘制
shapeDrawable.setBounds(0, 0, width, height)
// 6. 将 ShapeDrawable 绘制到 Canvas 上 (也就是绘制到 Bitmap 上)
shapeDrawable.draw(canvas)
// 7. 创建 BitmapDrawable
val bitmapDrawable = BitmapDrawable(context.resources, bitmap)
// 8. 将 BitmapDrawable 设置给 ImageView
// 记得 scaleType 仍然是 "matrix"
imageView.scaleType = ImageView.ScaleType.MATRIX // 再次确认
imageView.setImageDrawable(bitmapDrawable)
// 9. 现在可以愉快地设置 Matrix 了
val m = Matrix()
// 例如,实现水平镜像
m.setScale(-1f, 1f, width / 2f, height / 2f) // 以中心点镜像
// 或者缩放
// m.setScale(0.5f, 0.5f, width / 2f, height / 2f) // 以中心点缩放 50%
imageView.imageMatrix = m // 注意是 imageMatrix 属性 或 setImageMatrix(m) 方法
// imageView.invalidate() // imageMatrix 属性的 setter 通常内部会调用 invalidate
} else {
// 处理无法获取有效尺寸的情况
Log.e("ImageMatrixFix", "ImageView dimensions are zero, cannot create Bitmap.")
}
} else {
// 处理 Drawable 未找到的情况
Log.e("ImageMatrixFix", "ShapeDrawable not found.")
}
注意事项与建议:
- Bitmap 内存: 创建
Bitmap
会消耗内存。如果ImageView
尺寸很大,或者需要频繁变换形状/颜色并重新生成Bitmap
,要注意内存开销和潜在的 OOM 风险。 - 绘制时机: 获取
imageView.width
和imageView.height
需要在View
测量布局之后。如果在Activity/Fragment
的onCreate
/onViewCreated
中直接执行,可能得到 0。可以考虑使用ViewTreeObserver.OnGlobalLayoutListener
或post { ... }
来延迟执行获取尺寸和转换的操作。 - 缓存: 如果形状和颜色是固定的,生成的
BitmapDrawable
可以缓存起来复用,避免重复创建Bitmap
。 - 图片质量:
Bitmap.Config.ARGB_8888
提供最好的质量但占用内存最多。根据需要可以考虑ARGB_4444
或RGB_565
(如果不需要透明度)。
进阶技巧:
- 为啥
BitmapDrawable
就行?因为它有明确的“内禀宽高”(intrinsic width/height),即Bitmap
的原始尺寸。ImageView
在应用Matrix
时,知道这个原始尺寸,可以精确计算变换后的绘制方式。ShapeDrawable
没有这个内禀尺寸的概念。 - 可以封装一个工具方法
fun shapeToBitmapDrawable(context: Context, drawableId: Int, width: Int, height: Int): BitmapDrawable?
来简化转换过程。
方案二:自定义 ImageView
并重写 onDraw
如果你不想做 Drawable
转换,或者需要更精细地控制绘制过程,可以创建一个 ImageView
的子类,重写它的 onDraw
方法。
原理:
直接在绘制阶段(onDraw
)介入,获取到 Canvas
对象后,在调用父类(super.onDraw
)或者自己手动绘制 Drawable
之前,先对 Canvas
应用我们想要的 Matrix
变换。这样一来,后续所有的绘制操作都会在这个变换后的坐标系下进行。
操作步骤与代码示例 (Kotlin):
-
创建自定义
ImageView
类:import android.content.Context import android.graphics.Canvas import android.graphics.Matrix import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView class TransformableImageView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr) { private val transformMatrix = Matrix() private var matrixApplied = false // 标记是否应用了自定义 Matrix // 提供一个方法来设置你想要的变换矩阵 fun setTransformMatrix(matrix: Matrix?) { transformMatrix.set(matrix ?: Matrix()) // 如果传入 null,则重置为单位矩阵 matrixApplied = matrix != null && !matrix.isIdentity // 只有非单位矩阵才算应用了 invalidate() // 请求重绘 } // 获取当前应用的变换矩阵(可选) fun getTransformMatrix(): Matrix { return Matrix(transformMatrix) // 返回副本防止外部修改 } override fun onDraw(canvas: Canvas) { // 关键点:在绘制内容前应用 Matrix if (matrixApplied && drawable != null) { // 保存当前的 Canvas 状态 (包括 Matrix) val saveCount = canvas.save() // 将我们的变换矩阵应用到 Canvas 上 // concat 是在 Canvas 原有变换基础上再乘上我们的矩阵 canvas.concat(transformMatrix) // 让父类 ImageView 继续绘制它的 Drawable // 因为 Canvas 已经被变换了,所以 Drawable 的绘制自然就被影响了 super.onDraw(canvas) // 恢复 Canvas 状态到 save() 之前,避免影响后续其他 View 的绘制 canvas.restoreToCount(saveCount) } else { // 如果没有设置自定义 Matrix 或没有 Drawable,就按默认方式绘制 super.onDraw(canvas) } } // 注意:当使用这种方法时,XML 中的 android:scaleType 就不再是必须为 "matrix" 了。 // 因为我们是在 onDraw 中手动应用 Matrix。你可以设置成 fitCenter 等, // super.onDraw(canvas) 会先按 scaleType 处理,然后我们的 Matrix 会再作用于其上。 // 如果想完全由你的 Matrix 控制,可以在 onDraw 里不调用 super.onDraw(), // 而是自己获取 drawable,设置 bounds,然后调用 drawable.draw(canvas)。 // 但那样会失去 ImageView 内置 scaleType 的便利性。 // 示例:手动绘制 Drawable (替代 super.onDraw(canvas)) /* private fun drawManually(canvas: Canvas) { val drawable = drawable ?: return // 假设我们想让 Drawable 在应用 Matrix 后居中显示在 View 内 // 这里计算 bounds 会比较复杂,需要考虑 Matrix 的影响 // 为了简单起见,这里仍然使用 View 的尺寸作为初始 bounds // 然后依赖 Canvas 的 Matrix 来完成变换 val viewWidth = width - paddingLeft - paddingRight val viewHeight = height - paddingTop - paddingBottom drawable.setBounds(paddingLeft, paddingTop, paddingLeft + viewWidth, paddingTop + viewHeight) val saveCount = canvas.save() canvas.concat(transformMatrix) drawable.draw(canvas) canvas.restoreToCount(saveCount) } */ }
-
在 XML 布局中使用自定义
ImageView
:<com.yourpackage.TransformableImageView android:id="@+id/customSunImageView" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" android:src="@drawable/sun" app:scaleType="fitCenter" <!-- 注意:这里不一定非要是 matrix 了 --> /> <!-- 或者保持 android:scaleType="matrix" 然后完全自己控制 Matrix -->
-
在代码中设置
Matrix
:val customImageView = findViewById<TransformableImageView>(R.id.customSunImageView) val m = Matrix() // 实现水平镜像 m.setScale(-1f, 1f, customImageView.width / 2f, customImageView.height / 2f) // 或者缩放 // m.setScale(0.5f, 0.5f, customImageView.width / 2f, customImageView.height / 2f) customImageView.setTransformMatrix(m)
注意事项与建议:
- 性能:
onDraw
方法会被频繁调用(例如,在动画期间)。确保onDraw
内部的逻辑尽可能高效,避免创建新对象(如Matrix
实例,transformMatrix
应该是成员变量)。 canvas.save()
和canvas.restore()
: 这对操作非常重要。save()
保存Canvas
当前的状态(包括变换矩阵、裁剪区域等),restore()
恢复到最近一次save()
时的状态。这能确保你的变换只影响当前ImageView
的绘制,不会干扰到后面绘制的其他View
。canvas.concat(matrix)
vscanvas.setMatrix(matrix)
:concat
是在Canvas
当前已有的变换基础上 追加 新的变换(矩阵乘法)。setMatrix
则是 完全替换Canvas
的变换矩阵。通常情况下,在onDraw
里修改特定View
的绘制,使用concat
更为常见和安全,因为它能跟系统或其他效果(如View
的translationX/Y
,scaleX/Y
属性)叠加。- 与
ImageView
其他属性的交互: 如果你选择调用super.onDraw(canvas)
,那么ImageView
的scaleType
属性仍然会起作用,它会先应用自己的变换,然后你的Matrix
再作用于结果之上。如果你想完全掌控,就需要跳过super.onDraw
,自己调用drawable.draw(canvas)
,但这需要你自行处理Drawable
的定位和尺寸问题(计算bounds
)。
进阶技巧:
- 可以结合
View
的animate()
方法或者ValueAnimator
来实现平滑的Matrix
变换动画。在动画的onAnimationUpdate
回调中更新transformMatrix
并调用invalidate()
。 - 考虑处理
padding
。super.onDraw
通常会处理padding
,如果你自己调用drawable.draw
,需要手动在计算bounds
或在应用Matrix
前通过canvas.translate(paddingLeft, paddingTop)
来偏移绘制内容。
方案三:使用 Canvas
操作(针对 ShapeDrawable
的特定场景)
如果你的最终目的只是在一个区域内画一个经过变换的形状,也许你根本不需要一个功能完整的 ImageView
。一个更轻量级的自定义 View
可能更合适。
原理:
创建一个基础的 View
子类,在它的 onDraw
方法里,直接获取 ShapeDrawable
,然后使用 Canvas
的变换方法(如 canvas.scale()
, canvas.translate()
, canvas.concat(matrix)
) 来设置好坐标系,最后让 ShapeDrawable
在这个变换后的 Canvas
上绘制自己。
操作步骤与代码示例 (Kotlin):
-
创建自定义
View
类:import android.content.Context import android.graphics.Canvas import android.graphics.Matrix import android.graphics.drawable.ShapeDrawable import android.util.AttributeSet import android.view.View import androidx.core.content.ContextCompat class ShapeDrawingView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var shapeDrawable: ShapeDrawable? = null private val transformMatrix = Matrix() init { // 假设 R.drawable.sun 是 ShapeDrawable 资源 ID // 你也可以通过自定义属性来设置 Drawable 资源 ContextCompat.getDrawable(context, R.drawable.sun)?.let { if (it is ShapeDrawable) { shapeDrawable = it } // 或者如果 drawable 本身不是 ShapeDrawable,而是比如 VectorDrawable // 并且你知道如何绘制它,也可以处理 } } fun setDrawable(drawable: ShapeDrawable?) { this.shapeDrawable = drawable requestLayout() // Drawable 可能影响尺寸计算 invalidate() } fun setTransformMatrix(matrix: Matrix?) { transformMatrix.set(matrix ?: Matrix()) invalidate() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 基础实现:如果 Drawable 有内禀尺寸,可以用它们; // 对于 ShapeDrawable,通常没有,所以我们依赖布局参数或给个默认值 val defaultSize = 100 // dp, 需要转换成 px val width = resolveSize(dpToPx(defaultSize), widthMeasureSpec) val height = resolveSize(dpToPx(defaultSize), heightMeasureSpec) setMeasuredDimension(width, height) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 绘制背景等 shapeDrawable?.let { drawable -> // 设置 Drawable 的绘制边界,通常是 View 的内容区域 val availableWidth = width - paddingLeft - paddingRight val availableHeight = height - paddingTop - paddingBottom drawable.setBounds(paddingLeft, paddingTop, paddingLeft + availableWidth, paddingTop + availableHeight) // 应用变换 val saveCount = canvas.save() canvas.concat(transformMatrix) // 绘制 Drawable drawable.draw(canvas) // 恢复 Canvas canvas.restoreToCount(saveCount) } } private fun dpToPx(dp: Int): Int { return (dp * resources.displayMetrics.density + 0.5f).toInt() } }
-
在 XML 中使用:
<com.yourpackage.ShapeDrawingView android:id="@+id/shapeView" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" /> <!-- 注意:这里没有 src 属性,drawable 通常在代码中设置或通过自定义属性 -->
-
在代码中设置
Matrix
和(如果需要)Drawable
:val shapeView = findViewById<ShapeDrawingView>(R.id.shapeView) // (可选) 如果没在 init 中设置或想更换 Drawable // val myShapeDrawable = ... // 创建或获取 ShapeDrawable // shapeView.setDrawable(myShapeDrawable) val m = Matrix() // 设置镜像变换 val viewCenterX = shapeView.width / 2f val viewCenterY = shapeView.height / 2f m.setScale(-1f, 1f, viewCenterX, viewCenterY) shapeView.setTransformMatrix(m)
优点:
- 更轻量,没有
ImageView
那么多特性和开销(如果你不需要的话)。 - 完全掌控绘制逻辑。
缺点:
- 失去了
ImageView
内置的scaleType
等方便的功能。你需要自己处理Drawable
的定位和尺寸适应(虽然对于ShapeDrawable
来说,它本来就是填充bounds
的)。 - 需要自己处理
onMeasure
来确定View
的尺寸,特别是wrap_content
的情况。
进阶技巧:
- 在这种方式下,
ShapeDrawable
仍然是根据setBounds
来决定其基础绘制区域的。Canvas
的变换发生在Drawable.draw()
调用之前,效果上是改变了Drawable
绘制时所处的坐标环境。 - 可以扩展这个自定义
View
,添加更多控制属性,比如直接提供setScaleX
,setRotation
,setTranslateX
等方法来内部维护transformMatrix
。
方案选择和考量
那么,这三种方案该选哪个呢?
-
想最快解决
setImageMatrix
对ShapeDrawable
不生效的问题,同时还想继续使用ImageView
?
优选 方案一:转为BitmapDrawable
。这是最“对症下药”的办法,让Drawable
变得能配合setImageMatrix
。注意内存开销。 -
需要在
ImageView
上应用变换,但不想(或不能方便地)转换Drawable
,并且希望保留ImageView
的大部分功能(比如scaleType
参与计算)?
考虑 方案二:自定义ImageView
重写onDraw
。它提供了在ImageView
绘制流程中插入自定义变换的能力,相对灵活。 -
需求仅仅是显示一个经过变换的形状,
ImageView
的额外功能(如图层、复杂的scaleType
)并非必需?
方案三:自定义View
可能更简洁高效。代码更直接,没有ImageView
的中间层。
性能方面,方案一的主要成本在于 Bitmap
的创建和内存占用。方案二和三的成本主要在 onDraw
的计算,只要 onDraw
里的逻辑不复杂、不频繁创建对象,性能通常很好。动画场景下,方案二和三通过更新 Matrix
并 invalidate()
是常见的做法。
希望以上分析和方案能帮你解决 ImageView
和 Matrix
的这点小“误会”,让你的图片变换随心所欲动起来!