setPolyToPoly(...)
是 Matrix
提供的一个强大接口,根据src源点和dst目标点的对应关系得到一个变换矩阵,并用这个矩阵对坐标或位图做变换(平移/旋转/缩放/错切/透视等)。
Matrix.setPolyToPoly 参数解释
kotlin
public void setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
- src:源点数组,格式 x0, y0, x1, y1, ...
- srcIndex:src 起始偏移,通常为 0。
- dst:目标点数组,格式同 src。
- dstIndex:dst 起始偏移,通常为 0。
- pointCount:使用多少对点来求变换(只能是 0,1,2,3,4)。
对于 src & dst ,src = [x0, y0, x1, y1, x2, y2, ...]、 dst = [X0, Y0, X1, Y1, X2, Y2, ...],setPolyToPoly(src, 0, dst, 0, n) 会把 src[i]
映射到 dst[i]
(i=0..n-1
),第 i
个点是 (src[2*i], src[2*i+1])
映射到 (dst[2*i], dst[2*i+1])
,对应顺序必须一致。srcIndex、dstIndex对应的是偏移量,即从哪个点开始,通常都设置为0.;而对于pointCount,不同的值会产生不同的效果,如下:
pointCount | 变换类型 |
---|---|
0 | 单位矩阵 |
1 | 平移 |
2 | 平移 + 缩放 + 旋转 |
3 | 平移、缩放、旋转与错切 |
4 | 投影变换,任意图形扭曲 |
pointCount == 0
将矩阵置为单位矩阵(无变换),相当于 reset()。pointCount == 1
(1 个点)
只能确定 平移(translation)。把 src 的点移动到 dst 的点,矩阵只包含平移分量(dx, dy)。pointCount == 2
(2 个点)
可以确定 平移 + 统一缩放 + 旋转。不支持非等比例缩放或剪切。换言之,2 点确定了一个相似变换,有 4 个自由度(tx, ty, scale, rotation)。pointCount == 3
(3 个点)
可确定平移、任意缩放、旋转与错切,总共 6 个自由度。pointCount == 4
(4 个点)
支持任意二维投影变换(8 个自由度),可以做到图形扭曲(例如把矩形变成梯形、近大远小效果等)。
示例代码
效果图

如上所示,实现了一个图片的变形与复原,关键代码如下:
kotlin
class WarpImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private var bitmap: Bitmap? = null
private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
private val matrix = Matrix()
private var srcPts = FloatArray(8) // 源点(bitmap坐标系)
private var startDst = FloatArray(8) // 起始目标点(view坐标系)
private var endDst: FloatArray? = null // 最终目标点(view坐标系)
private var currentDst = FloatArray(8) // 动画当前目标点
private var animator: ValueAnimator? = null
fun setImageBitmap(bm: Bitmap) {
bitmap = bm
setupSrcAndStartDst()
invalidate()
}
fun setTargetCorners(dst: FloatArray) {
endDst = dst.copyOf(8)
}
fun startCorrection(duration: Long = 500L) {
val start = startDst.copyOf()
val end = endDst ?: return
animateCorners(start, end, duration)
}
fun resetToStart(duration: Long = 500L) {
val start = currentDst.copyOf()
val end = startDst.copyOf()
animateCorners(start, end, duration)
}
/** 通用动画方法 */
private fun animateCorners(
start: FloatArray,
end: FloatArray,
duration: Long,
onEnd: (() -> Unit)? = null
) {
animator?.cancel()
animator = ValueAnimator.ofFloat(0f, 1f).apply {
this.duration = duration
addUpdateListener { va ->
val frac = va.animatedValue as Float
for (i in 0 until 8) {
currentDst[i] = start[i] + (end[i] - start[i]) * frac
}
matrix.reset()
matrix.setPolyToPoly(srcPts, 0, currentDst, 0, 4)
invalidate()
}
doOnEnd { onEnd?.invoke() }
start()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val bm = bitmap ?: return
if (matrix.isIdentity) {
matrix.setPolyToPoly(srcPts, 0, currentDst, 0, 4)
}
canvas.drawBitmap(bm, matrix, paint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
setupSrcAndStartDst()
}
private fun setupSrcAndStartDst() {
val bm = bitmap ?: return
val bw = bm.width.toFloat()
val bh = bm.height.toFloat()
srcPts = floatArrayOf(0f, 0f, bw, 0f, bw, bh, 0f, bh)
val vw = width.toFloat()
val vh = height.toFloat()
if (vw == 0f || vh == 0f) {
post { setupSrcAndStartDst() }
return
}
val scale = min(vw / bw, vh / bh)
val sw = bw * scale
val sh = bh * scale
val dx = (vw - sw) / 2f
val dy = (vh - sh) / 2f
startDst = floatArrayOf(
dx, dy,
dx + sw, dy,
dx + sw, dy + sh,
dx, dy + sh,
)
System.arraycopy(startDst, 0, currentDst, 0, 8)
}
}
上述代码通过 Matrix.setPolyToPoly() 做变形的图片控件,支持平滑动画变形和复原,关键逻辑:
- 通过 srcPts(图片原始四个顶点坐标)和 currentDst(当前目标四个顶点坐标)计算 Matrix;使用 Matrix.setPolyToPoly() 让图片从原始矩形变形到任意四边形。
- 变形和复原都用 ValueAnimator 插值计算八个坐标点的中间值,实现平滑过渡。动画过程每一帧都会更新 matrix 并 invalidate() 重绘。
- setupSrcAndStartDst() 根据 View 尺寸和图片比例,计算缩放与平移,让图片初始状态居中显示。初始位置(startDst)既是加载时的矩形位置,也是复原目标。
- 初始状态:图片按比例缩放到 View 中心,无任何变形。点击 "start":图片的四个角会缓慢移动到指定的新位置,产生倾斜或拉伸的效果;点击 "reset":图片从当前变形状态平滑地恢复到最初的矩形位置。
XML代码:
ini
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.ninetripods.mq.study.widget.matrix.WarpImageView
android:id="@+id/warpView"
android:layout_width="match_parent"
android:layout_height="400dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start" />
<Button
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="reset" />
</LinearLayout>
</LinearLayout>
UI层:
kotlin
class SetPolyToPolyFragment : BaseFragment() {
private val warpView: WarpImageView by id(R.id.warpView)
private val btnStart: Button by id(R.id.btn_start)
private val btnReset: Button by id(R.id.btn_reset)
override fun getLayoutId(): Int {
return R.layout.layout_matrix_poly_fragment2
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val bm = BitmapFactory.decodeResource(resources, R.drawable.icon_cat_h)
warpView.setImageBitmap(bm)
btnStart.setOnClickListener {
warpView.post {
val vw = warpView.width.toFloat()
val vh = warpView.height.toFloat()
val dst = floatArrayOf(
10f, 40f, // 左上
vw - 300f, 40f, // 右上
vw - 40f, vh - 10f, // 右下
250f, vh - 20f // 左下
)
warpView.setTargetCorners(dst)
warpView.startCorrection(600L)
}
}
btnReset.setOnClickListener {
warpView.resetToStart(600L)
}
}
}
注意事项
- 点不要重复或共线(尤其是 3 点或 4 点的情况),否则会导致奇异矩阵(不可逆),从而产生错误效果。
pointCount
大时(尤其 4)计算成本比 1、2 要高,变换矩阵求解更复杂。频繁调用时请重用Matrix
对象。- 如果只是做简单平移/缩放/旋转,优先使用
postTranslate/postScale/postRotate
等更高效方法。