Skip to content

Instantly share code, notes, and snippets.

@lby1992
Created August 27, 2026 16:54
Show Gist options
  • Select an option

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

Select an option

Save lby1992/f10b1abf2dba8889a798dfa39dc51be6 to your computer and use it in GitHub Desktop.
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
}
}
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