Skip to content

Instantly share code, notes, and snippets.

@lby1992
Last active August 25, 2026 15:21
Show Gist options
  • Select an option

  • Save lby1992/a067476346ea5f6b25e5465cfbb7614d to your computer and use it in GitHub Desktop.

Select an option

Save lby1992/a067476346ea5f6b25e5465cfbb7614d to your computer and use it in GitHub Desktop.
Fixed textureView
package dev.dl.demoapp.player
import android.content.Context
import android.graphics.Matrix
import android.graphics.RectF
import android.util.AttributeSet
import android.view.TextureView
import android.widget.OverScroller
import kotlin.math.abs
class VideoTextureView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextureView(context, attrs, defStyleAttr) {
// ------------------------------------------------------------
// Video State
// ------------------------------------------------------------
private var videoWidth = 0
private var videoHeight = 0
private var initialized = false
// ------------------------------------------------------------
// User Transform State
// ------------------------------------------------------------
private var userScale = 1f
private var translationX = 0f
private var translationY = 0f
// ------------------------------------------------------------
// Matrix & Rect (复用全局对象,零 GC 压力)
// ------------------------------------------------------------
private val baseMatrix = Matrix()
private val finalMatrix = Matrix()
private val baseVideoRect = RectF()
private val transformedVideoRect = RectF()
private val srcRect = RectF()
private val dstRect = RectF()
// 状态防抖与对比复用数组
private val matrixValues = FloatArray(9)
private val lastAppliedMatrixValues = FloatArray(9) { Float.NaN } // 初始化为 NaN,确保第一帧一定刷新
// ------------------------------------------------------------
// Fling Engine
// ------------------------------------------------------------
private val scroller = OverScroller(context)
private var flingRunnable: Runnable? = null
init {
// 禁用 View 自身的 onDraw 绘制(TextureView 自身底层 Surface 负责渲染视频)
setWillNotDraw(true)
// 确保开启硬件加速层
setLayerType(LAYER_TYPE_HARDWARE, null)
}
companion object {
private const val MIN_SCALE = 1f
private const val MAX_SCALE = 5f
private const val EPSILON = 0.0001f
}
// ============================================================
// Public API
// ============================================================
fun setVideoSize(width: Int, height: Int) {
if (width <= 0 || height <= 0) return
val changed = videoWidth != width || videoHeight != height
videoWidth = width
videoHeight = height
initialized = true
rebuildBaseMatrix()
if (changed) {
resetUserTransform()
}
applyTransform(force = true)
}
/**
* 用户双指缩放
*/
fun scale(scaleFactor: Float, centerX: Float, centerY: Float) {
if (!initialized || scaleFactor <= 0f || width <= 0 || height <= 0) return
val oldScale = userScale
val newScale = (oldScale * scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE)
if (abs(newScale - oldScale) <= EPSILON) return
val pivotX = centerX.coerceIn(0f, width.toFloat())
val pivotY = centerY.coerceIn(0f, height.toFloat())
val scaleRatio = newScale / oldScale
// 以手势中心点做锚点缩放
translationX = pivotX - (pivotX - translationX) * scaleRatio
translationY = pivotY - (pivotY - translationY) * scaleRatio
userScale = newScale
if (isApproximatelyOne(userScale)) {
userScale = 1f
translationX = 0f
translationY = 0f
}
clampTranslation()
applyTransform()
}
/**
* 用户单指平移
*/
fun translate(dx: Float, dy: Float) {
if (!initialized || width <= 0 || height <= 0) return
if (isApproximatelyOne(userScale)) {
if (translationX != 0f || translationY != 0f) {
translationX = 0f
translationY = 0f
applyTransform()
}
return
}
translationX += dx
translationY += dy
clampTranslation()
applyTransform()
}
/**
* 触发惯性滑动
*/
fun fling(velocityX: Float, velocityY: Float) {
if (!initialized || width <= 0 || height <= 0) return
if (isApproximatelyOne(userScale)) return
cancelFling()
val scaledWidth = baseVideoRect.width() * userScale
val scaledHeight = baseVideoRect.height() * userScale
val maxX = maxOf(0f, (scaledWidth - width) / 2f).toInt()
val maxY = maxOf(0f, (scaledHeight - height) / 2f).toInt()
scroller.fling(
translationX.toInt(), translationY.toInt(),
velocityX.toInt(), velocityY.toInt(),
-maxX, maxX,
-maxY, maxY
)
flingRunnable = object : Runnable {
override fun run() {
if (scroller.computeScrollOffset()) {
translationX = scroller.currX.toFloat()
translationY = scroller.currY.toFloat()
clampTranslation()
applyTransform(force = true) // Fling 过程强制更新,防止被小阈值防抖截断
postOnAnimation(this)
}
}
}
postOnAnimation(flingRunnable)
}
/**
* 中断惯性滑动
*/
fun cancelFling() {
if (!scroller.isFinished) {
scroller.forceFinished(true)
}
flingRunnable?.let { removeCallbacks(it) }
flingRunnable = null
}
fun resetTransform() {
if (!initialized) return
cancelFling()
resetUserTransform()
applyTransform(force = true)
}
fun getScale(): Float = userScale
fun getVideoTranslationX(): Float = translationX
fun getVideoTranslationY(): Float = translationY
fun getTransformMatrix(): Matrix = Matrix(finalMatrix)
fun getVideoRect(): RectF = RectF(transformedVideoRect)
// ============================================================
// View Lifecycle
// ============================================================
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (!initialized || w <= 0 || h <= 0) return
rebuildBaseMatrix()
clampTranslation()
applyTransform(force = true)
}
override fun onDetachedFromWindow() {
cancelFling()
super.onDetachedFromWindow()
}
// ============================================================
// Internal Transform Calculations
// ============================================================
private fun rebuildBaseMatrix() {
if (width <= 0 || height <= 0 || videoWidth <= 0 || videoHeight <= 0) return
srcRect.set(0f, 0f, videoWidth.toFloat(), videoHeight.toFloat())
dstRect.set(0f, 0f, width.toFloat(), height.toFloat())
baseMatrix.reset()
baseMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.CENTER)
baseVideoRect.set(srcRect)
baseMatrix.mapRect(baseVideoRect)
}
/**
* 代数一步合成 Final Matrix,跳过中间 userMatrix 的创建与 postConcat 运算
*/
private fun rebuildFinalMatrixDirectly() {
finalMatrix.set(baseMatrix)
finalMatrix.postScale(userScale, userScale, width / 2f, height / 2f)
finalMatrix.postTranslate(translationX, translationY)
}
private fun applyTransform(force: Boolean = false) {
if (!initialized || width <= 0 || height <= 0) return
// 1. 直接一步计算 finalMatrix
rebuildFinalMatrixDirectly()
// 2. 防抖校验
finalMatrix.getValues(matrixValues)
if (!force && !isMatrixChanged(matrixValues, lastAppliedMatrixValues)) {
return
}
// 3. 保存并推送到渲染层
System.arraycopy(matrixValues, 0, lastAppliedMatrixValues, 0, 9)
setTransform(finalMatrix)
// 4. 更新实际显示区域
transformedVideoRect.set(0f, 0f, videoWidth.toFloat(), videoHeight.toFloat())
finalMatrix.mapRect(transformedVideoRect)
invalidate()
}
private fun isMatrixChanged(current: FloatArray, last: FloatArray): Boolean {
for (i in 0..8) {
if (last[i].isNaN() || abs(current[i] - last[i]) > EPSILON) return true
}
return false
}
// ============================================================
// Boundary Clamping
// ============================================================
private fun clampTranslation() {
if (width <= 0 || height <= 0 || videoWidth <= 0 || videoHeight <= 0) return
val baseWidth = baseVideoRect.width()
val baseHeight = baseVideoRect.height()
val scaledWidth = baseWidth * userScale
val scaledHeight = baseHeight * userScale
translationX = clampAxis(translationX, scaledWidth, width.toFloat())
translationY = clampAxis(translationY, scaledHeight, height.toFloat())
}
private fun clampAxis(
translation: Float,
contentSize: Float,
viewportSize: Float
): Float {
if (contentSize <= viewportSize + EPSILON) {
return 0f
}
val maxTranslation = (contentSize - viewportSize) / 2f
return translation.coerceIn(-maxTranslation, maxTranslation)
}
private fun resetUserTransform() {
userScale = 1f
translationX = 0f
translationY = 0f
}
private fun isApproximatelyOne(value: Float): Boolean {
return abs(value - 1f) <= EPSILON
}
}
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
// 1. 当手指刚刚按下屏幕时
override fun onDown(e: MotionEvent): Boolean {
// 【关键】立刻打断正在进行的惯性滑动,让画面“停在手指下”
videoTextureView.cancelFling()
return true
}
// 2. 正常拖动
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
// GestureDetector 传回来的 distanceX/Y 是相反的,所以需要加负号
videoTextureView.translate(-distanceX, -distanceY)
return true
}
// 3. 用户快速甩开手指
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
// 触发惯性滑动
videoTextureView.fling(velocityX, velocityY)
return true
}
})
// 最后把事件喂给手势检测器
videoTextureView.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event)
// 如果你还有 ScaleGestureDetector(双指缩放),也在这里一起调用
// scaleGestureDetector.onTouchEvent(event)
true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment