You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ExoPlayer视频缩放平移优化:无需销毁Activity实现流畅操作

解决方案:采用Matrix实现流畅的视频缩放平移(无需销毁Activity)

核心原理

直接设置scaleX/scaleY和translationX/translationY会触发View的布局重计算,导致操作卡顿。而Matrix变换是在渲染阶段直接对画布进行操作,跳过了冗余的布局流程——这也是你的ZoomableTextureView流畅度更高的核心原因。无需销毁Activity,你可以在现有View上通过Matrix实现同样的流畅效果。

修改后的Kotlin实现代码

替换原有基于scaleX/translationX的逻辑,改用Matrix管理缩放和平移:

// 新增Matrix相关核心变量
private val transformMatrix = Matrix()
private val matrixValues = FloatArray(9)
private var saveScale = 1f
private val MIN_ZOOM = 1f
private val MAX_ZOOM = 5f
private var mode = Mode.NONE
private val lastPoint = PointF()
private val startPoint = PointF()
private var rightBound = 0f
private var bottomBound = 0f

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
    event ?: return true
    scaleGestureDetector?.onTouchEvent(event)
    gestureDetector?.onTouchEvent(event)

    transformMatrix.getValues(matrixValues)
    val currentX = matrixValues[Matrix.MTRANS_X]
    val currentY = matrixValues[Matrix.MTRANS_Y]
    val currPoint = PointF(event.x, event.y)

    when (event.action and MotionEvent.ACTION_MASK) {
        MotionEvent.ACTION_DOWN -> {
            lastPoint.set(event.x, event.y)
            startPoint.set(lastPoint)
            mode = Mode.DRAG
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
            mode = Mode.NONE
        }
        MotionEvent.ACTION_POINTER_DOWN -> {
            lastPoint.set(event.x, event.y)
            startPoint.set(lastPoint)
            mode = Mode.ZOOM
        }
        MotionEvent.ACTION_MOVE -> {
            if (mode == Mode.ZOOM || (mode == Mode.DRAG && saveScale > MIN_ZOOM)) {
                val deltaX = currPoint.x - lastPoint.x
                val deltaY = currPoint.y - lastPoint.y

                // 边界限制:防止视频移出可视区域
                val newX = currentX + deltaX
                val newY = currentY + deltaY
                val clampedDeltaX = when {
                    newX > 0 -> -currentX
                    newX < -rightBound -> -(currentX + rightBound)
                    else -> deltaX
                }
                val clampedDeltaY = when {
                    newY > 0 -> -currentY
                    newY < -bottomBound -> -(currentY + bottomBound)
                    else -> deltaY
                }

                transformMatrix.postTranslate(clampedDeltaX, clampedDeltaY)
                lastPoint.set(currPoint.x, currPoint.y)
            }
        }
    }

    applyMatrixTransform()
    return true
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onScale(detector: ScaleGestureDetector): Boolean {
    val scaleFactor = detector.scaleFactor
    val origScale = saveScale
    saveScale *= scaleFactor
    saveScale = saveScale.coerceIn(MIN_ZOOM, MAX_ZOOM)
    val actualScaleFactor = saveScale / origScale

    // 更新可视边界
    rightBound = child().width * saveScale - child().width
    bottomBound = child().height * saveScale - child().height

    // 以双指中心点为基准缩放
    transformMatrix.postScale(actualScaleFactor, actualScaleFactor, detector.focusX, detector.focusY)

    // 缩放后修正边界,避免视频偏移出屏幕
    if (scaleFactor < 1) {
        transformMatrix.getValues(matrixValues)
        val x = matrixValues[Matrix.MTRANS_X]
        val y = matrixValues[Matrix.MTRANS_Y]
        if (x > 0) transformMatrix.postTranslate(-x, 0)
        else if (x < -rightBound) transformMatrix.postTranslate(-(x + rightBound), 0)
        if (y > 0) transformMatrix.postTranslate(0, -y)
        else if (y < -bottomBound) transformMatrix.postTranslate(0, -(y + bottomBound))
    }

    // 更新缩放比例显示文本
    val scaleVal = String.format("%.1f", saveScale)
    binding.txtScaleVideo.text = "$scaleVal x"

    // 保留原有BoundingBox绘制逻辑
    val height = binding.layoutPlayerView.measuredHeight
    val width = binding.layoutPlayerView.measuredWidth
    val drawBB = saveScale == 1.0f
    binding.layoutPlayerView.removeAllViews()
    if (drawBB && (seekToPositions.toDouble() == videoStartseekToPositions)) {
        startBbox?.let { bBoxList ->
            val drawView = PaintView(requireActivity(), bBoxList, height, width)
            binding.layoutPlayerView.addView(drawView)
        }
    }

    applyMatrixTransform()
    return true
}

override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
    mode = Mode.ZOOM
    return true
}

override fun onScaleEnd(detector: ScaleGestureDetector) {}

private fun applyMatrixTransform() {
    child().setTransform(transformMatrix)
    child().invalidate()
}

// 加载新视频时调用,重置缩放状态
fun resetZoomState() {
    transformMatrix.reset()
    saveScale = 1f
    rightBound = 0f
    bottomBound = 0f
    mode = Mode.NONE
    applyMatrixTransform()
    binding.txtScaleVideo.text = "1.0 x"
}

// 保留原有Mode枚举和GestureDetector类
enum class Mode { DRAG, ZOOM, NONE }
private inner class GestureDetector : android.view.GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
        isEnable = !isEnable
        return super.onSingleTapConfirmed(e)
    }

    override fun onDoubleTap(event: MotionEvent): Boolean {
        intLeft = event.x < (sWidth / 2)
        intRight = !intLeft
        return super.onDoubleTap(event)
    }
}

// 保留原有child()和zoomLayout()方法
private fun child(): View {
    return zoomLayout(0)
}
private fun zoomLayout(i: Int): View {
    return binding.videoView
}

关键优化点说明

  1. Matrix替代scale/translation属性:使用setTransform(Matrix)直接控制View的渲染矩阵,避免布局重计算,大幅提升操作流畅度。
  2. 边界限制逻辑优化:在平移和缩放时实时计算边界,确保视频始终在可视区域内。
  3. 新视频加载处理:加载新视频前调用resetZoomState()即可重置缩放状态,无需销毁Activity——只需释放旧播放器资源并初始化新播放器即可。
  4. 缩放基准点优化:以双指触摸的中心点为缩放中心,符合用户直观操作习惯。

注意事项

  • 确保你的videoView是TextureView:系统TextureView原生支持setTransform方法,若使用自定义View需重写该方法并将Matrix应用到画布。
  • 新视频加载流程:重置Matrix状态 → 释放旧播放器 → 初始化新播放器并绑定到videoView,全程无需销毁Activity或View。

内容的提问来源于stack exchange,提问作者Usama Wajid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 20:09:49