Android | Matrix.setPolyToPoly() 图像变换详解

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 等更高效方法。
相关推荐
tangweiguo030519874 小时前
Flutter 与 Android NDK 集成实战:实现高性能原生功能
android·flutter
_祝你今天愉快6 小时前
Android SurfaceView & TextureView
android·性能优化
q5507071776 小时前
uniapp/uniappx实现图片或视频文件选择时同步告知权限申请目的解决华为等应用市场上架审核问题
android·图像处理·uni-app·uniapp·unix
李新_7 小时前
一个复杂Android工程开发前我们要考虑哪些事情?
android·程序员·架构
casual_clover8 小时前
Android 中解决 Button 按钮背景色设置无效的问题
android·button
峥嵘life8 小时前
Android14 通过AMS 实例获取前台Activity 信息
android·安全
pengyu10 小时前
【Kotlin系统化精讲:伍】 | 数据类型之空安全:从防御性编程到类型革命🚀
android·kotlin
叽哥10 小时前
flutter学习第 11 节:状态管理进阶:Provider
android·flutter·ios