Last active
August 25, 2026 15:21
-
-
Save lby1992/a067476346ea5f6b25e5465cfbb7614d to your computer and use it in GitHub Desktop.
Fixed textureView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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