Created
August 27, 2026 16:54
-
-
Save lby1992/f10b1abf2dba8889a798dfa39dc51be6 to your computer and use it in GitHub Desktop.
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.graphics.SurfaceTexture | |
| import android.util.AttributeSet | |
| import android.util.Log | |
| import android.view.Surface | |
| import android.view.TextureView | |
| import android.widget.FrameLayout | |
| class PlayerView @JvmOverloads constructor( | |
| context: Context, | |
| attrs: AttributeSet? = null, | |
| defStyleAttr: Int = 0, | |
| ) : FrameLayout(context, attrs, defStyleAttr) { | |
| private val textureView: TextureView = TextureView(context) | |
| .apply { | |
| surfaceTextureListener = object : TextureView.SurfaceTextureListener { | |
| override fun onSurfaceTextureAvailable( | |
| surfaceTexture: SurfaceTexture, | |
| width: Int, | |
| height: Int, | |
| ) { | |
| logging("Surface available") | |
| // TODO 要记录并释放 | |
| val surface = Surface(surfaceTexture) | |
| onSurfaceChanged?.invoke(surface) | |
| } | |
| override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { | |
| surface.release() | |
| onSurfaceChanged?.invoke(null) | |
| return true | |
| } | |
| override fun onSurfaceTextureSizeChanged( | |
| surface: SurfaceTexture, | |
| width: Int, | |
| height: Int, | |
| ) { | |
| logging("surface size changed: ${width}x${height}") | |
| } | |
| override fun onSurfaceTextureUpdated(p0: SurfaceTexture) { | |
| } | |
| } | |
| } | |
| private val gestureLayerView: PlayerGestureLayerView = PlayerGestureLayerView(context) | |
| .apply { | |
| onScaleGesture = { scaleFactor, focusX, focusY -> | |
| handleScaleEvent(scaleFactor, focusX, focusY) | |
| } | |
| onMove = { dx, dy -> | |
| handleMovingEvent(dx, dy) | |
| } | |
| onFling = { vx, vy -> | |
| handleFlingEvent(vx, vy) | |
| } | |
| onTapped = { | |
| handleTapEvent() | |
| } | |
| onDoubleTapped = { focusX, focusY -> | |
| handleDoubleTapEvent(focusX, focusY) | |
| } | |
| } | |
| private var minScale = DEFAULT_MIN_SCALE | |
| private var midScale = DEFAULT_MID_SCALE | |
| private var maxScale = DEFAULT_MAX_SCALE | |
| private var currentScale = minScale | |
| private var videoWidth: Int = 0 | |
| private var videoHeight: Int = 0 | |
| private var videoPixelWidthHeightRatio = 1f | |
| var onSurfaceChanged: ((Surface?) -> Unit)? = null | |
| private val baseRect = RectF() | |
| private val baseMatrix = Matrix() | |
| init { | |
| addView(textureView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) | |
| addView( | |
| gestureLayerView, | |
| LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) | |
| ) | |
| } | |
| fun updateVideoSize( | |
| width: Int, | |
| height: Int, | |
| pixelWidthHeightRatio: Float, | |
| ) { | |
| videoWidth = width | |
| videoHeight = height | |
| videoPixelWidthHeightRatio = pixelWidthHeightRatio | |
| logging("video size: ${width}x${height}, ratio=$pixelWidthHeightRatio") | |
| // TODO | |
| rebuildBaseMatrix() | |
| } | |
| fun updateVideoSize( | |
| width: Int, | |
| height: Int, | |
| sarNum: Int, | |
| sarDen: Int, | |
| ) { | |
| val pixelWidthHeightRatio = if (sarNum > 0 && sarDen > 0) { | |
| sarNum.toFloat() / sarDen.toFloat() | |
| } else { | |
| 1f | |
| } | |
| updateVideoSize( | |
| width = width, | |
| height = height, | |
| pixelWidthHeightRatio = pixelWidthHeightRatio | |
| ) | |
| } | |
| fun resetTransform() { | |
| } | |
| private fun handleScaleEvent( | |
| scaleFactor: Float, | |
| focusX: Float, | |
| focusY: Float | |
| ): Boolean { | |
| logging("scale factor: $scaleFactor, x: $focusX, y: $focusY, going to be ${currentScale * scaleFactor}") | |
| val targetScale = (currentScale * scaleFactor).coerceIn(minScale, maxScale) | |
| if (targetScale == currentScale) return false | |
| currentScale = targetScale | |
| logging("doing scaling: $currentScale") | |
| return true | |
| } | |
| private fun handleMovingEvent( | |
| dx: Float, | |
| dy: Float | |
| ): Boolean { | |
| logging("moving, dx: $dx, dy: $dy") | |
| return true | |
| } | |
| private fun handleFlingEvent( | |
| velocityX: Float, | |
| velocityY: Float | |
| ): Boolean { | |
| logging("fling: $velocityX $velocityY") | |
| return true | |
| } | |
| private fun handleTapEvent(): Boolean { | |
| logging("Tapped") | |
| return true | |
| } | |
| private fun handleDoubleTapEvent( | |
| focusX: Float, | |
| focusY: Float | |
| ): Boolean { | |
| logging("Double tapped, x=$focusX, y=$focusY") | |
| return true | |
| } | |
| private fun rebuildBaseMatrix() { | |
| val viewWidth = textureView.width | |
| val viewHeight = textureView.height | |
| if (viewWidth <= 0 || viewHeight <= 0 || videoWidth <= 0 || videoHeight <= 0) { | |
| return | |
| } | |
| calculateBaseRect( | |
| viewWidth = viewWidth, | |
| viewHeight = viewHeight, | |
| videoWidth = videoWidth, | |
| videoHeight = viewHeight, | |
| pixelWidthHeightRatio = videoPixelWidthHeightRatio | |
| ) | |
| logging("baseRect=[width=${baseRect.width()}, height=${baseRect.height()}, left=${baseRect.left}, top=${baseRect.top}]") | |
| val scaleX = baseRect.width() / viewWidth | |
| val scaleY = baseRect.height() / viewHeight | |
| baseMatrix.reset() | |
| baseMatrix.setScale(scaleX, scaleY) | |
| baseMatrix.postTranslate(baseRect.left, baseRect.top) | |
| textureView.setTransform(baseMatrix) | |
| applyTransform() | |
| } | |
| private fun calculateBaseRect( | |
| viewWidth: Int, | |
| viewHeight: Int, | |
| videoWidth: Int, | |
| videoHeight: Int, | |
| pixelWidthHeightRatio: Float, | |
| ) { | |
| val videoAspectRatio = videoWidth.toFloat() * pixelWidthHeightRatio / videoHeight | |
| val viewAspectRatio = viewWidth.toFloat() / videoHeight | |
| if (videoAspectRatio > viewAspectRatio) { | |
| // 视频更宽,要撑满View的宽度 | |
| val width = viewWidth.toFloat() | |
| val height = width / videoAspectRatio | |
| val top = (viewHeight - height) / 2 | |
| baseRect.set( | |
| 0f, | |
| top, | |
| width, | |
| top + height, | |
| ) | |
| } else { | |
| // 视频更高,要撑满View的高度/两者宽高比一样 | |
| val height = viewHeight.toFloat() | |
| val width = height * videoAspectRatio | |
| val left = (viewWidth - width) / 2 | |
| baseRect.set( | |
| left, | |
| 0f, | |
| left + width, | |
| height | |
| ) | |
| } | |
| } | |
| private fun applyTransform() { | |
| textureView.setTransform(baseMatrix) | |
| } | |
| private fun logging(msg: String) { | |
| Log.i("PlayerView", msg) | |
| } | |
| companion object { | |
| private const val DEFAULT_MIN_SCALE = 1f | |
| private const val DEFAULT_MID_SCALE = 2f | |
| private const val DEFAULT_MAX_SCALE = 4f | |
| } | |
| } |
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.util.AttributeSet | |
| import android.view.GestureDetector | |
| import android.view.MotionEvent | |
| import android.view.ScaleGestureDetector | |
| import android.view.View | |
| class PlayerGestureLayerView @JvmOverloads constructor( | |
| context: Context, | |
| attrs: AttributeSet? = null, | |
| defStyleAttr: Int = 0, | |
| ) : View(context, attrs, defStyleAttr) { | |
| var onScaleGesture: (( | |
| scaleFactor: Float, | |
| focusX: Float, | |
| focusY: Float | |
| ) -> Boolean)? = null | |
| var onMove: (( | |
| dx: Float, | |
| dy: Float, | |
| ) -> Boolean)? = null | |
| var onFling: (( | |
| velocityX: Float, | |
| velocityY: Float, | |
| ) -> Boolean)? = null | |
| var onTapped: (() -> Boolean)? = null | |
| var onDoubleTapped: (( | |
| focusX: Float, | |
| focusY: Float | |
| ) -> Boolean)? = null | |
| private val scaleGestureListener = | |
| object : ScaleGestureDetector.SimpleOnScaleGestureListener() { | |
| override fun onScale(detector: ScaleGestureDetector): Boolean { | |
| onScaleGesture?.invoke( | |
| detector.scaleFactor, | |
| detector.focusX, | |
| detector.focusY | |
| ) | |
| return true | |
| } | |
| override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { | |
| isScaling = true | |
| return true | |
| } | |
| override fun onScaleEnd(detector: ScaleGestureDetector) { | |
| isScaling = false | |
| } | |
| } | |
| private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { | |
| override fun onScroll( | |
| e1: MotionEvent?, | |
| e2: MotionEvent, | |
| distanceX: Float, | |
| distanceY: Float | |
| ): Boolean { | |
| return onMove?.invoke(-distanceX, -distanceY) ?: super.onScroll( | |
| e1, | |
| e2, | |
| distanceX, | |
| distanceY | |
| ) | |
| } | |
| override fun onFling( | |
| e1: MotionEvent?, | |
| e2: MotionEvent, | |
| velocityX: Float, | |
| velocityY: Float | |
| ): Boolean { | |
| return onFling?.invoke(velocityX, velocityY) ?: super.onFling( | |
| e1, | |
| e2, | |
| velocityX, | |
| velocityY | |
| ) | |
| } | |
| override fun onSingleTapConfirmed(e: MotionEvent): Boolean { | |
| return onTapped?.invoke() ?: super.onSingleTapConfirmed(e) | |
| } | |
| override fun onDoubleTap(e: MotionEvent): Boolean { | |
| return onDoubleTapped?.invoke(e.x, e.y) ?: super.onDoubleTap(e) | |
| } | |
| } | |
| private val scaleDetector = ScaleGestureDetector(context, scaleGestureListener) | |
| private val gestureDetector = GestureDetector(context, gestureListener) | |
| private var isScaling = false | |
| private var isMultiTouch = false | |
| override fun onTouchEvent(event: MotionEvent): Boolean { | |
| when (event.actionMasked) { | |
| MotionEvent.ACTION_POINTER_DOWN -> { | |
| isMultiTouch = true | |
| cancelGestureDetector(event) | |
| } | |
| MotionEvent.ACTION_UP, | |
| MotionEvent.ACTION_CANCEL -> { | |
| isMultiTouch = false | |
| } | |
| } | |
| val scaleHandled = scaleDetector.onTouchEvent(event) | |
| if (isScaling || isMultiTouch) { | |
| return true | |
| } | |
| val gestureHandled = gestureDetector.onTouchEvent(event) | |
| return scaleHandled || | |
| gestureHandled || | |
| super.onTouchEvent(event) | |
| } | |
| private fun cancelGestureDetector(event: MotionEvent) { | |
| val cancelEvent = MotionEvent.obtain(event) | |
| cancelEvent.action = MotionEvent.ACTION_CANCEL | |
| gestureDetector.onTouchEvent(cancelEvent) | |
| cancelEvent.recycle() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment