1) 纯色遮罩层: TvFocusStyler.install() 会把背景整体换成不透明的红→橙渐变作为
焦点/按下反馈, 而 prepareTvFocusableTree(window.decorView) 会遍历整棵树施加。
按钮/Tab/卡片需要该反馈, 但铺满屏幕或覆盖播放画面的「表面型」View 一旦被套上,
触摸一次(state_pressed)就会用纯色盖住整屏内容。
新增 TvFocusStyler.suppressFill(view, focusable) 显式排除:
- 播放触摸层 playerTouchOverlay(浮在播放画面上方, 主要元凶; 传 focusable=false
保持其 isFocusable=false, 不再抢遥控器焦点)
- 全屏播放容器 R.id.container、启动加载遮罩 databaseLoadingOverlay
- 手机点歌二维码浮层(首页右下 + 全屏右上)、全屏音量面板
- 原 preserveBackground 更名为 suppressFill(播放画面宿主/海报沿用)
同时 install() 对已标记的 View 直接跳过, 不再替调用方强制可聚焦。
2) App 音量默认 85%: 新增 KtvStore.DEFAULT_MUSIC_VOLUME 作为唯一来源,
KtvStore 字段默认值与 optInt 兜底、MainActivity.musicVolume 初值、
调音弹窗「默认」按钮(原 80)统一引用; 远程页面 assets/mobile 默认值同步为 85。
版本号 1.3.2 -> 1.3.3(versionCode 8 -> 9)。按约定本次不发布 Release, 仅本地打包测试。
117 lines
5.4 KiB
Kotlin
117 lines
5.4 KiB
Kotlin
package com.local.ktv
|
|
|
|
import android.graphics.Color
|
|
import android.graphics.drawable.ColorDrawable
|
|
import android.graphics.drawable.GradientDrawable
|
|
import android.graphics.drawable.StateListDrawable
|
|
import android.os.Build
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import java.util.Collections
|
|
import java.util.WeakHashMap
|
|
|
|
/** Applies the original OTT red-to-orange focus fill to remote-control targets. */
|
|
object TvFocusStyler {
|
|
private val installed = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
|
private val fillSuppressed = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
|
|
|
/**
|
|
* 禁止对该 View 施加焦点/按下填充 —— 用于「大面积展示型表面」。
|
|
*
|
|
* 背景:[install] 会把背景整体替换成不透明的红→橙渐变,作为遥控器焦点/按下的视觉反馈。
|
|
* 这对按钮、Tab、卡片是期望效果;但对铺满屏幕或覆盖播放画面的表面型 View
|
|
* (播放器宿主、视频层、播放触摸层、全屏播放容器、启动加载遮罩、二维码浮层)就是灾难:
|
|
* 只要触摸一次(`state_pressed`)就会用一个纯色层盖住整屏内容,即「纯色遮罩层」。
|
|
*
|
|
* 调用后该 View 保留自身背景且不再被 [install] 处理,平台默认灰色高亮也被关闭。
|
|
*
|
|
* @param focusable 是否顺带让该 View 可被遥控器聚焦(默认 true,与旧的 [install] 行为一致);
|
|
* 纯触摸捕获层(如 `playerTouchOverlay`)应传 false,否则会抢走遥控器焦点
|
|
* 且自身没有任何焦点视觉反馈。
|
|
*/
|
|
fun suppressFill(view: View, focusable: Boolean = true) {
|
|
fillSuppressed.add(view)
|
|
disablePlatformHighlight(view)
|
|
if (focusable && view.isClickable && view.isEnabled && view.visibility == View.VISIBLE) {
|
|
view.isFocusable = true
|
|
view.isFocusableInTouchMode = false
|
|
}
|
|
}
|
|
|
|
fun install(view: View) {
|
|
install(view, false)
|
|
}
|
|
|
|
/** Force the shared focus fill on platform widgets whose stock background is stateful. */
|
|
fun installAction(view: View) {
|
|
install(view, true)
|
|
}
|
|
|
|
private fun install(view: View, force: Boolean) {
|
|
disablePlatformHighlight(view)
|
|
if (!view.isEnabled || view.visibility != View.VISIBLE || !view.isClickable) return
|
|
// 显式标记为「表面型」的 View: 既不填充背景, 也不替调用方强制可聚焦
|
|
// (例如 playerTouchOverlay 刻意要求 isFocusable=false, 否则会抢走遥控器焦点)。
|
|
if (!force && fillSuppressed.contains(view)) return
|
|
view.isFocusable = true
|
|
view.isFocusableInTouchMode = false
|
|
if (!force && view.background?.isStateful == true) return
|
|
if (!installed.add(view) && !force) return
|
|
val normal = view.background ?: ColorDrawable(Color.TRANSPARENT)
|
|
val normalShape = normal as? GradientDrawable
|
|
val normalCornerRadius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
runCatching { normalShape?.cornerRadius }.getOrNull()
|
|
} else {
|
|
null
|
|
}
|
|
val normalCornerRadii = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
// Android 9's GradientDrawable getter throws when the shape uses a
|
|
// uniform radius and therefore has no per-corner array.
|
|
runCatching { normalShape?.cornerRadii?.clone() }.getOrNull()
|
|
} else {
|
|
null
|
|
}
|
|
fun focusFill() = GradientDrawable(
|
|
GradientDrawable.Orientation.LEFT_RIGHT,
|
|
intArrayOf(Color.rgb(232, 49, 111), Color.rgb(255, 154, 48)),
|
|
).apply {
|
|
when {
|
|
normalCornerRadii != null -> cornerRadii = normalCornerRadii.clone()
|
|
normalCornerRadius != null -> cornerRadius = normalCornerRadius
|
|
else -> cornerRadius = view.resources.displayMetrics.density * 7f
|
|
}
|
|
setStroke((view.resources.displayMetrics.density + 0.5f).toInt(), Color.argb(150, 255, 255, 255))
|
|
}
|
|
|
|
view.background = StateListDrawable().apply {
|
|
// Focus must never retain the normal drawable. Platform Buttons and a
|
|
// few legacy OTT assets use rectangular grey insets which remain visible
|
|
// around a rounded overlay and make the focus target look oversized.
|
|
addState(intArrayOf(android.R.attr.state_focused), focusFill())
|
|
addState(intArrayOf(android.R.attr.state_pressed), focusFill())
|
|
addState(intArrayOf(), normal)
|
|
}
|
|
}
|
|
|
|
fun installTree(view: View) {
|
|
disablePlatformHighlight(view)
|
|
// Structural dialog containers own navigation, but are not action targets.
|
|
// Styling them paints the entire option sheet instead of the active option.
|
|
if (view !is ViewGroup) install(view)
|
|
if (view is ViewGroup) repeat(view.childCount) { installTree(view.getChildAt(it)) }
|
|
}
|
|
|
|
fun disablePlatformHighlightTree(view: View) {
|
|
disablePlatformHighlight(view)
|
|
if (view is ViewGroup) repeat(view.childCount) {
|
|
disablePlatformHighlightTree(view.getChildAt(it))
|
|
}
|
|
}
|
|
|
|
private fun disablePlatformHighlight(view: View) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
view.defaultFocusHighlightEnabled = false
|
|
}
|
|
}
|
|
}
|