V1.3.5: 调音弹窗重构(音量+均衡器) + 公放切歌修复 + 远程均衡器可折叠
1) 公放歌曲按「切歌」不再停止 原先 advancePlayback 在已点队列为空时把「用户主动切歌」一律当作停止(进入空闲态), 导致公放(随机)歌曲播放中按切歌就停了。现在: 若当前曲目属于公放清单 (RandomPlayQueue.isCurrentTrack), 按切歌直接接着播清单里的下一首; 不调用 markFinished(跳过不等于播完, 不该 +1), 但它已在 markStarted 计入本轮已播, 所以本轮不会重复。playRandomLocalSong 新增 skipped 参数用于提示文案。 2) 「调音」弹窗重构为 App 内唯一音控入口(播放界面 + 主界面右上角同一弹窗) 自上而下: 音量(横向控制杆, 即时生效) → 均衡器开关(默认关闭) → 均衡器预设 (自定义/流行/摇滚/舞曲, 手改频段自动转「自定义」并保存) → EQ 频段(竖向控制杆)。 全部即时生效, 因此没有「完成/确认」按钮, 只有关闭。 新增自绘控件 VerticalEqBar(SeekBar 只支持横向): 触摸拖动 + 遥控器上下键步进, 量程端点放行按键以便焦点离开, 焦点态自绘橙色边框并 suppressFill。 均衡器预设改为 标准(仅供远程"恢复标准", 不在 App 行内)/流行/摇滚/舞曲, eqPreset 默认 "自定义"。 清除旧 UI 与代码: 删除 dialog_tuning.xml / dialog_audio_control.xml / CircularVolumeView.kt, 移除 showAudioControlDialog / showAlignedTuningDialog / showEqualizerDialog / showFullScreenVolumeDialog / bindAtmospherePreset / updateToggleState 及 fullScreenVolume* / fullScreenToneControl / hideFullScreenVolumeIndicatorRunnable 等字段与 dispatchKeyEvent 中的面板分支。 设置页「音频控制」改为指向调音弹窗(麦克风音量仍提示由功放硬件调节; 升降调保留在下一页「音调控制」)。 3) 远程点歌页面均衡器界面不变, 均衡器文字栏改为折叠按钮(默认收起)。 版本号 1.3.4 -> 1.3.5(versionCode 10 -> 11)。按约定本次不发布 Release, 仅本地打包测试。
This commit is contained in:
@@ -108,7 +108,7 @@ $BT/apksigner verify 目标名.apk
|
||||
|
||||
### 版本号
|
||||
|
||||
- `app/build.gradle`:`versionName "1.3.4"` / `versionCode 10`
|
||||
- `app/build.gradle`:`versionName "1.3.5"` / `versionCode 11`
|
||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||
|
||||
## 三、关键实现约束(避免踩坑)
|
||||
@@ -154,8 +154,8 @@ $BT/apksigner verify 目标名.apk
|
||||
统一在 `applyPlaybackMode()` 内按线性系数施加到播放器(`KtvVideoView.setPlaybackVolume` /
|
||||
`selectAudioChannel`)与副音轨播放器(`vocalPlayer` / `embeddedVocalEngine`)。
|
||||
**严禁调用 `AudioManager.setStreamVolume` / `adjustStreamVolume` 修改系统音量**。
|
||||
三条入口必须全部收敛到 `applyPlaybackMode()`:播放界面-调音(`showFullScreenVolumeDialog()` 与
|
||||
`showAudioControlDialog()` 的音乐音量滑杆)、声音设置-歌曲最大音量、远程控制-音乐音量
|
||||
三条入口必须全部收敛到 `applyPlaybackMode()`:**「调音」弹窗**(`showTuningDialog()`,播放界面与
|
||||
主界面右上角共用同一个弹窗)的音量横向控制杆、声音设置-歌曲最大音量、远程控制-音乐音量
|
||||
(`remoteAudio()` 的 `musicVolume`)。曲线是纯衰减(100% 即 unity gain),不做系统级增益;
|
||||
设备最终响度仍取决于系统音量,App 不再替用户改动它
|
||||
- **音频均衡器**:内核 `KtvEqualizer`(参考开源项目 OpenEQ,`Turbofan3360/OpenEQ`,GPL-3.0 的
|
||||
@@ -165,10 +165,15 @@ $BT/apksigner verify 目标名.apk
|
||||
(IJK 的原生 `IjkMediaPlayer_getAudioSessionId` 返回真实 AudioTrack 会话)。会话会随歌曲变化,
|
||||
因此由 500ms 的 `lyricTicker` → `syncEqualizerSessions()` 周期对齐,消失的会话延迟 3s 释放。
|
||||
`globalMix`(会话 0)是设备不支持按会话挂载时的显式开关,会影响本机所有声音,默认关闭。
|
||||
四个入口:主页顶栏「调音」弹窗里的「均衡器」、播放界面「调音」面板里的「均衡器」、
|
||||
声音设置「均衡器入口」、远程点歌页面「均衡器」卡片(`PATCH /api/v1/player/equalizer`,
|
||||
状态随 `GET /api/v1/state` 的 `equalizer` 字段下发)。设置持久化在 `KtvStore.eq*`;
|
||||
**滑杆刻度固定 30 档**(`eqSliderSteps`),步进由设备增益范围均分,保证 0 dB 一定可选中
|
||||
均衡器已并入**「调音」弹窗**(`showTuningDialog()`,自上而下:音量横向杆 → 均衡器开关 →
|
||||
预设 → EQ 竖向杆),主界面右上角与播放界面「调音」是同一个弹窗;另有声音设置入口与
|
||||
远程点歌页面「均衡器」卡片(`PATCH /api/v1/player/equalizer`,状态随
|
||||
`GET /api/v1/state` 的 `equalizer` 字段下发)。预设:App 内为「自定义/流行/摇滚/舞曲」
|
||||
(`appPresetNames()`,手动改频段即转「自定义」并自动保存),远程页面额外保留「标准」
|
||||
供「恢复标准」使用。设置持久化在 `KtvStore.eq*`;
|
||||
**刻度固定 30 档**(`eqSliderSteps`),步进由设备增益范围均分,保证 0 dB 一定可选中。
|
||||
EQ 竖向杆是自绘控件 `VerticalEqBar`(SeekBar 只支持横向),已 `suppressFill` 并自绘焦点框;
|
||||
它在上下键到达量程端点时会放行按键,否则遥控器会困在滑杆上出不去
|
||||
- **公放随机清单**:`RandomPlayQueue`(纯内存单例)——随机续播不再「每次现随机挑一首」,
|
||||
而是一次性生成打乱顺序的临时清单,一轮内不重复;播完一首 `markFinished` +1;
|
||||
`next()` 每次都重新比对曲库,**自动吸纳新增歌曲**、剔除已删除歌曲;一轮播完自动开新一轮。
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.local.ktv"
|
||||
minSdk 21
|
||||
targetSdk 25
|
||||
versionCode 10
|
||||
versionName "1.3.4"
|
||||
versionCode 11
|
||||
versionName "1.3.5"
|
||||
ndk {
|
||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
.empty.compact{padding:42px 15px}
|
||||
|
||||
.audio-panel label.mic-hw{opacity:.55}.audio-panel label.mic-hw>small{grid-column:1/3;color:#9f8eb0;font-size:10px;margin-top:2px}.audio-panel label.mic-hw input[type=range]{background:rgba(255,255,255,.14)}.eq-panel{padding:18px;margin-top:14px}.eq-switch{grid-template-columns:1fr 1fr}.eq-bands{margin-top:6px}.eq-band{display:grid;grid-template-columns:1fr auto;align-items:center;margin-top:14px;font-size:12px;color:#e9dfef}.eq-band>span{color:#cbbad9}.eq-band>b{color:var(--gold);font-size:12px}.eq-band b em{font-style:normal;font-size:14px}.eq-presets{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-top:17px}.eq-preset{height:40px;border-radius:13px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.09);color:#d9cce4;font-size:12px}.eq-preset.active{color:#36123d;background:linear-gradient(135deg,#ffdf7f,#ffb84a);font-weight:700}.eq-hint{margin:14px 0 0;color:#9f8eb0;font-size:10px;line-height:1.5}
|
||||
.eq-title{width:100%;padding:0;margin:0;background:transparent;border:0;text-align:left;cursor:pointer}.eq-title-right{display:flex;align-items:center;gap:8px}.eq-chevron{font-style:normal;color:#c9a8e8;font-size:13px;transition:transform .2s}.eq-title[aria-expanded=true] .eq-chevron{transform:rotate(180deg)}.eq-body{margin-top:4px}
|
||||
|
||||
@@ -275,6 +275,13 @@ $("#musicVolume").addEventListener("change", (event) => updateAudio({ musicVolum
|
||||
// 均衡器入口(与设备端弹窗等价): 开关 / 恢复标准
|
||||
$("#eqToggle")?.addEventListener("click", () => updateEqualizer({ enabled: !uiState.eqEnabled }));
|
||||
$("#eqReset")?.addEventListener("click", () => updateEqualizer({ preset: "标准" }));
|
||||
// 均衡器文字栏作为折叠按钮: 默认收起; 展开后在同一次页面会话内保持, 重新加载页面回到收起态。
|
||||
$("#eqCollapse")?.addEventListener("click", () => {
|
||||
const body = $("#eqBody");
|
||||
if (!body) return;
|
||||
const collapsed = body.classList.toggle("hidden");
|
||||
$("#eqCollapse").setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
});
|
||||
document.addEventListener("visibilitychange", () => { uiState.visible = !document.hidden; if (uiState.visible) refresh(); });
|
||||
window.setInterval(() => { if (uiState.visible) refresh(); }, 1000);
|
||||
refresh();
|
||||
|
||||
@@ -41,7 +41,11 @@
|
||||
</article>
|
||||
|
||||
<article class="glass-card eq-panel">
|
||||
<div class="panel-title"><div><span class="tiny-icon">≡</span><b>均衡器</b></div><small id="eqState">已关闭</small></div>
|
||||
<button type="button" class="panel-title eq-title" id="eqCollapse" aria-expanded="false">
|
||||
<div><span class="tiny-icon">≡</span><b>均衡器</b></div>
|
||||
<span class="eq-title-right"><small id="eqState">已关闭</small><i class="eq-chevron">▾</i></span>
|
||||
</button>
|
||||
<div id="eqBody" class="eq-body hidden">
|
||||
<div class="seg eq-switch">
|
||||
<button type="button" id="eqToggle">开启均衡器</button>
|
||||
<button type="button" id="eqReset">恢复标准</button>
|
||||
@@ -49,6 +53,7 @@
|
||||
<div id="eqBands" class="eq-bands"></div>
|
||||
<div id="eqPresets" class="eq-presets"></div>
|
||||
<p class="eq-hint" id="eqHint">均衡器作用于本机播放输出,不改动系统音量</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="next-card"><div class="next-number">02</div><div><small>下一首</small><b id="nextSong">队列为空</b></div><button data-page="queue">查看队列 <span>›</span></button></article>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.local.ktv
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.View
|
||||
import kotlin.math.min
|
||||
|
||||
/** Full-screen volume indicator modelled after the original KTV circular overlay. */
|
||||
class CircularVolumeView(context: Context) : View(context) {
|
||||
private val density = resources.displayMetrics.density
|
||||
private val arcBounds = RectF()
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private var volume = 0
|
||||
|
||||
fun setVolume(value: Int) {
|
||||
volume = value.coerceIn(0, 100)
|
||||
contentDescription = "音量 $volume"
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desired = (154f * density).toInt()
|
||||
val width = resolveSize(desired, widthMeasureSpec)
|
||||
val height = resolveSize(desired, heightMeasureSpec)
|
||||
val size = min(width, height)
|
||||
setMeasuredDimension(size, size)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val size = min(width, height).toFloat()
|
||||
val center = size / 2f
|
||||
val outerWidth = 12f * density
|
||||
val progressWidth = 12f * density
|
||||
val radius = center - outerWidth
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = Color.rgb(48, 39, 102)
|
||||
canvas.drawCircle(center, center, radius - progressWidth * 0.15f, paint)
|
||||
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeCap = Paint.Cap.BUTT
|
||||
paint.strokeWidth = outerWidth
|
||||
paint.color = Color.rgb(47, 37, 106)
|
||||
canvas.drawCircle(center, center, radius, paint)
|
||||
|
||||
val ringRadius = radius - outerWidth * 0.52f
|
||||
arcBounds.set(
|
||||
center - ringRadius,
|
||||
center - ringRadius,
|
||||
center + ringRadius,
|
||||
center + ringRadius,
|
||||
)
|
||||
paint.strokeWidth = progressWidth
|
||||
paint.color = Color.rgb(60, 51, 111)
|
||||
canvas.drawArc(arcBounds, -90f, 360f, false, paint)
|
||||
paint.color = Color.rgb(151, 155, 143)
|
||||
canvas.drawArc(arcBounds, -90f, volume * 3.6f, false, paint)
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
paint.color = Color.rgb(220, 220, 214)
|
||||
paint.textSize = 30f * density
|
||||
canvas.drawText(volume.toString(), center, center - 3f * density, paint)
|
||||
paint.textSize = 20f * density
|
||||
canvas.drawText("音量", center, center + 38f * density, paint)
|
||||
}
|
||||
}
|
||||
@@ -55,25 +55,30 @@ object KtvEqualizer {
|
||||
/**
|
||||
* 预设曲线:以「频段在可用范围内的相对位置 0..1」为横轴、以最大增益的比例 -1..1 为纵轴。
|
||||
* 这样同一套预设能自适应任意频段数的设备。
|
||||
*
|
||||
* `标准` 不显示在 App 的预设行里(App 用「自定义」表示用户自己调的状态),
|
||||
* 但保留给远程页面的「恢复标准」按钮使用。
|
||||
*/
|
||||
private val PRESETS: LinkedHashMap<String, List<Pair<Float, Float>>> = linkedMapOf(
|
||||
"标准" to listOf(0f to 0f),
|
||||
"流行" to listOf(0f to 0.35f, 0.25f to -0.15f, 0.6f to 0.1f, 1f to 0.35f),
|
||||
"抒情" to listOf(0f to -0.1f, 0.35f to 0.25f, 0.7f to 0.15f, 1f to -0.05f),
|
||||
"摇滚" to listOf(0f to 0.7f, 0.3f to -0.4f, 1f to 0.6f),
|
||||
"唱将" to listOf(0f to -0.2f, 0.5f to 0.55f, 0.8f to 0.3f, 1f to 0.05f),
|
||||
"舞曲" to listOf(0f to 0.85f, 0.22f to 0.2f, 0.5f to -0.35f, 0.78f to 0.25f, 1f to 0.65f),
|
||||
)
|
||||
|
||||
/** 手动改过频段后使用的预设名。 */
|
||||
const val CUSTOM_PRESET = "自定义"
|
||||
|
||||
/** App 内「调音」弹窗预设行的顺序(含「自定义」)。 */
|
||||
fun appPresetNames(): List<String> = listOf(CUSTOM_PRESET, "流行", "摇滚", "舞曲")
|
||||
|
||||
@Volatile var enabled: Boolean = false
|
||||
private set
|
||||
|
||||
@Volatile var globalMix: Boolean = false
|
||||
private set
|
||||
|
||||
@Volatile var preset: String = "标准"
|
||||
@Volatile var preset: String = CUSTOM_PRESET
|
||||
private set
|
||||
|
||||
private var levels: FloatArray = FloatArray(0)
|
||||
|
||||
@@ -49,7 +49,7 @@ class KtvStore {
|
||||
@JvmField var tableBroadcastText = "欢迎光临"
|
||||
// ===== 均衡器(KtvEqualizer)=====
|
||||
@JvmField var eqEnabled = false
|
||||
@JvmField var eqPreset = "标准"
|
||||
@JvmField var eqPreset = "自定义"
|
||||
/** 各频段增益(dB),逗号分隔;不同设备频段数可能不同,加载后会按实际频段数对齐。 */
|
||||
@JvmField var eqLevels = ""
|
||||
@JvmField var eqGlobalMix = false
|
||||
@@ -103,7 +103,7 @@ class KtvStore {
|
||||
tableBroadcastSeconds = root.optInt("tableBroadcastSeconds", 8)
|
||||
tableBroadcastText = root.optString("tableBroadcastText", "欢迎光临")
|
||||
eqEnabled = root.optBoolean("eqEnabled", false)
|
||||
eqPreset = root.optString("eqPreset", "标准")
|
||||
eqPreset = root.optString("eqPreset", "自定义")
|
||||
eqLevels = root.optString("eqLevels", "")
|
||||
eqGlobalMix = root.optBoolean("eqGlobalMix", false)
|
||||
readSongs(root.optJSONArray("orderQueue"), orderQueue)
|
||||
|
||||
@@ -1004,8 +1004,8 @@ class MainActivity : AppCompatActivity() {
|
||||
addTile(
|
||||
grid,
|
||||
"音量音调",
|
||||
"原伴唱 / 音量 / 升降调",
|
||||
View.OnClickListener { v: View? -> showAudioControlDialog() })
|
||||
"音量 / 均衡器",
|
||||
View.OnClickListener { v: View? -> showTuningDialog() })
|
||||
addTile(
|
||||
grid,
|
||||
"气氛模式",
|
||||
@@ -1118,7 +1118,7 @@ class MainActivity : AppCompatActivity() {
|
||||
controls.addView(
|
||||
button(
|
||||
"音控",
|
||||
View.OnClickListener { v: View? -> showAudioControlDialog() }),
|
||||
View.OnClickListener { v: View? -> showTuningDialog() }),
|
||||
LinearLayout.LayoutParams(0, dp(50), 1f)
|
||||
)
|
||||
controls.addView(
|
||||
@@ -1858,23 +1858,23 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
1 -> {
|
||||
// 音频控制(链接到调音弹窗)
|
||||
// 音量与均衡器统一走「调音」弹窗(升降调在下一页单独设置)
|
||||
addSettingsTitle(content, "音频控制")
|
||||
addSettingsItem(
|
||||
content,
|
||||
"音量与均衡器",
|
||||
"音量 " + musicVolume + "% · 均衡器" + (if (KtvEqualizer.enabled) "开" else "关"),
|
||||
View.OnClickListener { v: View? -> showTuningDialog() })
|
||||
addSettingsItem(
|
||||
content,
|
||||
"均衡器预设",
|
||||
KtvEqualizer.preset,
|
||||
View.OnClickListener { v: View? -> showTuningDialog() })
|
||||
addSettingsItem(
|
||||
content,
|
||||
"麦克风音量",
|
||||
micVolume.toString() + "%",
|
||||
View.OnClickListener { v: View? -> showAudioControlDialog() })
|
||||
addSettingsItem(
|
||||
content,
|
||||
"音乐音量",
|
||||
musicVolume.toString() + "%",
|
||||
View.OnClickListener { v: View? -> showAudioControlDialog() })
|
||||
addSettingsItem(
|
||||
content,
|
||||
"音调",
|
||||
(if (tone > 0) "+" else "") + tone + " 调",
|
||||
View.OnClickListener { v: View? -> showAudioControlDialog() })
|
||||
"由功放/混音器硬件调节",
|
||||
View.OnClickListener { v: View? -> toast("麦克风音量由功放/混音器硬件调节") })
|
||||
addSettingsItem(
|
||||
content,
|
||||
"原唱模式",
|
||||
@@ -2128,7 +2128,7 @@ class MainActivity : AppCompatActivity() {
|
||||
* * `onPlayPause` → [.togglePlay]
|
||||
* * `onPrev` / `onNext` → [.playPrevSong] / [.playNext]
|
||||
* * `onVocalSwitch` → 使用 [VocalSwitchHelper] 切换原唱/伴唱声道
|
||||
* * `onVolume` → [.showAudioControlDialog]
|
||||
* * `onVolume` → [.showTuningDialog]
|
||||
* * `onFullScreen` → [.showFullScreenPlayer]
|
||||
*
|
||||
*/
|
||||
@@ -2152,7 +2152,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onVolume() {
|
||||
showAudioControlDialog()
|
||||
showTuningDialog()
|
||||
}
|
||||
|
||||
override fun onFullScreen() {
|
||||
@@ -2266,8 +2266,7 @@ class MainActivity : AppCompatActivity() {
|
||||
if (playWhenPrepared) "暂停" else "播放"
|
||||
) { togglePlay() }.also { (text, icon) -> fullScreenPauseText = text; fullScreenPauseIcon = icon }
|
||||
addControl(R.drawable.ott_ic_ctrl_replay, "重唱") { replay() }
|
||||
addControl(R.drawable.ott_ic_ctrl_volume, "调音") { showFullScreenVolumeDialog() }
|
||||
fullScreenToneControl = controlItems.lastOrNull()
|
||||
addControl(R.drawable.ott_ic_ctrl_volume, "调音") { showTuningDialog() }
|
||||
|
||||
controlItems.forEachIndexed { index, item ->
|
||||
item.nextFocusLeftId = controlItems.getOrNull(index - 1)?.id ?: item.id
|
||||
@@ -2359,16 +2358,9 @@ class MainActivity : AppCompatActivity() {
|
||||
fullScreenSeeking = false
|
||||
fullScreenControls = null
|
||||
fullScreenProgressRow = null
|
||||
fullScreenVolumePanel = null
|
||||
fullScreenVolumeIndicator = null
|
||||
fullScreenVolumeMinusButton = null
|
||||
fullScreenVolumePlusButton = null
|
||||
fullScreenVolumeMuteButton = null
|
||||
fullScreenToneControl = null
|
||||
fullScreenQrOverlay = null
|
||||
fullScreenChromeVisible = false
|
||||
main.removeCallbacks(hideFullScreenChromeRunnable)
|
||||
main.removeCallbacks(hideFullScreenVolumeIndicatorRunnable)
|
||||
scheduleAutoFullscreen()
|
||||
}
|
||||
|
||||
@@ -2377,11 +2369,10 @@ class MainActivity : AppCompatActivity() {
|
||||
fullScreenChromeVisible = visible
|
||||
fullScreenControls?.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
fullScreenProgressRow?.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
if (!visible) fullScreenVolumePanel?.visibility = View.GONE
|
||||
main.removeCallbacks(hideFullScreenChromeRunnable)
|
||||
if (visible && isFullScreen) {
|
||||
val controls = fullScreenControls
|
||||
if (controls != null && !controls.hasFocus() && fullScreenVolumePanel?.hasFocus() != true) {
|
||||
if (controls != null && !controls.hasFocus()) {
|
||||
controls.getChildAt(0)?.requestFocus()
|
||||
}
|
||||
main.postDelayed(hideFullScreenChromeRunnable, 5000L)
|
||||
@@ -2411,140 +2402,9 @@ class MainActivity : AppCompatActivity() {
|
||||
* 供 [FullScreenPlayerFragment] 调用打开音量控制面板。
|
||||
*/
|
||||
fun showAudioControlFromFragment() {
|
||||
showAudioControlDialog()
|
||||
showTuningDialog()
|
||||
}
|
||||
|
||||
private fun showFullScreenVolumeDialog() {
|
||||
if (!isFullScreen || fullScreenContainer == null) return
|
||||
val existing = fullScreenVolumePanel
|
||||
if (existing != null) {
|
||||
existing.visibility = if (existing.visibility == View.VISIBLE) View.GONE else View.VISIBLE
|
||||
if (existing.visibility == View.VISIBLE) {
|
||||
fullScreenVolumeMinusButton?.requestFocus()
|
||||
} else {
|
||||
fullScreenToneControl?.requestFocus()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (musicVolume > 0) fullScreenPreviousVolume = musicVolume
|
||||
val panel = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
// 仅用于吞掉触摸(点击穿透到全屏容器会误切控制层显示), 不是操作项;
|
||||
// 因此禁止焦点/按下填充, 否则触摸时整块面板会变成纯色。
|
||||
isClickable = true
|
||||
}
|
||||
TvFocusStyler.suppressFill(panel)
|
||||
val indicator = CircularVolumeView(this).apply {
|
||||
setVolume(musicVolume)
|
||||
visibility = View.GONE
|
||||
alpha = 1f
|
||||
}
|
||||
panel.addView(indicator, LinearLayout.LayoutParams(dp(154), dp(154)).apply {
|
||||
bottomMargin = dp(22)
|
||||
})
|
||||
panel.addView(label("调音", 26, Color.WHITE).apply {
|
||||
gravity = Gravity.CENTER
|
||||
}, LinearLayout.LayoutParams(dp(420), dp(54)))
|
||||
|
||||
val buttons = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
fun volumeButton(text: String): TextView = TextView(this).apply {
|
||||
this.text = text
|
||||
id = View.generateViewId()
|
||||
gravity = Gravity.CENTER
|
||||
textSize = 18f
|
||||
setTextColor(Color.WHITE)
|
||||
setBackgroundResource(R.drawable.ott_bg_volume_ctrl_action_button)
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
isClickable = true
|
||||
includeFontPadding = false
|
||||
setPadding(dp(14), 0, dp(14), 0)
|
||||
}
|
||||
val minus = volumeButton("音量 -")
|
||||
val plus = volumeButton("音量 +")
|
||||
val mute = volumeButton("静音").apply {
|
||||
compoundDrawablePadding = dp(8)
|
||||
}
|
||||
fun renderMuteButton() {
|
||||
mute.text = if (musicVolume == 0) "放音" else "静音"
|
||||
val drawable = getDrawable(
|
||||
if (musicVolume == 0) R.drawable.ic_ctrl_sound_enable
|
||||
else R.drawable.ic_ctrl_sound_disable
|
||||
)
|
||||
drawable?.setBounds(0, 0, dp(23), dp(23))
|
||||
mute.setCompoundDrawables(null, null, drawable, null)
|
||||
}
|
||||
fun showVolumeResult() {
|
||||
indicator.animate().cancel()
|
||||
indicator.setVolume(musicVolume)
|
||||
indicator.alpha = 1f
|
||||
indicator.visibility = View.VISIBLE
|
||||
renderMuteButton()
|
||||
// 只调 App 内部输出音量, 不改系统音量(见 applyPlaybackMode)。
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
main.removeCallbacks(hideFullScreenVolumeIndicatorRunnable)
|
||||
main.postDelayed(hideFullScreenVolumeIndicatorRunnable, 2200L)
|
||||
setFullScreenChromeVisible(true)
|
||||
}
|
||||
minus.setOnClickListener {
|
||||
musicVolume = (musicVolume - 5).coerceAtLeast(0)
|
||||
if (musicVolume > 0) fullScreenPreviousVolume = musicVolume
|
||||
showVolumeResult()
|
||||
}
|
||||
plus.setOnClickListener {
|
||||
musicVolume = (musicVolume + 5).coerceAtMost(100)
|
||||
if (musicVolume > 0) fullScreenPreviousVolume = musicVolume
|
||||
showVolumeResult()
|
||||
}
|
||||
mute.setOnClickListener {
|
||||
if (musicVolume > 0) {
|
||||
fullScreenPreviousVolume = musicVolume
|
||||
musicVolume = 0
|
||||
} else {
|
||||
musicVolume = fullScreenPreviousVolume.coerceAtLeast(5)
|
||||
}
|
||||
showVolumeResult()
|
||||
}
|
||||
// 播放界面「调音」面板里的均衡器入口(与主页顶栏「调音」弹窗里的入口等价)。
|
||||
val equalizer = volumeButton("均衡器")
|
||||
equalizer.setOnClickListener { showEqualizerDialog() }
|
||||
val volumeButtons = listOf(minus, plus, mute, equalizer)
|
||||
volumeButtons.forEachIndexed { index, button ->
|
||||
button.nextFocusLeftId = volumeButtons.getOrNull(index - 1)?.id ?: button.id
|
||||
button.nextFocusRightId = volumeButtons.getOrNull(index + 1)?.id ?: button.id
|
||||
button.nextFocusUpId = fullScreenToneControl?.id ?: button.id
|
||||
button.nextFocusDownId = button.id
|
||||
buttons.addView(button, LinearLayout.LayoutParams(dp(136), dp(58)).apply {
|
||||
leftMargin = dp(6)
|
||||
rightMargin = dp(6)
|
||||
})
|
||||
}
|
||||
panel.addView(buttons, LinearLayout.LayoutParams(-2, dp(70)).apply { topMargin = dp(12) })
|
||||
renderMuteButton()
|
||||
|
||||
fullScreenContainer!!.addView(
|
||||
panel,
|
||||
FrameLayout.LayoutParams(-2, -2, Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM).apply {
|
||||
bottomMargin = dp(86)
|
||||
},
|
||||
)
|
||||
fullScreenVolumePanel = panel
|
||||
fullScreenVolumeIndicator = indicator
|
||||
fullScreenVolumeMinusButton = minus
|
||||
fullScreenVolumePlusButton = plus
|
||||
fullScreenVolumeMuteButton = mute
|
||||
minus.requestFocus()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一首歌曲(供控制层 onPrev 回调使用)。
|
||||
*/
|
||||
fun playPrevSong() {
|
||||
if (orderQueue!!.isEmpty() || currentSong == null) return
|
||||
val index = orderQueue.indexOf(currentSong!!)
|
||||
@@ -2590,197 +2450,6 @@ class MainActivity : AppCompatActivity() {
|
||||
saveState()
|
||||
toast("声道模式:" + vocalChannelMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示调音/音频控制弹窗(对应"弹窗-调音.jpg")
|
||||
* 使用 dialog_audio_control.xml 实现
|
||||
*/
|
||||
private fun showAudioControlDialog() {
|
||||
if (showAlignedTuningDialog()) return
|
||||
val root = getLayoutInflater().inflate(R.layout.dialog_audio_control, null)
|
||||
val btnClose = root.findViewById<TextView>(R.id.btn_audio_close)
|
||||
val btnStereo = root.findViewById<TextView>(R.id.btn_tone_stereo)
|
||||
val btnAccomp = root.findViewById<TextView>(R.id.btn_tone_accomp)
|
||||
val btnOrigin = root.findViewById<TextView>(R.id.btn_tone_origin)
|
||||
val btnMute = root.findViewById<TextView>(R.id.btn_tone_mute)
|
||||
val seekMicVol = root.findViewById<SeekBar>(R.id.seek_mic_volume)
|
||||
val seekMicEffect = root.findViewById<SeekBar>(R.id.seek_mic_effect)
|
||||
val seekMusicVol = root.findViewById<SeekBar>(R.id.seek_music_volume)
|
||||
val tvMicVol = root.findViewById<TextView>(R.id.text_mic_volume_value)
|
||||
val tvMicEffect = root.findViewById<TextView>(R.id.text_mic_effect_value)
|
||||
val tvMusicVol = root.findViewById<TextView>(R.id.text_music_volume_value)
|
||||
val btnDefault = root.findViewById<TextView>(R.id.btn_audio_default)
|
||||
val btnSave = root.findViewById<TextView>(R.id.btn_audio_save)
|
||||
|
||||
// 初始值
|
||||
seekMicVol.setProgress(micVolume)
|
||||
seekMicEffect.setProgress(50)
|
||||
seekMusicVol.setProgress(musicVolume)
|
||||
tvMicVol.setText(micVolume.toString())
|
||||
tvMicEffect.setText("50")
|
||||
tvMusicVol.setText(musicVolume.toString())
|
||||
|
||||
// 4 个切换按钮:伴唱 / 原唱 / 立体声 / 静音
|
||||
updateToggleState(btnAccomp, !originalVocal)
|
||||
updateToggleState(btnOrigin, originalVocal)
|
||||
updateToggleState(btnStereo, "自动" == vocalChannelMode)
|
||||
updateToggleState(btnMute, false)
|
||||
btnAccomp.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
if (originalVocal) toggleOriginalVocal()
|
||||
updateToggleState(btnAccomp, true)
|
||||
updateToggleState(btnOrigin, false)
|
||||
})
|
||||
btnOrigin.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
if (!originalVocal) toggleOriginalVocal()
|
||||
updateToggleState(btnAccomp, false)
|
||||
updateToggleState(btnOrigin, true)
|
||||
})
|
||||
btnStereo.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
setVocalChannelMode("自动")
|
||||
updateToggleState(btnStereo, true)
|
||||
})
|
||||
btnMute.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
if (musicVolume > 0) {
|
||||
muteBeforeVol = musicVolume
|
||||
musicVolume = 0
|
||||
} else {
|
||||
musicVolume = muteBeforeVol
|
||||
}
|
||||
applyPlaybackMode()
|
||||
seekMusicVol.setProgress(musicVolume)
|
||||
tvMusicVol.setText(musicVolume.toString())
|
||||
updateToggleState(btnMute, musicVolume == 0)
|
||||
saveState()
|
||||
})
|
||||
|
||||
// 3 个滑块
|
||||
seekMicVol.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(s: SeekBar?, p: Int, u: Boolean) {
|
||||
micVolume = p
|
||||
tvMicVol.setText(p.toString())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(s: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(s: SeekBar?) {
|
||||
saveState()
|
||||
}
|
||||
})
|
||||
seekMicEffect.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(s: SeekBar?, p: Int, u: Boolean) {
|
||||
tvMicEffect.setText(p.toString())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(s: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(s: SeekBar?) {}
|
||||
})
|
||||
seekMusicVol.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(s: SeekBar?, p: Int, u: Boolean) {
|
||||
musicVolume = p
|
||||
tvMusicVol.setText(p.toString())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(s: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(s: SeekBar?) {
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
}
|
||||
})
|
||||
|
||||
// 默认 / 保存
|
||||
btnDefault.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
seekMicVol.setProgress(60)
|
||||
seekMicEffect.setProgress(50)
|
||||
seekMusicVol.setProgress(KtvStore.DEFAULT_MUSIC_VOLUME)
|
||||
micVolume = 60
|
||||
musicVolume = KtvStore.DEFAULT_MUSIC_VOLUME
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
tvMicVol.setText("60")
|
||||
tvMicEffect.setText("50")
|
||||
tvMusicVol.setText("80")
|
||||
updateToggleState(btnMute, false)
|
||||
})
|
||||
|
||||
// 创建并显示弹窗
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(root)
|
||||
.create()
|
||||
// 透明背景显示圆角
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow()!!.setBackgroundDrawableResource(android.R.color.transparent)
|
||||
}
|
||||
btnClose.setOnClickListener(View.OnClickListener { v: View? -> dialog.dismiss() })
|
||||
btnSave.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
saveState()
|
||||
dialog.dismiss()
|
||||
})
|
||||
dialog.restoreSourceFocusOnDismiss()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showAlignedTuningDialog(): Boolean {
|
||||
val root = getLayoutInflater().inflate(R.layout.dialog_tuning, null)
|
||||
val toneValue = root.findViewById<TextView>(R.id.text_tone_counter)
|
||||
val micValue = root.findViewById<TextView>(R.id.text_mic_counter)
|
||||
toneValue.setText(tone.toString())
|
||||
micValue.setText(micVolume.toString())
|
||||
|
||||
val dialog = AlertDialog.Builder(this).setView(root).create()
|
||||
root.findViewById<View?>(R.id.btn_tuning_close)
|
||||
.setOnClickListener(View.OnClickListener { v: View? -> dialog.dismiss() })
|
||||
root.findViewById<View?>(R.id.btn_tone_down)
|
||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
tone = max(-5, tone - 1)
|
||||
toneValue.setText(tone.toString())
|
||||
player?.setTone(tone)
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
})
|
||||
root.findViewById<View?>(R.id.btn_tone_up)
|
||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
tone = min(5, tone + 1)
|
||||
toneValue.setText(tone.toString())
|
||||
player?.setTone(tone)
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
})
|
||||
root.findViewById<View?>(R.id.btn_mic_down)
|
||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
toast("麦克风音量由功放/混音器硬件调节")
|
||||
})
|
||||
root.findViewById<View?>(R.id.btn_mic_up)
|
||||
.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
toast("麦克风音量由功放/混音器硬件调节")
|
||||
})
|
||||
bindAtmospherePreset(root, R.id.btn_preset_pop, "流行")
|
||||
bindAtmospherePreset(root, R.id.btn_preset_lyric, "抒情")
|
||||
bindAtmospherePreset(root, R.id.btn_preset_rock, "摇滚")
|
||||
bindAtmospherePreset(root, R.id.btn_preset_singer, "唱将")
|
||||
// 均衡器入口:主页顶栏「调音」/播放界面「调音」/控制层「音量」都走本弹窗。
|
||||
root.findViewById<View?>(R.id.btn_tuning_equalizer)?.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
showEqualizerDialog()
|
||||
}
|
||||
|
||||
dialog.showForTv(afterShow = {
|
||||
dialog.window?.apply {
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
attributes = attributes.apply { dimAmount = 0.72f }
|
||||
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
}
|
||||
root.findViewById<View>(R.id.btn_tone_down).requestFocus()
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
private fun bindAtmospherePreset(root: View, id: Int, preset: String) {
|
||||
root.findViewById<View?>(id).setOnClickListener(View.OnClickListener { v: View? ->
|
||||
atmosphere = preset
|
||||
saveState()
|
||||
toast("音效: " + preset)
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 均衡器 =====
|
||||
|
||||
/** 把持久化的 "1.0,-2.0,..." 解析成频段增益数组。 */
|
||||
@@ -2822,56 +2491,32 @@ class MainActivity : AppCompatActivity() {
|
||||
syncEqualizerSessions()
|
||||
saveState()
|
||||
}
|
||||
|
||||
/**
|
||||
* 均衡器弹窗 —— App 内统一入口(主页顶栏「调音」、播放界面「调音」、
|
||||
* 声音设置「均衡器入口」,以及远程点歌页面的同名入口)。
|
||||
* 「调音」弹窗 —— 播放界面与主界面右上角共用的**唯一**调音入口。
|
||||
*
|
||||
* 内核见 [KtvEqualizer]:参考开源项目 OpenEQ,直接使用系统 `audiofx.Equalizer`,
|
||||
* 并按设备真实支持的频段数与增益范围自适应,因此频段是动态生成的。
|
||||
* UI 与「调音」弹窗保持一致:深色圆角面板 + 紫色频段卡 + 橙色焦点反馈。
|
||||
* 自上而下(全部即时生效,所以没有「完成 / 确认」按钮):
|
||||
* 1. 音量:横向控制杆 —— App 内部输出音量,不改动系统音量;
|
||||
* 2. 均衡器开关(默认关闭);
|
||||
* 3. 均衡器预设:自定义(用户改动频段后自动保存)/ 流行 / 摇滚 / 舞曲;
|
||||
* 4. EQ 频段调节:竖向控制杆(频段数与增益范围取自设备真实能力)。
|
||||
*/
|
||||
private fun showEqualizerDialog() {
|
||||
private fun showTuningDialog() {
|
||||
val caps = KtvEqualizer.capabilities(this)
|
||||
val bandLabels = KtvEqualizer.bandLabels(caps)
|
||||
val working = KtvEqualizer.levelsSnapshot().let { saved ->
|
||||
FloatArray(caps.bandCount) { saved.getOrElse(it) { 0f } }
|
||||
}
|
||||
val globalSupported = KtvEqualizer.globalMixSupported()
|
||||
|
||||
val panel = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
background = getDrawable(R.drawable.bg_tuning_dialog)
|
||||
setPadding(dp(26), dp(16), dp(26), dp(18))
|
||||
setPadding(dp(26), dp(14), dp(26), dp(16))
|
||||
}
|
||||
TvFocusStyler.suppressFill(panel)
|
||||
|
||||
// ── 标题 ──
|
||||
val titleRow = FrameLayout(this)
|
||||
titleRow.addView(
|
||||
label("均衡器", 24, Color.WHITE).apply { gravity = Gravity.CENTER },
|
||||
FrameLayout.LayoutParams(-1, dp(46)),
|
||||
)
|
||||
val close = label("×", 22, Color.argb(153, 255, 255, 255)).apply {
|
||||
gravity = Gravity.CENTER
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
TvFocusStyler.installAction(this)
|
||||
}
|
||||
titleRow.addView(
|
||||
close,
|
||||
FrameLayout.LayoutParams(dp(44), dp(40), Gravity.END or Gravity.CENTER_VERTICAL),
|
||||
)
|
||||
panel.addView(titleRow, LinearLayout.LayoutParams(-1, dp(46)))
|
||||
|
||||
// ── 状态行:总开关 / 全局混音 / 重置 ──
|
||||
val switchRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
fun pill(text: String, action: () -> Unit): TextView = TextView(this).apply {
|
||||
this.text = text
|
||||
textSize = 16f
|
||||
textSize = 15f
|
||||
setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER
|
||||
background = getDrawable(R.drawable.bg_btn_glass)
|
||||
@@ -2881,66 +2526,79 @@ class MainActivity : AppCompatActivity() {
|
||||
setOnClickListener { action() }
|
||||
TvFocusStyler.installAction(this)
|
||||
}
|
||||
lateinit var enabledPill: TextView
|
||||
lateinit var globalPill: TextView
|
||||
fun renderPills() {
|
||||
enabledPill.text = if (KtvEqualizer.enabled) "均衡器:开启" else "均衡器:关闭"
|
||||
enabledPill.setTextColor(if (KtvEqualizer.enabled) Color.rgb(39, 204, 164) else Color.WHITE)
|
||||
globalPill.text = if (!globalSupported) {
|
||||
"全局混音:不支持"
|
||||
} else if (KtvEqualizer.globalMix) {
|
||||
"全局混音:开"
|
||||
} else {
|
||||
"全局混音:关"
|
||||
|
||||
// ── 标题 ──
|
||||
val titleRow = FrameLayout(this)
|
||||
titleRow.addView(
|
||||
label("调音", 23, Color.WHITE).apply { gravity = Gravity.CENTER },
|
||||
FrameLayout.LayoutParams(-1, dp(42)),
|
||||
)
|
||||
val close = label("×", 22, Color.argb(153, 255, 255, 255)).apply {
|
||||
gravity = Gravity.CENTER
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
TvFocusStyler.installAction(this)
|
||||
}
|
||||
globalPill.alpha = if (globalSupported) 1f else 0.45f
|
||||
titleRow.addView(
|
||||
close,
|
||||
FrameLayout.LayoutParams(dp(44), dp(38), Gravity.END or Gravity.CENTER_VERTICAL),
|
||||
)
|
||||
panel.addView(titleRow, LinearLayout.LayoutParams(-1, dp(42)))
|
||||
|
||||
// ── 1. 音量:横向控制杆(即时生效)──
|
||||
val volumeTitle = label("音量 $musicVolume%", 16, Color.WHITE)
|
||||
panel.addView(volumeTitle, LinearLayout.LayoutParams(-1, dp(28)))
|
||||
var lastVolumeAppliedAt = 0L
|
||||
val volumeBar = SeekBar(this).apply {
|
||||
max = 100
|
||||
progress = musicVolume
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
setPadding(0, 0, 0, 0)
|
||||
TvFocusStyler.installAction(this)
|
||||
setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(bar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
musicVolume = progress.coerceIn(0, 100)
|
||||
volumeTitle.text = "音量 $musicVolume%"
|
||||
// 拖动时按 100ms 节流即时生效,避免每个像素都重建音轨。
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastVolumeAppliedAt >= 100L) {
|
||||
lastVolumeAppliedAt = now
|
||||
applyPlaybackMode()
|
||||
}
|
||||
enabledPill = pill(if (KtvEqualizer.enabled) "均衡器:开启" else "均衡器:关闭") {
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(bar: SeekBar?) = Unit
|
||||
|
||||
override fun onStopTrackingTouch(bar: SeekBar?) {
|
||||
applyPlaybackMode()
|
||||
saveState()
|
||||
}
|
||||
})
|
||||
}
|
||||
panel.addView(volumeBar, LinearLayout.LayoutParams(-1, dp(44)))
|
||||
|
||||
// ── 2. 均衡器开关(默认关闭)──
|
||||
lateinit var eqSwitch: TextView
|
||||
lateinit var eqArea: LinearLayout
|
||||
fun renderEqEnabled() {
|
||||
eqSwitch.text = if (KtvEqualizer.enabled) "均衡器:开启" else "均衡器:关闭"
|
||||
eqSwitch.setTextColor(if (KtvEqualizer.enabled) Color.rgb(39, 204, 164) else Color.WHITE)
|
||||
// 关闭时预设/频段整体降透明度,直观表达"当前不生效"。
|
||||
eqArea.alpha = if (KtvEqualizer.enabled) 1f else 0.45f
|
||||
}
|
||||
eqSwitch = pill(if (KtvEqualizer.enabled) "均衡器:开启" else "均衡器:关闭") {
|
||||
KtvEqualizer.setEnabled(!KtvEqualizer.enabled)
|
||||
renderPills()
|
||||
renderEqEnabled()
|
||||
applyEqualizerSettings()
|
||||
toast(if (KtvEqualizer.enabled) "均衡器已开启" else "均衡器已关闭")
|
||||
}
|
||||
if (KtvEqualizer.enabled) enabledPill.setTextColor(Color.rgb(39, 204, 164))
|
||||
globalPill = pill("全局混音:关") {
|
||||
if (!globalSupported) {
|
||||
toast("本机不支持全局混音")
|
||||
return@pill
|
||||
}
|
||||
KtvEqualizer.setGlobalMix(!KtvEqualizer.globalMix)
|
||||
renderPills()
|
||||
applyEqualizerSettings()
|
||||
toast(if (KtvEqualizer.globalMix) "均衡器已作用于全局混音" else "均衡器只作用于本 App")
|
||||
}
|
||||
// 「重置」要回到『标准』预设并刷新滑杆,但滑杆此时还没建好,用 lateinit 延后绑定。
|
||||
lateinit var resetEqualizer: () -> Unit
|
||||
val resetPill = pill("重置") { resetEqualizer() }
|
||||
renderPills()
|
||||
switchRow.addView(enabledPill, LinearLayout.LayoutParams(-2, dp(40)).apply { marginEnd = dp(8) })
|
||||
switchRow.addView(globalPill, LinearLayout.LayoutParams(-2, dp(40)).apply { marginEnd = dp(8) })
|
||||
switchRow.addView(resetPill, LinearLayout.LayoutParams(-2, dp(40)))
|
||||
panel.addView(switchRow, LinearLayout.LayoutParams(-1, dp(48)))
|
||||
panel.addView(eqSwitch, LinearLayout.LayoutParams(dp(190), dp(42)).apply { topMargin = dp(8) })
|
||||
|
||||
// ── 设备能力提示 ──
|
||||
val hint = label(
|
||||
"本机支持 ${caps.bandCount} 段 · ${String.format(Locale.ROOT, "%.0f", caps.minDb)} ~ " +
|
||||
"${String.format(Locale.ROOT, "%.0f", caps.maxDb)} dB" +
|
||||
if (globalSupported) "" else " · 本机不支持全局混音",
|
||||
13,
|
||||
Color.argb(150, 255, 255, 255),
|
||||
)
|
||||
panel.addView(hint, LinearLayout.LayoutParams(-1, dp(26)))
|
||||
eqArea = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
panel.addView(eqArea, LinearLayout.LayoutParams(-1, -2))
|
||||
|
||||
// ── 频段滑杆 ──
|
||||
val bars = ArrayList<SeekBar>(caps.bandCount)
|
||||
val valueViews = ArrayList<TextView>(caps.bandCount)
|
||||
// 刻度换算:0 dB 必定落在某个刻度上(见 eqSliderSteps 说明)。
|
||||
val stepDb = (caps.maxDb - caps.minDb) / eqSliderSteps
|
||||
fun toProgress(level: Float): Int =
|
||||
Math.round((level - caps.minDb) / stepDb).coerceIn(0, eqSliderSteps)
|
||||
fun fromProgress(progress: Int): Float =
|
||||
(caps.minDb + progress * stepDb).coerceIn(caps.minDb, caps.maxDb)
|
||||
// 预设按钮与高亮渲染先声明(频段滑杆回调里要用来刷新"自定义"高亮)。
|
||||
// ── 3. 均衡器预设 ──
|
||||
val presetButtons = LinkedHashMap<String, TextView>()
|
||||
fun renderPresets() {
|
||||
presetButtons.forEach { (name, view) ->
|
||||
@@ -2949,83 +2607,73 @@ class MainActivity : AppCompatActivity() {
|
||||
view.alpha = if (active) 1f else 0.82f
|
||||
}
|
||||
}
|
||||
/** 把内核当前频段值回灌到滑杆与数值文本。 */
|
||||
fun syncBarsFromEqualizer() {
|
||||
val presetRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
eqArea.addView(presetRow, LinearLayout.LayoutParams(-1, dp(52)).apply { topMargin = dp(10) })
|
||||
|
||||
// ── 4. EQ 频段:竖向控制杆 ──
|
||||
val bars = ArrayList<VerticalEqBar>(caps.bandCount)
|
||||
val valueViews = ArrayList<TextView>(caps.bandCount)
|
||||
fun syncBars() {
|
||||
val applied = KtvEqualizer.levelsSnapshot()
|
||||
for (index in working.indices) {
|
||||
working[index] = applied.getOrElse(index) { 0f }
|
||||
bars[index].progress = toProgress(working[index])
|
||||
valueViews[index].text = eqLevelText(working[index])
|
||||
valueViews[index].setTextColor(
|
||||
if (working[index] == 0f) Color.argb(170, 255, 255, 255)
|
||||
else Color.rgb(255, 204, 92),
|
||||
)
|
||||
bars.getOrNull(index)?.setLevel(working[index])
|
||||
valueViews.getOrNull(index)?.text = eqLevelText(working[index])
|
||||
}
|
||||
}
|
||||
fun applyBandLevels() {
|
||||
KtvEqualizer.setLevels(working.copyOf())
|
||||
KtvEqualizer.markCustomPreset()
|
||||
syncBarsFromEqualizer()
|
||||
applyEqualizerSettings()
|
||||
}
|
||||
|
||||
caps.frequencies.forEachIndexed { index, _ ->
|
||||
val row = LinearLayout(this).apply {
|
||||
val barRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
row.addView(
|
||||
label(bandLabels.getOrElse(index) { "${index + 1}" } + "Hz", 15, Color.WHITE).apply {
|
||||
gravity = Gravity.CENTER
|
||||
},
|
||||
LinearLayout.LayoutParams(dp(74), dp(38)),
|
||||
)
|
||||
val seek = SeekBar(this).apply {
|
||||
max = eqSliderSteps
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
setPadding(0, 0, 0, 0)
|
||||
TvFocusStyler.installAction(this)
|
||||
setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(bar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (!fromUser) return
|
||||
working[index] = fromProgress(progress)
|
||||
valueViews[index].text = eqLevelText(working[index])
|
||||
valueViews[index].setTextColor(
|
||||
if (working[index] == 0f) Color.argb(170, 255, 255, 255)
|
||||
else Color.rgb(255, 204, 92)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(bar: SeekBar?) = Unit
|
||||
|
||||
override fun onStopTrackingTouch(bar: SeekBar?) {
|
||||
eqArea.addView(barRow, LinearLayout.LayoutParams(-1, dp(216)).apply { topMargin = dp(6) })
|
||||
caps.frequencies.forEachIndexed { index, _ ->
|
||||
val column = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
}
|
||||
val value = label(
|
||||
eqLevelText(working.getOrElse(index) { 0f }),
|
||||
12,
|
||||
Color.argb(170, 255, 255, 255),
|
||||
).apply { gravity = Gravity.CENTER }
|
||||
valueViews.add(value)
|
||||
column.addView(value, LinearLayout.LayoutParams(-1, dp(20)))
|
||||
val bar = VerticalEqBar(this).apply {
|
||||
setRange(caps.minDb, caps.maxDb, eqSliderSteps)
|
||||
setLevel(working.getOrElse(index) { 0f })
|
||||
onLevelChanged = { level, fromUser ->
|
||||
if (fromUser) {
|
||||
working[index] = level
|
||||
value.text = eqLevelText(level)
|
||||
value.setTextColor(
|
||||
if (level == 0f) Color.argb(170, 255, 255, 255) else Color.rgb(255, 204, 92),
|
||||
)
|
||||
// 用户手动改动 → 预设转为「自定义」并自动保存。
|
||||
KtvEqualizer.setLevels(working.copyOf())
|
||||
KtvEqualizer.markCustomPreset()
|
||||
renderPresets()
|
||||
applyEqualizerSettings()
|
||||
}
|
||||
})
|
||||
}
|
||||
bars.add(seek)
|
||||
row.addView(seek, LinearLayout.LayoutParams(0, dp(38), 1f))
|
||||
val value = label("0.0", 15, Color.argb(170, 255, 255, 255)).apply {
|
||||
}
|
||||
bars.add(bar)
|
||||
column.addView(bar, LinearLayout.LayoutParams(dp(46), 0, 1f))
|
||||
column.addView(
|
||||
label(bandLabels.getOrElse(index) { "${index + 1}" } + "Hz", 12, Color.WHITE).apply {
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
valueViews.add(value)
|
||||
row.addView(value, LinearLayout.LayoutParams(dp(76), dp(38)))
|
||||
panel.addView(row, LinearLayout.LayoutParams(-1, dp(42)))
|
||||
},
|
||||
LinearLayout.LayoutParams(-1, dp(20)),
|
||||
)
|
||||
barRow.addView(column, LinearLayout.LayoutParams(0, -1, 1f))
|
||||
}
|
||||
|
||||
// ── 预设 ──
|
||||
val presetRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
KtvEqualizer.presetNames().forEach { name ->
|
||||
KtvEqualizer.appPresetNames().forEach { name ->
|
||||
val presetButton = TextView(this).apply {
|
||||
text = name
|
||||
textSize = 16f
|
||||
textSize = 15f
|
||||
setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER
|
||||
isClickable = true
|
||||
@@ -3033,70 +2681,45 @@ class MainActivity : AppCompatActivity() {
|
||||
background = getDrawable(R.drawable.bg_tuning_counter)
|
||||
TvFocusStyler.installAction(this)
|
||||
setOnClickListener {
|
||||
if (name == KtvEqualizer.CUSTOM_PRESET) {
|
||||
// 保留当前频段值,仅把预设标记为「自定义」。
|
||||
KtvEqualizer.markCustomPreset()
|
||||
} else {
|
||||
KtvEqualizer.applyPreset(name)
|
||||
syncBarsFromEqualizer()
|
||||
syncBars()
|
||||
}
|
||||
renderPresets()
|
||||
applyEqualizerSettings()
|
||||
toast("均衡器预设:" + name)
|
||||
}
|
||||
}
|
||||
presetButtons[name] = presetButton
|
||||
presetRow.addView(
|
||||
presetButton,
|
||||
LinearLayout.LayoutParams(0, dp(56), 1f).apply { marginStart = dp(3); marginEnd = dp(3) },
|
||||
LinearLayout.LayoutParams(0, dp(46), 1f).apply {
|
||||
marginStart = dp(4)
|
||||
marginEnd = dp(4)
|
||||
},
|
||||
)
|
||||
}
|
||||
panel.addView(presetRow, LinearLayout.LayoutParams(-1, dp(64)))
|
||||
renderEqEnabled()
|
||||
renderPresets()
|
||||
resetEqualizer = {
|
||||
KtvEqualizer.applyPreset("标准")
|
||||
syncBarsFromEqualizer()
|
||||
renderPresets()
|
||||
applyEqualizerSettings()
|
||||
toast("均衡器已重置为标准")
|
||||
}
|
||||
|
||||
// ── 底部 ──
|
||||
val actions = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
val done = pill("完成") { }
|
||||
actions.addView(done, LinearLayout.LayoutParams(dp(110), dp(44)))
|
||||
panel.addView(actions, LinearLayout.LayoutParams(-1, dp(52)))
|
||||
|
||||
val scroll = android.widget.ScrollView(this).apply { addView(panel) }
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(scroll)
|
||||
.create()
|
||||
val dialog = AlertDialog.Builder(this).setView(scroll).create()
|
||||
close.setOnClickListener { dialog.dismiss() }
|
||||
done.setOnClickListener { dialog.dismiss() }
|
||||
dialog.restoreSourceFocusOnDismiss()
|
||||
dialog.showForTv(afterShow = {
|
||||
dialog.window?.apply {
|
||||
setBackgroundDrawableResource(android.R.color.transparent)
|
||||
attributes = attributes.apply { dimAmount = 0.72f }
|
||||
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
val available = (resources.displayMetrics.widthPixels * 0.92f).toInt()
|
||||
setLayout(min(dp(700), available), ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val available = (resources.displayMetrics.widthPixels * 0.94f).toInt()
|
||||
setLayout(min(dp(760), available), ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
// 初始化滑杆位置(不触发 fromUser 回调,避免被当成自定义)。
|
||||
bars.forEachIndexed { index, bar ->
|
||||
bar.progress = toProgress(working.getOrElse(index) { 0f })
|
||||
valueViews[index].text = eqLevelText(working.getOrElse(index) { 0f })
|
||||
}
|
||||
enabledPill.requestFocus()
|
||||
volumeBar.requestFocus()
|
||||
})
|
||||
}
|
||||
|
||||
/** 4 个切换按钮的状态切换 */
|
||||
private var muteBeforeVol = 80
|
||||
private fun updateToggleState(btn: TextView?, on: Boolean) {
|
||||
if (btn == null) return
|
||||
btn.setBackgroundResource(if (on) R.drawable.bg_btn_toggle_on else R.drawable.bg_btn_toggle_off)
|
||||
btn.setTextColor(if (on) Color.WHITE else Color.rgb(204, 204, 204))
|
||||
}
|
||||
|
||||
private fun controlRow(
|
||||
left: String?,
|
||||
right: String?,
|
||||
@@ -5656,7 +5279,14 @@ class MainActivity : AppCompatActivity() {
|
||||
playRandomLocalSong()
|
||||
return
|
||||
}
|
||||
// 用户主动切歌/停止:取消挂起的随机续播,进入空闲态。
|
||||
// 公放(随机)歌曲播放中按「切歌」:不该停下来,接着播公放清单里的下一首。
|
||||
// 注意这里不调用 markFinished —— 用户是跳过而不是播完,不该 +1;
|
||||
// 但它已在 markStarted 时计入本轮已播,所以本轮不会再重复。
|
||||
if (!fromCompletion && currentSong?.let { RandomPlayQueue.isCurrentTrack(it) } == true) {
|
||||
playRandomLocalSong(skipped = true)
|
||||
return
|
||||
}
|
||||
// 其它情况(没有公放曲目在播 / 用户主动停止):取消挂起的随机续播,进入空闲态。
|
||||
randomLocalRequestSerial++
|
||||
playbackPreparing = false
|
||||
playWhenPrepared = false
|
||||
@@ -5702,10 +5332,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本机所有有效歌曲中随机续播(启动自动播放 / 点歌列表播放完毕均走此逻辑)。
|
||||
* 从本机所有有效歌曲中随机续播(启动自动播放 / 点歌列表播放完毕 / 公放中切歌均走此逻辑)。
|
||||
* 随机歌曲不写入已点列表;用户此时点歌会立即取消本次随机请求并优先播放已点歌曲。
|
||||
*
|
||||
* @param startup 是否开机自动随机播放
|
||||
* @param skipped 是否是用户在公放歌曲上按了「切歌」(仅影响提示文案)
|
||||
*/
|
||||
private fun playRandomLocalSong(startup: Boolean = false) {
|
||||
private fun playRandomLocalSong(startup: Boolean = false, skipped: Boolean = false) {
|
||||
val previousSongId = currentSong?.let(::stableId) ?: lastRandomLocalSongId
|
||||
val requestSerial = ++randomLocalRequestSerial
|
||||
playbackPreparing = true
|
||||
@@ -5714,7 +5347,11 @@ class MainActivity : AppCompatActivity() {
|
||||
currentSong = null
|
||||
clearLyrics()
|
||||
updateBottomBar(null, false)
|
||||
status?.text = if (startup) "正在随机播放本地歌曲..." else "已点播放完成,正在随机本地歌曲..."
|
||||
status?.text = when {
|
||||
startup -> "正在随机播放本地歌曲..."
|
||||
skipped -> "切歌:正在播放下一首公放歌曲..."
|
||||
else -> "已点播放完成,正在随机本地歌曲..."
|
||||
}
|
||||
persistRuntimeState()
|
||||
|
||||
io.execute {
|
||||
@@ -6809,13 +6446,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private var fullScreenSeeking = false
|
||||
private var fullScreenControls: LinearLayout? = null
|
||||
private var fullScreenProgressRow: View? = null
|
||||
private var fullScreenToneControl: View? = null
|
||||
private var fullScreenVolumePanel: View? = null
|
||||
private var fullScreenVolumeIndicator: CircularVolumeView? = null
|
||||
private var fullScreenVolumeMinusButton: TextView? = null
|
||||
private var fullScreenVolumePlusButton: TextView? = null
|
||||
private var fullScreenVolumeMuteButton: TextView? = null
|
||||
private var fullScreenPreviousVolume = 70
|
||||
private var randomLocalRequestSerial = 0L
|
||||
private var lastRandomLocalSongId: String? = null
|
||||
/** 启动时无恢复播放/空队列: 曲库就绪后自动随机播放本地已下载歌曲(仅触发一次)。 */
|
||||
@@ -6823,14 +6453,8 @@ class MainActivity : AppCompatActivity() {
|
||||
private var advancingPlayback = false
|
||||
private var lastAutoAdvanceAtMs = 0L
|
||||
private var fullScreenChromeVisible = false
|
||||
private val hideFullScreenVolumeIndicatorRunnable = Runnable {
|
||||
fullScreenVolumeIndicator?.animate()?.alpha(0f)?.setDuration(220L)?.withEndAction {
|
||||
fullScreenVolumeIndicator?.visibility = View.GONE
|
||||
fullScreenVolumeIndicator?.alpha = 1f
|
||||
}?.start()
|
||||
}
|
||||
private val hideFullScreenChromeRunnable = Runnable {
|
||||
if (isFullScreen && fullScreenVolumePanel?.visibility != View.VISIBLE) {
|
||||
if (isFullScreen) {
|
||||
setFullScreenChromeVisible(false)
|
||||
}
|
||||
}
|
||||
@@ -7052,7 +6676,7 @@ class MainActivity : AppCompatActivity() {
|
||||
if (btnTopNext != null) btnTopNext!!.setOnClickListener(View.OnClickListener { v: View? -> playNext() })
|
||||
if (btnTopPause != null) btnTopPause!!.setOnClickListener(View.OnClickListener { v: View? -> togglePlay() })
|
||||
if (btnTopReplay != null) btnTopReplay!!.setOnClickListener(View.OnClickListener { v: View? -> replay() })
|
||||
if (btnTopTone != null) btnTopTone!!.setOnClickListener(View.OnClickListener { v: View? -> showAudioControlDialog() })
|
||||
if (btnTopTone != null) btnTopTone!!.setOnClickListener(View.OnClickListener { v: View? -> showTuningDialog() })
|
||||
|
||||
// 设置底栏按钮事件
|
||||
if (btnBottomGuess != null) btnBottomGuess!!.setOnClickListener(View.OnClickListener { v: View? -> showGuessPage() })
|
||||
@@ -9273,7 +8897,7 @@ class MainActivity : AppCompatActivity() {
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
setPadding(dp(12), dp(10), dp(12), dp(10))
|
||||
setOnClickListener { showEqualizerDialog() }
|
||||
setOnClickListener { showTuningDialog() }
|
||||
TvFocusStyler.installAction(this)
|
||||
}
|
||||
val body = LinearLayout(this).apply {
|
||||
@@ -10299,27 +9923,11 @@ class MainActivity : AppCompatActivity() {
|
||||
code == KeyEvent.KEYCODE_NUMPAD_ENTER
|
||||
val direction = code == KeyEvent.KEYCODE_DPAD_LEFT || code == KeyEvent.KEYCODE_DPAD_RIGHT ||
|
||||
code == KeyEvent.KEYCODE_DPAD_UP || code == KeyEvent.KEYCODE_DPAD_DOWN
|
||||
val volumePanelVisible = fullScreenVolumePanel?.visibility == View.VISIBLE
|
||||
val volumePanelHasFocus = volumePanelVisible && fullScreenVolumePanel?.hasFocus() == true
|
||||
if (code == KeyEvent.KEYCODE_BACK && volumePanelVisible) {
|
||||
main.removeCallbacks(hideFullScreenVolumeIndicatorRunnable)
|
||||
fullScreenVolumePanel?.visibility = View.GONE
|
||||
fullScreenToneControl?.requestFocus()
|
||||
setFullScreenChromeVisible(true)
|
||||
return true
|
||||
}
|
||||
if (center && !fullScreenChromeVisible) {
|
||||
setFullScreenChromeVisible(true)
|
||||
return true
|
||||
}
|
||||
if (volumePanelVisible && (center || direction) && !volumePanelHasFocus &&
|
||||
fullScreenControls?.hasFocus() != true
|
||||
) {
|
||||
setFullScreenChromeVisible(true)
|
||||
fullScreenVolumeMinusButton?.requestFocus()
|
||||
return true
|
||||
}
|
||||
if (center && fullScreenControls?.hasFocus() != true && !volumePanelHasFocus) {
|
||||
if (center && fullScreenControls?.hasFocus() != true) {
|
||||
setFullScreenChromeVisible(true)
|
||||
fullScreenControls?.getChildAt(0)?.requestFocus()
|
||||
return true
|
||||
@@ -10332,19 +9940,6 @@ class MainActivity : AppCompatActivity() {
|
||||
main.removeCallbacks(hideFullScreenChromeRunnable)
|
||||
main.postDelayed(hideFullScreenChromeRunnable, 5000L)
|
||||
}
|
||||
if (volumePanelHasFocus && code == KeyEvent.KEYCODE_DPAD_UP) {
|
||||
fullScreenToneControl?.requestFocus()
|
||||
return true
|
||||
}
|
||||
if (volumePanelHasFocus && code == KeyEvent.KEYCODE_DPAD_DOWN) {
|
||||
return true
|
||||
}
|
||||
if (volumePanelVisible && window.decorView.findFocus() === fullScreenToneControl &&
|
||||
code == KeyEvent.KEYCODE_DPAD_DOWN
|
||||
) {
|
||||
fullScreenVolumeMinusButton?.requestFocus()
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (code == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || code == KeyEvent.KEYCODE_SPACE) {
|
||||
togglePlay()
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.local.ktv
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* 均衡器竖向控制杆。
|
||||
*
|
||||
* Android 自带的 `SeekBar` 只支持横向,所以这里自绘一个竖向滑杆:
|
||||
* - 触摸点击/拖动定位;
|
||||
* - 遥控器上/下键步进(按住可连续微调);
|
||||
* - 获得焦点时描一圈橙色边框(与 App 的 TV 焦点风格一致)。
|
||||
*
|
||||
* 数值单位是 dB,范围由 [setRange] 给出(取自设备真实支持的增益范围)。
|
||||
* 绘制中包含 0 dB 基准线,并从基准线填充到当前值,便于一眼看出提升/衰减。
|
||||
*/
|
||||
class VerticalEqBar(context: Context) : View(context) {
|
||||
|
||||
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.argb(60, 255, 255, 255)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(255, 184, 74)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val zeroPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.argb(110, 255, 255, 255)
|
||||
strokeWidth = 1.5f
|
||||
}
|
||||
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val focusPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.rgb(255, 152, 48)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 3f
|
||||
}
|
||||
|
||||
private val density = resources.displayMetrics.density
|
||||
private var minDb = -15f
|
||||
private var maxDb = 15f
|
||||
private var steps = 30
|
||||
private var level = 0f
|
||||
|
||||
/** 值变化回调(含 fromUser 标记,用于区分初始化与用户操作)。 */
|
||||
var onLevelChanged: ((level: Float, fromUser: Boolean) -> Unit)? = null
|
||||
|
||||
init {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
isClickable = true
|
||||
// 自绘控件:禁止 TvFocusStyler 用不透明渐变覆盖背景(自己画焦点框)。
|
||||
TvFocusStyler.suppressFill(this)
|
||||
}
|
||||
|
||||
fun setRange(minDb: Float, maxDb: Float, steps: Int) {
|
||||
this.minDb = minDb
|
||||
this.maxDb = maxDb
|
||||
this.steps = steps.coerceAtLeast(1)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun level(): Float = level
|
||||
|
||||
/** 设置当前值;notify=false 用于初始化(不触发回调)。 */
|
||||
fun setLevel(value: Float, notify: Boolean = false) {
|
||||
val clamped = value.coerceIn(minDb, maxDb)
|
||||
if (clamped == level) {
|
||||
invalidate()
|
||||
return
|
||||
}
|
||||
level = clamped
|
||||
invalidate()
|
||||
if (notify) onLevelChanged?.invoke(level, true)
|
||||
}
|
||||
|
||||
/** 按刻度步进取值,保证 0 dB 一定落在某个刻度上。 */
|
||||
private fun stepDb(): Float = (maxDb - minDb) / steps
|
||||
|
||||
private fun progressToLevel(progress: Int): Float =
|
||||
(minDb + progress * stepDb()).coerceIn(minDb, maxDb)
|
||||
|
||||
private fun levelToProgress(value: Float): Int =
|
||||
Math.round((value - minDb) / stepDb()).coerceIn(0, steps)
|
||||
|
||||
private fun levelFromY(y: Float): Float {
|
||||
val usable = height - paddingTop - paddingBottom
|
||||
if (usable <= 0) return level
|
||||
val ratio = 1f - ((y - paddingTop) / usable).coerceIn(0f, 1f)
|
||||
return minDb + ratio * (maxDb - minDb)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val trackWidth = 10f * density
|
||||
val centerX = width / 2f
|
||||
val top = paddingTop + 6f * density
|
||||
val bottom = height - paddingBottom - 6f * density
|
||||
if (bottom <= top) return
|
||||
val radius = trackWidth / 2f
|
||||
|
||||
canvas.drawRoundRect(
|
||||
RectF(centerX - radius, top, centerX + radius, bottom),
|
||||
radius,
|
||||
radius,
|
||||
trackPaint,
|
||||
)
|
||||
|
||||
val zeroRatio = ((0f - minDb) / (maxDb - minDb)).coerceIn(0f, 1f)
|
||||
val zeroY = bottom - zeroRatio * (bottom - top)
|
||||
canvas.drawLine(centerX - radius * 1.8f, zeroY, centerX + radius * 1.8f, zeroY, zeroPaint)
|
||||
|
||||
val valueRatio = ((level - minDb) / (maxDb - minDb)).coerceIn(0f, 1f)
|
||||
val valueY = bottom - valueRatio * (bottom - top)
|
||||
val fillTop = minOf(zeroY, valueY)
|
||||
val fillBottom = maxOf(zeroY, valueY)
|
||||
if (fillBottom - fillTop > 1f) {
|
||||
canvas.drawRoundRect(
|
||||
RectF(centerX - radius, fillTop, centerX + radius, fillBottom),
|
||||
radius,
|
||||
radius,
|
||||
fillPaint,
|
||||
)
|
||||
}
|
||||
|
||||
canvas.drawCircle(centerX, valueY, trackWidth * 0.85f, thumbPaint)
|
||||
|
||||
if (isFocused) {
|
||||
val inset = 1.5f * density
|
||||
canvas.drawRoundRect(
|
||||
RectF(inset, inset, width - inset, height - inset),
|
||||
8f * density,
|
||||
8f * density,
|
||||
focusPaint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
if (!isFocused) requestFocus()
|
||||
val target = levelFromY(event.y)
|
||||
val snapped = progressToLevel(levelToProgress(target))
|
||||
if (snapped != level) {
|
||||
level = snapped
|
||||
invalidate()
|
||||
onLevelChanged?.invoke(level, true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
performClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val progress = levelToProgress(level)
|
||||
val delta = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_UP -> 1
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> -1
|
||||
else -> return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
val next = (progress + delta).coerceIn(0, steps)
|
||||
if (next == progress) {
|
||||
// 已到量程端点:放行按键,让焦点能离开滑杆(否则遥控器会困在这里出不去)。
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
level = progressToLevel(next)
|
||||
invalidate()
|
||||
onLevelChanged?.invoke(level, true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
调音/音频控制弹窗(对应"弹窗-调音.jpg")
|
||||
包含:
|
||||
- 4 个开关按钮:立体声/伴唱/原唱/静音
|
||||
- 2 个滑块:麦克风(音量+效果)、音乐音量
|
||||
- 关闭按钮
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="640dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_dialog_panel"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- 标题 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="36dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="14dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="🎚 调音设置"
|
||||
android:textColor="#FF27CCA4"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_audio_close"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/bg_btn_dialog_close"
|
||||
android:gravity="center"
|
||||
android:text="×"
|
||||
android:textColor="#FFCCCCCC"
|
||||
android:textSize="22sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 4 个开关按钮 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:weightSum="4">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_tone_stereo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_btn_toggle_on"
|
||||
android:gravity="center"
|
||||
android:text="🔊 立体声"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_tone_accomp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_btn_toggle_off"
|
||||
android:gravity="center"
|
||||
android:text="🎤 伴唱"
|
||||
android:textColor="#FFCCCCCC"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_tone_origin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_btn_toggle_on"
|
||||
android:gravity="center"
|
||||
android:text="🎙 原唱"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_tone_mute"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_btn_toggle_off"
|
||||
android:gravity="center"
|
||||
android:text="🔇 静音"
|
||||
android:textColor="#FFCCCCCC"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 麦克风(增益/效果由功放混音器硬件调节, 滑杆仅作指示) -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:text="麦克风音量(功放/混音器硬件调节)"
|
||||
android:textColor="#FFE0E0FF"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="14dp">
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_mic_volume"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="60"
|
||||
android:progressTint="#FF27CCA4"
|
||||
android:thumbTint="#FF27CCA4"
|
||||
android:enabled="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_mic_volume_value"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:text="60"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:text="麦克风效果(功放/混音器硬件调节)"
|
||||
android:textColor="#FFE0E0FF"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_mic_effect"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="50"
|
||||
android:progressTint="#FF27CCA4"
|
||||
android:thumbTint="#FF27CCA4"
|
||||
android:enabled="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_mic_effect_value"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:text="50"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 音乐音量 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="音乐音量"
|
||||
android:textColor="#FFE0E0FF"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="20dp">
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_music_volume"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:max="100"
|
||||
android:progress="80"
|
||||
android:progressTint="#FF27CCA4"
|
||||
android:thumbTint="#FF27CCA4" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_music_volume_value"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:text="80"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 底部按钮行 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_audio_default"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/bg_btn_glass"
|
||||
android:gravity="center"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="恢复默认"
|
||||
android:textColor="#FFCCCCCC"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_audio_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/bg_btn_save"
|
||||
android:gravity="center"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"
|
||||
android:text="保存"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,116 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="470dp"
|
||||
android:layout_height="404dp"
|
||||
android:background="@drawable/bg_tuning_dialog"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingEnd="28dp"
|
||||
android:paddingBottom="22dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="调音"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="24sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_tuning_close"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:gravity="center"
|
||||
android:text="×"
|
||||
android:textColor="#99FFFFFF"
|
||||
android:textSize="22sp" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="116dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/bg_tuning_counter"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:text="升降调" android:textColor="#BFFFFFFF" android:textSize="17sp" />
|
||||
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="54dp"
|
||||
android:gravity="center" android:orientation="horizontal">
|
||||
<TextView android:id="@+id/btn_tone_down" android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="−" android:textColor="#BFFFFFFF" android:textSize="34sp" />
|
||||
<TextView android:id="@+id/text_tone_counter" android:layout_width="56dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="0" android:textColor="#BFFFFFFF" android:textSize="20sp" />
|
||||
<TextView android:id="@+id/btn_tone_up" android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="+" android:textColor="#BFFFFFFF" android:textSize="30sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_tuning_counter"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:text="麦克风" android:textColor="#FFFFFFFF" android:textSize="17sp" />
|
||||
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="54dp"
|
||||
android:gravity="center" android:orientation="horizontal">
|
||||
<TextView android:id="@+id/btn_mic_down" android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="−" android:textColor="#FFFFFFFF" android:textSize="34sp" />
|
||||
<TextView android:id="@+id/text_mic_counter" android:layout_width="56dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="50" android:textColor="#FFFFFFFF" android:textSize="20sp" />
|
||||
<TextView android:id="@+id/btn_mic_up" android:layout_width="48dp" android:layout_height="48dp"
|
||||
android:gravity="center" android:text="+" android:textColor="#FFFFFFFF" android:textSize="30sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="4">
|
||||
|
||||
<TextView android:id="@+id/btn_preset_pop" android:layout_width="0dp" android:layout_height="92dp"
|
||||
android:layout_weight="1" android:gravity="center" android:text="♬\n流行" android:textColor="#FFFFFFFF" android:textSize="16sp" />
|
||||
<TextView android:id="@+id/btn_preset_lyric" android:layout_width="0dp" android:layout_height="92dp"
|
||||
android:layout_weight="1" android:gravity="center" android:text="▥\n抒情" android:textColor="#FFFFFFFF" android:textSize="16sp" />
|
||||
<TextView android:id="@+id/btn_preset_rock" android:layout_width="0dp" android:layout_height="92dp"
|
||||
android:layout_weight="1" android:gravity="center" android:text="♨\n摇滚" android:textColor="#FFFFFFFF" android:textSize="16sp" />
|
||||
<TextView android:id="@+id/btn_preset_singer" android:layout_width="0dp" android:layout_height="92dp"
|
||||
android:layout_weight="1" android:gravity="center" android:text="♪\n唱将" android:textColor="#FFFFFFFF" android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 均衡器入口(内核参考开源项目 OpenEQ, 按设备真实频段自适应) -->
|
||||
<TextView
|
||||
android:id="@+id/btn_tuning_equalizer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_btn_glass"
|
||||
android:gravity="center"
|
||||
android:text="均衡器"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="17sp" />
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user