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 ae4b785..a758541 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
@@ -28,6 +28,7 @@ class OralTrainerPlayerView @JvmOverloads constructor(
private var longPressStartY = 0f
private var longPressActive = false
private var speedBeforeLongPress: Float? = null
+ private var verticalSwipeListener: ((isSwipeUp: Boolean) -> Unit)? = null
private val longPressSpeedRunnable = Runnable {
val activeController = controller ?: return@Runnable
@@ -70,6 +71,11 @@ class OralTrainerPlayerView @JvmOverloads constructor(
val dy = e2.y - start.y
val minDistancePx = gestureControls.minSwipeDistanceDp * density
val minVelocityPx = gestureControls.minSwipeVelocityDpPerSecond * density
+ if (abs(dy) > minDistancePx && abs(dy) > abs(dx) && abs(velocityY) >= minVelocityPx) {
+ val isSwipeUp = dy < 0
+ verticalSwipeListener?.invoke(isSwipeUp)
+ return true
+ }
if (abs(dx) < abs(dy) || abs(dx) < minDistancePx || abs(velocityX) < minVelocityPx) {
return false
}
@@ -112,6 +118,10 @@ class OralTrainerPlayerView @JvmOverloads constructor(
gestureControls = config
}
+ fun setVerticalSwipeListener(listener: ((isSwipeUp: Boolean) -> Unit)?) {
+ verticalSwipeListener = listener
+ }
+
override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
}
diff --git a/android/sample-app/build.gradle.kts b/android/sample-app/build.gradle.kts
index a043e68..6768f7a 100644
--- a/android/sample-app/build.gradle.kts
+++ b/android/sample-app/build.gradle.kts
@@ -19,7 +19,7 @@ android {
minSdk = 26
targetSdk = 35
versionCode = 1
- versionName = "0.1.0"
+ versionName = "0.1.1"
}
compileOptions {
diff --git a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt
index 9108917..31b9cc4 100644
--- a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt
+++ b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt
@@ -2,8 +2,11 @@ package cn.learningpad.oraltrainer.sample
import android.Manifest
import android.app.Activity
-import android.content.Intent
+import android.content.BroadcastReceiver
+import android.content.Context
import android.content.pm.ActivityInfo
+import android.content.Intent
+import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
@@ -17,11 +20,13 @@ import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
+import android.view.animation.Animation
+import android.view.animation.TranslateAnimation
import android.widget.Button
import android.widget.FrameLayout
-import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.ImageView
+import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.Switch
@@ -75,6 +80,15 @@ class MainActivity : Activity() {
private var catalogStatusTextValue = "正在同步"
private var activeModule = Module.TRAIN
private var continuousPlaybackEnabled = false
+ private var landscapeSubtitleVisible = true
+
+ private val screenActionReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == Intent.ACTION_SCREEN_OFF) {
+ controller.pause()
+ }
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -104,6 +118,11 @@ class MainActivity : Activity() {
setContentView(createContentView())
loadCatalog()
refreshCurrentUi()
+
+ registerReceiver(
+ screenActionReceiver,
+ IntentFilter(Intent.ACTION_SCREEN_OFF),
+ )
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -132,6 +151,33 @@ class MainActivity : Activity() {
}
}
+ override fun onPause() {
+ super.onPause()
+ controller.pause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ registerReceiver(
+ screenActionReceiver,
+ IntentFilter(Intent.ACTION_SCREEN_OFF),
+ )
+ }
+
+ override fun onStop() {
+ runCatching { unregisterReceiver(screenActionReceiver) }
+ super.onStop()
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onBackPressed() {
+ if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ } else {
+ moveTaskToBack(true)
+ }
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != PICK_VIDEO_REQUEST || resultCode != RESULT_OK) {
@@ -277,26 +323,82 @@ class MainActivity : Activity() {
setBackgroundColor(Color.BLACK)
addView(createPlayerView())
addView(createPlayerOverlay())
- addView(
- LinearLayout(this@MainActivity).apply {
- orientation = LinearLayout.VERTICAL
- layoutParams = FrameLayout.LayoutParams(
+ val subtitleContainer = LinearLayout(this@MainActivity).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.BOTTOM,
+ )
+ addView(createLandscapeSentencePanel().apply {
+ layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
- Gravity.BOTTOM,
)
- addView(createLandscapeSentencePanel().apply {
- layoutParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- )
- })
- addView(createRotationButtonRow())
- }
- )
+ })
+ }
+ addView(subtitleContainer)
+ applyLandscapeSubtitleState(subtitleContainer, animate = false)
+ installLandscapeSwipeGesture(subtitleContainer)
}
}
+ private fun installLandscapeSwipeGesture(subtitleContainer: View) {
+ playerView.setVerticalSwipeListener { isSwipeUp ->
+ if (isSwipeUp == landscapeSubtitleVisible) {
+ return@setVerticalSwipeListener
+ }
+ landscapeSubtitleVisible = isSwipeUp
+ applyLandscapeSubtitleState(subtitleContainer, animate = true)
+ }
+ }
+
+ private fun applyLandscapeSubtitleState(container: View, animate: Boolean) {
+ if (!animate || container.visibility == View.GONE) {
+ container.clearAnimation()
+ if (landscapeSubtitleVisible) {
+ container.visibility = View.VISIBLE
+ } else {
+ container.visibility = View.GONE
+ }
+ return
+ }
+ if (landscapeSubtitleVisible) {
+ container.visibility = View.VISIBLE
+ container.post {
+ if (landscapeSubtitleVisible && container.visibility == View.VISIBLE) {
+ startSlideAnimation(container, fromYDelta = 1f, toYDelta = 0f) {
+ container.visibility = View.VISIBLE
+ }
+ }
+ }
+ } else {
+ startSlideAnimation(container, fromYDelta = 0f, toYDelta = 1f) {
+ container.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun startSlideAnimation(
+ view: View,
+ fromYDelta: Float,
+ toYDelta: Float,
+ onEnd: () -> Unit,
+ ) {
+ val animation = TranslateAnimation(0f, 0f, view.height * fromYDelta, view.height * toYDelta).apply {
+ duration = 220L
+ fillAfter = true
+ setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationStart(animation: Animation?) = Unit
+ override fun onAnimationRepeat(animation: Animation?) = Unit
+ override fun onAnimationEnd(animation: Animation?) {
+ view.post(onEnd)
+ }
+ })
+ }
+ view.startAnimation(animation)
+ }
+
private fun createLandscapeSentencePanel(): View {
return createSentenceContent(translucent = true).apply {
layoutParams = FrameLayout.LayoutParams(
@@ -391,35 +493,18 @@ class MainActivity : Activity() {
return overlay
}
- private fun createRotationButtonRow(): View {
- return LinearLayout(this).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.END
- setPadding(0, 0, 12.dp, 12.dp)
- addView(createRotationButton())
- }
- }
-
private fun createRotationButton(): ImageButton {
- val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
return ImageButton(this).apply {
setImageResource(R.drawable.ic_screen_rotation)
imageTintList = ColorStateList.valueOf(Color.WHITE)
background = rounded(Color.argb(178, 9, 12, 16), 22.dp.toFloat())
scaleType = ImageView.ScaleType.CENTER_INSIDE
setPadding(10.dp, 10.dp, 10.dp, 10.dp)
- contentDescription = if (isLandscape) "竖屏" else "横屏"
+ contentDescription = "横屏"
layoutParams = LinearLayout.LayoutParams(44.dp, 44.dp)
- setOnClickListener { toggleOrientation() }
- }
- }
-
- private fun toggleOrientation() {
- val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
- requestedOrientation = if (isLandscape) {
- ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- } else {
- ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ setOnClickListener {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ }
}
}
diff --git a/android/sample-app/src/main/res/drawable/ic_screen_rotation.xml b/android/sample-app/src/main/res/drawable/ic_screen_rotation.xml
index 8fb167c..de5a6f7 100644
--- a/android/sample-app/src/main/res/drawable/ic_screen_rotation.xml
+++ b/android/sample-app/src/main/res/drawable/ic_screen_rotation.xml
@@ -1,9 +1,18 @@
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ android:strokeColor="@android:color/white"
+ android:strokeWidth="3.2"
+ android:pathData="M15,15h10v20h-10z"/>
+
+
diff --git a/android/sample-app/src/main/res/values/styles.xml b/android/sample-app/src/main/res/values/styles.xml
index 6d3edab..a0b8c1d 100644
--- a/android/sample-app/src/main/res/values/styles.xml
+++ b/android/sample-app/src/main/res/values/styles.xml
@@ -2,7 +2,8 @@