Skip to content

Commit 1e93545

Browse files
committed
compose: H-round P0 fixes for library, demo and docs
Library (gsyVideoPlayer-compose): - GSYPlayerController: drop unused private context field; constructor now takes no arguments. Update rememberGSYPlayerController accordingly. - GSYGestureModifier: replace boxed mutableStateOf for Float/Int/Long with mutableFloatStateOf/mutableIntStateOf/mutableLongStateOf to avoid per-frame autoboxing inside drag callbacks; remove dead accumDelta branch and unused snapshotFlow/distinctUntilChanged imports. - syncFromHost: keep dual write to _snapshot + _stateFlow but document the same-thread, same-callsite contract; commit-time difference between Compose snapshot and StateFlow is semantic, not data drift. Demo (app/.../compose/host): - SubtitleComposeActivity / ExoSwitchSourceComposeActivity: snapshot the original PlayerFactory.sPlayerManager via reflection on entry and restore it in onDestroy, so Ijk/Exo overrides no longer leak into later demos. - MediaCodecComposeActivity: capture GSYVideoType.isMediaCodec on entry and restore enable/disableMediaCodec(+Texture) on dispose; drop unconditional disableMediaCodec and duplicate controller.release. - FloatingWindowComposeActivity: stop calling GSYVideoManager.releaseMediaPlayer in onDestroy (it nukes the shared FloatWindow player); FloatWindow.destroy already handles view detach + player release. - AudioOnly / LocalFile / CustomControlsTheme: remove redundant DisposableEffect onDispose controller.release - the rememberGSY PlayerController helper already disposes via its internal DisposableEffect. - CacheDownloadComposeActivity: replace derivedStateOf around ProxyCacheManager.newProxy(...).getProxyUrl(...) with a plain remember(appContext) block; derivedStateOf disallows side effects (newProxy starts an HTTP server). Docs (doc/COMPOSE_USE.md): - Replace non-existent controller.setOptions with the actual pass-through entries: controller.setUp(builder) / controller.withHost. - Replace controller.state with the real fields: controller.snapshot (Compose State) / controller.stateFlow. - Fix AudioOnly description: it does mount a 1dp invisible GSYPlayerSurface; the host requires a Surface carrier. - Fix SwitchSeamless row: native demo does not call SwitchUtil; it reuses the same GSYPlayerController + GSYPlayerSurface across Composables (host setSurfaceToPlay handles the takeover). Verification: - gsyVideoPlayer-compose:assembleRelease + app:assembleDebug both green. - emulator-5554 regression: D2 (Native detail), D11 (Subtitle, Ijk override), D15 (ExoSwitchSource, Exo2 override), back to D2 - PlayerFactory restored, no crash; D21 (AudioOnly), D23 (MediaCodec) start playback OK. - Monkey -p com.example.gsyvideoplayer 200 events: 0 FATAL, crash buffer 0.
1 parent 57bb9df commit 1e93545

12 files changed

Lines changed: 87 additions & 56 deletions

File tree

app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,7 @@ private fun AudioOnlyScreen() {
7777
GSYVideoManager.instance().enableRawPlay(ctx.applicationContext)
7878
}
7979

80-
DisposableEffect(Unit) {
81-
onDispose {
82-
runCatching { controller.release() }
83-
}
84-
}
80+
// controller 释放由 rememberGSYPlayerController 内部 DisposableEffect 托管,无需重复调用。
8581

8682
fun playAt(idx: Int) {
8783
current = idx

app/src/main/java/com/example/gsyvideoplayer/compose/host/CacheDownloadComposeActivity.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import androidx.compose.material3.Text
2424
import androidx.compose.material3.TopAppBar
2525
import androidx.compose.runtime.Composable
2626
import androidx.compose.runtime.LaunchedEffect
27-
import androidx.compose.runtime.derivedStateOf
2827
import androidx.compose.runtime.getValue
2928
import androidx.compose.runtime.mutableStateOf
3029
import androidx.compose.runtime.remember
@@ -62,13 +61,13 @@ private fun CacheDownloadScreen() {
6261
val snap by controller.snapshot
6362

6463
var statusText by remember { mutableStateOf("等待开始") }
65-
val proxyUrl by remember {
66-
derivedStateOf {
67-
// 用 ProxyCacheManager 套一层代理,命中缓存时第二次播放本地秒开
68-
// 与 Java DetailDownloadPlayer 同 API 路径
69-
val server = ProxyCacheManager.instance().newProxy(appContext)
70-
server.getProxyUrl(DemoSamples.SAMPLE_URL)
71-
}
64+
val proxyUrl = remember(appContext) {
65+
// 用 ProxyCacheManager 套一层代理,命中缓存时第二次播放本地秒开。
66+
// 与 Java DetailDownloadPlayer 同 API 路径
67+
// 注:不能用 derivedStateOf —— newProxy(...) 是带副作用的 IO 调用(会启动 HTTP 代理 server)
68+
// derivedStateOf 的 calculation 只能是纯派生函数,不允许有副作用。
69+
val server = ProxyCacheManager.instance().newProxy(appContext)
70+
server.getProxyUrl(DemoSamples.SAMPLE_URL)
7271
}
7372

7473
LaunchedEffect(proxyUrl) {

app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,7 @@ private fun CustomControlsThemeScreen() {
8080
controller.setUp(builder, autoPlay = true)
8181
}
8282

83-
DisposableEffect(Unit) {
84-
onDispose {
85-
runCatching { controller.release() }
86-
}
87-
}
83+
// controller 释放由 rememberGSYPlayerController 内部 DisposableEffect 托管,无需重复调用。
8884

8985
Scaffold(
9086
topBar = { TopAppBar(title = { Text("Compose 自定义主题 Controls Demo") }) }

app/src/main/java/com/example/gsyvideoplayer/compose/host/ExoSwitchSourceComposeActivity.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,18 @@ import com.shuyu.gsyvideoplayer.player.PlayerFactory
4141
import tv.danmaku.ijk.media.exo2.Exo2PlayerManager
4242

4343
class ExoSwitchSourceComposeActivity : ComponentActivity() {
44+
private var originalPlayManager: Class<*>? = null
45+
4446
override fun onCreate(savedInstanceState: Bundle?) {
4547
super.onCreate(savedInstanceState)
4648
// 切换到 EXO2 内核(与 Java DetailExoListPlayer 同条件)
4749
// 这是 GSY 的核心多内核能力之一:无需切换 Player 实现,UI 层完全复用。
50+
// 进入页面前通过反射记录原 PlayManager 类,离开时还原,避免污染后续 demo。
51+
originalPlayManager = runCatching {
52+
val f = PlayerFactory::class.java.getDeclaredField("sPlayerManager")
53+
f.isAccessible = true
54+
f.get(null) as? Class<*>
55+
}.getOrNull()
4856
PlayerFactory.setPlayManager(Exo2PlayerManager::class.java)
4957
setContent {
5058
MaterialTheme {
@@ -54,6 +62,16 @@ class ExoSwitchSourceComposeActivity : ComponentActivity() {
5462
}
5563
}
5664
}
65+
66+
override fun onDestroy() {
67+
super.onDestroy()
68+
@Suppress("UNCHECKED_CAST")
69+
originalPlayManager?.let {
70+
runCatching {
71+
PlayerFactory.setPlayManager(it as Class<out com.shuyu.gsyvideoplayer.player.IPlayerManager>)
72+
}
73+
}
74+
}
5775
}
5876

5977
private data class ExoSource(

app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ class FloatingWindowComposeActivity : ComponentActivity() {
5555

5656
override fun onDestroy() {
5757
super.onDestroy()
58-
GSYVideoManager.instance().releaseMediaPlayer()
58+
// 不再强制 GSYVideoManager.releaseMediaPlayer():
59+
// 该方法是全局门面,会顺手干掉悬浮窗里仍在播的常驻 player(FloatWindow.player 与
60+
// GSYVideoManager.instance() 共享同一个 IjkMediaPlayer 实例)。
61+
// FloatWindow.destroy() 自身在 view detach + player release 上已正确处理。
5962
FloatWindow.destroy()
6063
}
6164
}

app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,7 @@ private fun LocalFileScreen() {
8080
GSYVideoManager.instance().enableRawPlay(ctx.applicationContext)
8181
}
8282

83-
DisposableEffect(Unit) {
84-
onDispose {
85-
runCatching { controller.release() }
86-
}
87-
}
83+
// controller 释放由 rememberGSYPlayerController 内部 DisposableEffect 托管,无需重复调用。
8884

8985
fun start(url: String) {
9086
if (url.isBlank()) return

app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,23 @@ class MediaCodecComposeActivity : ComponentActivity() {
5454
private fun MediaCodecScreen() {
5555
val controller = rememberGSYPlayerController()
5656
val snapshot by controller.snapshot
57-
var hwOn by remember { mutableStateOf(GSYVideoType.isMediaCodec()) }
57+
val originalHw = remember { GSYVideoType.isMediaCodec() }
58+
var hwOn by remember { mutableStateOf(originalHw) }
5859
var lastEnabledAt by remember { mutableStateOf("") }
5960

6061
DisposableEffect(Unit) {
6162
onDispose {
62-
runCatching { controller.release() }
63-
runCatching { GSYVideoType.disableMediaCodec() }
63+
// 还原全局硬解开关,避免污染后续页面(GSYVideoType 是 process 级状态)。
64+
runCatching {
65+
if (originalHw) {
66+
GSYVideoType.enableMediaCodec()
67+
GSYVideoType.enableMediaCodecTexture()
68+
} else {
69+
GSYVideoType.disableMediaCodec()
70+
GSYVideoType.disableMediaCodecTexture()
71+
}
72+
}
73+
// controller 释放由 rememberGSYPlayerController 内部 DisposableEffect 托管。
6474
}
6575
}
6676

app/src/main/java/com/example/gsyvideoplayer/compose/host/SubtitleComposeActivity.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,18 @@ import com.shuyu.gsyvideoplayer.subtitle.GSYSubtitleSource
4545
import com.shuyu.gsyvideoplayer.subtitle.GSYSubtitleStyle
4646

4747
class SubtitleComposeActivity : ComponentActivity() {
48+
private var originalPlayManager: Class<*>? = null
49+
4850
override fun onCreate(savedInstanceState: Bundle?) {
4951
super.onCreate(savedInstanceState)
5052
// 通用字幕路径需要 IJK 内核(与 Java SubtitleDetailPlayer 同条件)。
53+
// 进入页面前通过反射记录原 PlayManager 类,离开时还原,避免污染后续 demo
54+
// (PlayerFactory.sPlayerManager 是 process 级单例)。
55+
originalPlayManager = runCatching {
56+
val f = PlayerFactory::class.java.getDeclaredField("sPlayerManager")
57+
f.isAccessible = true
58+
f.get(null) as? Class<*>
59+
}.getOrNull()
5160
PlayerFactory.setPlayManager(IjkPlayerManager::class.java)
5261
setContent {
5362
MaterialTheme {
@@ -57,6 +66,16 @@ class SubtitleComposeActivity : ComponentActivity() {
5766
}
5867
}
5968
}
69+
70+
override fun onDestroy() {
71+
super.onDestroy()
72+
@Suppress("UNCHECKED_CAST")
73+
originalPlayManager?.let {
74+
runCatching {
75+
PlayerFactory.setPlayManager(it as Class<out com.shuyu.gsyvideoplayer.player.IPlayerManager>)
76+
}
77+
}
78+
}
6079
}
6180

6281
@OptIn(ExperimentalMaterial3Api::class)

doc/COMPOSE_USE.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ App 模块下入口 `Compose Demo`([ComposeDemoListActivity.kt](../app/src/mai
320320
| 18 | 悬浮窗(画中画) | [FloatingWindowComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt) | `SYSTEM_ALERT_WINDOW` 拉起 `FloatPlayerView`,跨 Activity 常驻 |
321321
| 19 | 多类型列表 | [MoreTypeComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/MoreTypeComposeActivity.kt) | LazyColumn 多 type cell(Normal/Ad/Cover/Unknown) |
322322
| 20 | 图文混排(视频 + WebView) | [WebDetailComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/WebDetailComposeActivity.kt) | 上方 Compose 播放器 + 下方 `AndroidView` WebView 双栈共存 |
323-
| 21 | 纯音频播放 | [AudioOnlyComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) | raw 资源 + `enableRawPlay`;Compose 端只接 controller,不用 Surface |
323+
| 21 | 纯音频播放 | [AudioOnlyComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) | raw 资源 + `enableRawPlay`;Compose controller 仍要挂一个 `GSYPlayerSurface`(host 必须有 Surface 载体,1dp 隐身节点即可) |
324324
| 22 | 自定义 URL / 本地文件 | [LocalFileComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt) | URL 输入 + cache 切换 + `raw://` / `http://` 多种源类型 |
325325
| 23 | MediaCodec 硬解切换 | [MediaCodecComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt) | `GSYVideoType.enableMediaCodec()` / `disableMediaCodec()` 实时切换 |
326326
| 24 | 自定义主题 Controls | [CustomControlsThemeComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt) | Compose 自绘控件取代 `GSYDefaultControls`:渐变浮层 + Slider seek + 多主题切换 |
@@ -414,11 +414,11 @@ gsyVideoPlayer-compose/
414414
| 诉求 | Wrapper(`GSYVideoPlayerView` / `GSYAnyVideoPlayerView`) | Native(`GSYComposePlayer` / `GSYPlayerController` + `GSYPlayerSurface`) |
415415
|---|---|---|
416416
| 几乎零成本接入、与 Java demo 一一对应 | ✅ 推荐 | ⚠️ 需要自己写 UI / 状态订阅 |
417-
| 复用所有 `GSYVideoOptionBuilder` 链式选项(~30+ 项,含字幕、回调、缓存、滤镜) | ✅ 直接 `.build(player)` | ⚠️ 仅常用项有 controller 直挂入口(其余仍可通过 `controller.setOptions { ... }` 透传) |
417+
| 复用所有 `GSYVideoOptionBuilder` 链式选项(~30+ 项,含字幕、回调、缓存、滤镜) | ✅ 直接 `.build(player)` | ⚠️ 仅常用项有 controller 直挂入口(其余仍可通过 `controller.setUp(builder)` 完整透传,或 `controller.withHost { player -> ... }` 直访 host API) |
418418
| 内置全屏(`startWindowFullscreen`)、SwitchUtil 无缝切换 | ✅ 完整 | ❌ 需要自己组合(见 D8 SwitchSeamless) |
419419
| 列表多 item 各自独立播放(每行一个 player) | ✅ 但每个都是完整 View 树,性能略重 | ✅ 推荐:单 controller + LazyColumn 复用 surface |
420420
| 多窗口"真并行"(同屏多路同时出声) | ⚠️ 受 GSYVideoManager 互斥 | ✅ D7 多 CustomManager 实例 |
421-
| Compose 原生订阅 events / state(流式驱动 UI) | ❌ 内部仍是命令式 callback | ✅ 推荐:`controller.events` + `controller.state` |
421+
| Compose 原生订阅 events / state(流式驱动 UI) | ❌ 内部仍是命令式 callback | ✅ 推荐:`controller.events`(SharedFlow) + `controller.snapshot`(Compose `State<GSYPlayerSnapshot>`,`val s by controller.snapshot`) / `controller.stateFlow`(StateFlow) |
422422
| 完全自定义控件层(手势、按钮、字幕、画中画…) | ⚠️ 需要继承 `StandardGSYVideoPlayer` 重写 | ✅ 推荐:`GSYPlayerSurface` + 自绘 |
423423
| 需要"播完接力下一段"且**零黑屏** | ✅ Wrapper 路径走 SwitchUtil | ❌ 当前 Native setUp 会丢 surface 接管,会闪一下(P0-1 落地后改善,见 D7 注释) |
424424
| 需要在不打断播放的前提下,跨 Activity / Fragment 复用同一个播放实例 | ⚠️ 需要手写 `release` 兜底 | ✅ controller `rememberSaveable` 化是后续 backlog |
@@ -461,7 +461,7 @@ GSYVideoPlayerView(
461461

462462
**Compose(Wrapper)**:保持上方 builder 不变,把这三项打开即可,全屏由内部 `OrientationUtils` 接管;返回键拦截在 `BackHandler { ... }` 内调用 `player.onBackFullscreen()`
463463

464-
**Compose(Native)**:自绘控件层,监听 `controller.state.isPlaying` + 自定义 fullscreenComposable 切换,参考 [ListWithFullscreenActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt)
464+
**Compose(Native)**:自绘控件层,监听 `controller.snapshot.value.state.isPlaying` + 自定义 fullscreenComposable 切换,参考 [ListWithFullscreenActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt)
465465

466466
### 3) 列表内播放
467467

@@ -497,16 +497,16 @@ LaunchedEffect(listState, playingIndex) {
497497
})
498498
```
499499

500-
**Compose(Native)**:把 callback 替换为 `controller.events`(hot SharedFlow) / `controller.state`(StateFlow),用 `LaunchedEffect { controller.events.collect { ... } }` 即可订阅,**完全不依赖 callback 接口**
500+
**Compose(Native)**:把 callback 替换为 `controller.events`(hot SharedFlow) / `controller.stateFlow`(StateFlow) / `controller.snapshot`(Compose `State<GSYPlayerSnapshot>`),用 `LaunchedEffect { controller.events.collect { ... } }` 即可订阅,**完全不依赖 callback 接口**
501501

502502
### 5) 切源(同 player 切流 / 接力)
503503

504504
| 场景 | 路径 | 说明 |
505505
|---|---|---|
506506
| 暴力切源(允许一次黑屏 + 重新 prepare) | `controller.setUp(newUrl, ...)` | [SwitchUrlActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchUrlActivity.kt) |
507-
| 无缝切源(同 surface 跨实例复用,零黑屏) | `SwitchUtil.optionPlayer(...)` | [SwitchSeamlessComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) |
507+
| 无缝切源(同 controller 跨 Composable 复用,零黑屏) | 同一 `GSYPlayerController` + `GSYPlayerSurface` 在 list/detail 间换位 attach(host 内部 `setSurfaceToPlay` 接管,等价 Java 的 `SwitchUtil.savePlayState/clonePlayState` | [SwitchSeamlessComposeActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) |
508508
| 列表自动接力下一段 | `controller.events.collect { if (it is AutoComplete) controller.setUp(next) }` | [AutoPlayListActivity.kt](../app/src/main/java/com/example/gsyvideoplayer/compose/host/AutoPlayListActivity.kt) (顶部 KDoc 注释了 surface 接管取舍) |
509509

510510
> **通用原则**:Java 里"链式 builder + callback"的写法,**Wrapper 路径几乎等价复用**
511-
> Native 路径把 callback 换成 `events` / `state` 订阅,把 fullscreen 等粘性 View 行为改成"一个 Composable 状态切换"。
511+
> Native 路径把 callback 换成 `events` / `stateFlow` / `snapshot` 订阅,把 fullscreen 等粘性 View 行为改成"一个 Composable 状态切换"。
512512

gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYGestureModifier.kt

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import android.view.WindowManager
77
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
88
import androidx.compose.foundation.gestures.detectVerticalDragGestures
99
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableFloatStateOf
11+
import androidx.compose.runtime.mutableIntStateOf
12+
import androidx.compose.runtime.mutableLongStateOf
1013
import androidx.compose.runtime.mutableStateOf
1114
import androidx.compose.runtime.remember
1215
import androidx.compose.runtime.setValue
13-
import androidx.compose.runtime.snapshotFlow
1416
import androidx.compose.ui.Modifier
1517
import androidx.compose.ui.composed
1618
import androidx.compose.ui.geometry.Offset
@@ -19,7 +21,6 @@ import androidx.compose.ui.platform.LocalContext
1921
import androidx.compose.ui.platform.LocalView
2022
import androidx.compose.ui.unit.IntSize
2123
import androidx.compose.ui.layout.onSizeChanged
22-
import kotlinx.coroutines.flow.distinctUntilChanged
2324
import kotlin.math.abs
2425

2526
// =====================================================================================
@@ -100,11 +101,11 @@ fun Modifier.gsyGestureControl(
100101

101102
// 当前手势会话状态(仅在一次 down → up 间有效)
102103
var sessionType by remember { mutableStateOf(GSYGestureType.None) }
103-
var startX by remember { mutableStateOf(0f) }
104-
var startVolume by remember { mutableStateOf(0) }
105-
var startBrightness by remember { mutableStateOf(0f) }
106-
var startPosition by remember { mutableStateOf(0L) }
107-
var lastSeekTarget by remember { mutableStateOf(0L) }
104+
var startX by remember { mutableFloatStateOf(0f) }
105+
var startVolume by remember { mutableIntStateOf(0) }
106+
var startBrightness by remember { mutableFloatStateOf(0f) }
107+
var startPosition by remember { mutableLongStateOf(0L) }
108+
var lastSeekTarget by remember { mutableLongStateOf(0L) }
108109
var lastUpdate by remember { mutableStateOf(GSYGestureUpdate(GSYGestureType.None, 0f)) }
109110

110111
/**
@@ -203,13 +204,7 @@ fun Modifier.gsyGestureControl(
203204
GSYGestureType.Volume -> {
204205
val am = audioManager ?: return@detectVerticalDragGestures
205206
val max = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
206-
// 与 GSYVideoControlView.touchSurfaceMove 同算法:deltaY = -dragAmount,
207-
// ratio = max * (-dragAmount) * 3 / height
208-
val accumDelta = (change.position.y - change.previousPosition.y).let {
209-
// 取本帧与上一帧 y 差值积累,非纯瞬时
210-
it + (-dragAmount)
211-
}
212-
// 直接按 dragAmount 累加更稳:每像素映射 (3 * max / height) 步
207+
// 与 GSYVideoControlView.touchSurfaceMove 同算法:每像素映射 (3 * max / height) 步。
213208
val stepFloat = (-dragAmount * 3f * max / height)
214209
val newVol = (am.getStreamVolume(AudioManager.STREAM_MUSIC) + stepFloat.toInt())
215210
.coerceIn(0, max)

0 commit comments

Comments
 (0)