diff --git a/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerPlayerView.kt b/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerPlayerView.kt index fa53bff..d1cffec 100644 --- a/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerPlayerView.kt +++ b/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerPlayerView.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent +import android.view.View import android.view.ViewConfiguration import android.widget.FrameLayout import androidx.media3.ui.PlayerView @@ -123,6 +124,26 @@ class OralTrainerPlayerView @JvmOverloads constructor( verticalSwipeListener = listener } + /** Forwards touches from an overlay (for example a subtitle layer) as player gestures. */ + fun forwardGestureFrom(source: View, event: MotionEvent): Boolean { + if (controller == null || !gestureControls.enabled) { + return false + } + val sourceLocation = IntArray(2).also(source::getLocationOnScreen) + val playerLocation = IntArray(2).also(::getLocationOnScreen) + val playerEvent = MotionEvent.obtain(event).apply { + offsetLocation( + (playerLocation[0] - sourceLocation[0]).toFloat(), + (playerLocation[1] - sourceLocation[1]).toFloat(), + ) + } + return try { + handleLongPressSpeed(playerEvent) || gestureDetector.onTouchEvent(playerEvent) + } finally { + playerEvent.recycle() + } + } + override fun onTouchEvent(event: MotionEvent): Boolean { return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) } diff --git a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/ui/PlayerChrome.kt b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/ui/PlayerChrome.kt index 2266cea..c1caeb9 100644 --- a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/ui/PlayerChrome.kt +++ b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/ui/PlayerChrome.kt @@ -8,6 +8,7 @@ import android.graphics.drawable.GradientDrawable import android.graphics.drawable.InsetDrawable import android.graphics.Typeface import android.text.TextUtils +import android.view.MotionEvent import android.view.Gravity import android.view.View import android.view.ViewGroup @@ -166,7 +167,28 @@ internal class PlayerChrome( addView(createPlayerView()) }, ) - val subtitleContainer = LinearLayout(activity).apply { + val subtitleContainer = object : LinearLayout(activity) { + private var forwardTouchToPlayer = false + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + forwardTouchToPlayer = !isTouchOverInteractiveControl(this, event) + } + val forwarded = if (forwardTouchToPlayer) { + bindings.playerView.forwardGestureFrom(this, event) + } else { + false + } + val handled = forwarded || super.dispatchTouchEvent(event) + if (event.actionMasked == MotionEvent.ACTION_UP || + event.actionMasked == MotionEvent.ACTION_CANCEL + ) { + forwardTouchToPlayer = false + } + return handled + } + + }.apply { orientation = LinearLayout.VERTICAL layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -503,4 +525,33 @@ internal class PlayerChrome( } view.startAnimation(animation) } + + /** Lets controls own their touches while the subtitle background drives player gestures. */ + private fun isTouchOverInteractiveControl(root: View, event: MotionEvent): Boolean = + hasInteractiveTouchTarget(root, event.rawX, event.rawY) + + private fun hasInteractiveTouchTarget(view: View, screenX: Float, screenY: Float): Boolean { + if (view.visibility != View.VISIBLE || !view.isEnabled) { + return false + } + if (view.isClickable || view.isLongClickable || view.isContextClickable) { + val location = IntArray(2).also(view::getLocationOnScreen) + if (screenX >= location[0] && + screenX < location[0] + view.width && + screenY >= location[1] && + screenY < location[1] + view.height + ) { + return true + } + } + if (view !is ViewGroup) { + return false + } + for (index in view.childCount - 1 downTo 0) { + if (hasInteractiveTouchTarget(view.getChildAt(index), screenX, screenY)) { + return true + } + } + return false + } }