feat(android): 横屏远控(自动方向 + 控制条自动隐藏 + 黑边唤出)
竖屏看 16:9 桌面太小(用户 2026-11-01 反馈)。探针实测(docs/output/ 2026-11-01-landscape-probe/):App 级 setRequestedOrientation 在这台 vivo 上有效(761ms),且旋转只触发 surfaceChanged、不销毁 surface、不断会话、 横屏 fps=58~60——所以不需要 TextureView(探针另测 TextureView 掉 30~37% 帧率)、不需要任何解码器重建配合,旧 S3 作废。 - OrientationPolicy(AUTO/LANDSCAPE/PORTRAIT/SYSTEM)纯逻辑 + 10 单测, 常量值经 javap 对 android-34 核实;已做 mutation 反向验证(AUTO 判定 > 改 >= 时方屏用例确实红)。 - 表单页强制竖屏;连接成功按策略设方向(默认 AUTO:远端宽>高即横屏)。 - 横屏 immersive + 状态行压 36dp + 控制条 3s 无操作自动隐藏。 - 唤出区 = letterbox 黑边(stage 在隐藏态消费画面外触摸)。刻意不用 底部边缘带:R7 回归实测那种带会吃掉远端桌面底部 ~11%(Windows 任务栏 就在那里),点任务栏会变成唤出菜单。黑边不属于 SurfaceView,零冲突。 - 竖屏永不隐藏(裁决只针对横屏空间不足)。
This commit is contained in:
parent
1a6c3f15a2
commit
cff7fc8ab3
@ -11,8 +11,8 @@ android {
|
||||
applicationId = "com.verify.remote"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 3
|
||||
versionName = "0.1-phase3b"
|
||||
versionCode = 4
|
||||
versionName = "0.1-phase4a"
|
||||
// 阶段 1 只测真机 arm64;x86_64 留给模拟器回归
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a")
|
||||
|
||||
@ -46,10 +46,14 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
private lateinit var connectButton: Button
|
||||
private lateinit var disconnectButton: Button
|
||||
private lateinit var modeButton: Button
|
||||
private lateinit var orientationButton: Button
|
||||
private lateinit var sensUpButton: Button
|
||||
private lateinit var sensDownButton: Button
|
||||
private lateinit var sensLabel: TextView
|
||||
private lateinit var connectStatus: TextView
|
||||
/** 4a:横屏自动隐藏的两个控制条(Q3=A)+ 状态行 */
|
||||
private lateinit var controlBar: LinearLayout
|
||||
private lateinit var sensBar: LinearLayout
|
||||
|
||||
@Volatile
|
||||
private var handle = 0L
|
||||
@ -80,13 +84,119 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
@Volatile
|
||||
private var lastTouchDbg = "-"
|
||||
|
||||
/** 4a:方向策略(Q1=A 默认 AUTO,Q4=A 底栏按钮循环切换并落 prefs) */
|
||||
private var orientationPolicy = OrientationPolicy.AUTO
|
||||
|
||||
/** 4a:首次进横屏提示一次「点底部唤出菜单」(Q7=A),只提示一次不骚扰 */
|
||||
private var landscapeHintShown = false
|
||||
|
||||
// ---- 4a/Q3=A:横屏控制条自动隐藏(Q7=A:3s + 40dp 边缘带 + 首次 toast) ----
|
||||
|
||||
private val ui = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
private var controlsVisible = true
|
||||
|
||||
private val hideControlsTask = Runnable {
|
||||
// 调度后可能已转回竖屏:执行时再判一次,竖屏永不隐藏(裁决只针对横屏)
|
||||
if (isLandscape()) setControlsVisible(false)
|
||||
}
|
||||
|
||||
private fun isLandscape(): Boolean =
|
||||
resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
/**
|
||||
* 控制条显隐的唯一入口(状态行 + 灵敏度行 + 底栏一起)。
|
||||
* `controlsVisible` 同时是 stage 黑边唤出的判据,只有这一处写它。
|
||||
*/
|
||||
private fun setControlsVisible(v: Boolean) {
|
||||
controlsVisible = v
|
||||
val vis = if (v) View.VISIBLE else View.GONE
|
||||
statusView.visibility = vis
|
||||
sensBar.visibility = vis
|
||||
controlBar.visibility = vis
|
||||
}
|
||||
|
||||
/** 任何触摸/唤出都重新计时;只在横屏 + 已连接 + 非等待期才挂隐藏任务 */
|
||||
private fun scheduleControlsHide() {
|
||||
ui.removeCallbacks(hideControlsTask)
|
||||
if (isLandscape() && handle != 0L && !connecting) {
|
||||
ui.postDelayed(hideControlsTask, CONTROLS_HIDE_MS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 横屏沉浸式:隐藏系统状态栏 + 导航栏(minSdk 26 → 用 legacy 标志;
|
||||
* API 30+ 走 insetsController 分支,本机 API 29 实测走的是 legacy 路径)。
|
||||
* IMMERSIVE_STICKY:滑动边缘临时唤出后自动收回,不打断远控操作。
|
||||
*
|
||||
* 名字刻意不叫 setImmersive——`Activity` 基类有同名废弃方法,
|
||||
* Kotlin 会要求 override(真机编译实测 2026-11-01)。
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun applyImmersive(on: Boolean) {
|
||||
val flags = android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
android.view.View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
window.decorView.systemUiVisibility = if (on) flags else 0
|
||||
}
|
||||
|
||||
/** 横屏时状态行压成 36dp 单行(纵向空间是稀缺资源),竖屏恢复 64dp 三行 */
|
||||
private fun applyStatusLayout() {
|
||||
val lp = statusView.layoutParams as LinearLayout.LayoutParams
|
||||
lp.height = dp(if (isLandscape()) 36 else 64)
|
||||
statusView.layoutParams = lp
|
||||
statusView.maxLines = if (isLandscape()) 1 else 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前方向应用远控页形态(进面板、旋转各调一次)。
|
||||
* 竖屏保持 3b 原样(常驻控制条)——本轮裁决只针对横屏空间不足的问题。
|
||||
*/
|
||||
private fun applyRemoteChrome() {
|
||||
val land = isLandscape()
|
||||
applyImmersive(land)
|
||||
applyStatusLayout()
|
||||
if (land) {
|
||||
if (!landscapeHintShown) {
|
||||
landscapeHintShown = true
|
||||
android.widget.Toast.makeText(this, "无操作 3 秒隐藏菜单,点屏幕底部唤出",
|
||||
android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
setControlsVisible(true)
|
||||
scheduleControlsHide()
|
||||
} else {
|
||||
setControlsVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前策略 + 已知远端尺寸应用屏幕方向(4a)。
|
||||
*
|
||||
* 探针实测(2026-11-01):App 级 requestedOrientation 在这台 vivo 上有效(761ms),
|
||||
* 且旋转只触发 surfaceChanged、不销毁 surface、不断会话——所以这里**只设方向**,
|
||||
* 不需要任何解码器/会话保护配套(旧 S3 作废的根据)。
|
||||
*/
|
||||
private fun applyOrientation() {
|
||||
requestedOrientation = Orientation.requestFor(orientationPolicy, remoteW, remoteH)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.i(TAG, "onCreate start")
|
||||
|
||||
// 4a/Q6=A:表单页强制竖屏(输入框 + 软键盘在竖屏好用)。
|
||||
// 放在 setContentView 前:首帧就定下方向,避免横屏手机打开 App 先闪一下。
|
||||
orientationPolicy = Orientation.fromName(prefs().getString(PREF_ORIENTATION, null))
|
||||
requestedOrientation = Orientation.REQUEST_PORTRAIT
|
||||
|
||||
surfaceView = RemoteSurfaceView(this).apply {
|
||||
listener = this@MainActivity
|
||||
onDebug = { msg -> lastTouchDbg = msg; refreshStatus() }
|
||||
// 4a:画面内的任何触摸重置隐藏计时;黑边唤出在 stage 的
|
||||
// dispatchTouchEvent 里做(那里能区分「落在画面外」)
|
||||
onAnyTouch = { scheduleControlsHide() }
|
||||
holder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {}
|
||||
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
|
||||
@ -200,7 +310,32 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
// (2026-10-30 实测:视图被撑成 1080x2001,触摸按 2001 归一化 → 坐标全错)。
|
||||
// MATCH_PARENT + gravity=center 时父容器传 AT_MOST,onMeasure 的
|
||||
// setMeasuredDimension 才真正决定尺寸,再被 gravity 居中摆放。
|
||||
val stage = FrameLayout(this).apply {
|
||||
//
|
||||
// 4a:控制条隐藏时,落在**画面黑边**(SurfaceView 之外)的触摸唤出控制条。
|
||||
// 为什么用黑边而不是底部边缘带(后者已废弃):letterbox 黑边本来就不承载
|
||||
// 任何远端操作,拿它当唤出区是零冲突的;底部边缘带会吃掉远端桌面最下
|
||||
// ~11% 的高度,而 Windows 任务栏就在那里(2026-11-01 R7 回归实测:
|
||||
// 任务栏位置的 tap 被唤出带吞掉,映射误差 357px)。
|
||||
// 落在画面内的触摸一律放行给 SurfaceView。
|
||||
val stage = object : FrameLayout(this) {
|
||||
override fun dispatchTouchEvent(e: android.view.MotionEvent): Boolean {
|
||||
if (e.actionMasked == android.view.MotionEvent.ACTION_DOWN &&
|
||||
!controlsVisible && remotePanel.visibility == View.VISIBLE
|
||||
) {
|
||||
val v = surfaceView
|
||||
val inside = e.x >= v.x && e.x <= v.x + v.width &&
|
||||
e.y >= v.y && e.y <= v.y + v.height
|
||||
if (!inside) {
|
||||
setControlsVisible(true)
|
||||
scheduleControlsHide()
|
||||
lastTouchDbg = "唤出菜单"
|
||||
refreshStatus()
|
||||
return true // 消费:黑边不产生远端事件
|
||||
}
|
||||
}
|
||||
return super.dispatchTouchEvent(e)
|
||||
}
|
||||
}.apply {
|
||||
setBackgroundColor(0xFF000000.toInt())
|
||||
addView(
|
||||
surfaceView,
|
||||
@ -247,13 +382,26 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
// 取消语义必须只有一条路径(V-A1 判据依赖取消后的固定状态)。
|
||||
setOnClickListener { if (connecting) cancelConnect() else disconnect() }
|
||||
}
|
||||
val bar = LinearLayout(this).apply {
|
||||
// 4a/Q4=A:方向循环按钮 AUTO→横→竖→系统
|
||||
orientationButton = Button(this).apply {
|
||||
text = Orientation.label(orientationPolicy)
|
||||
setOnClickListener {
|
||||
orientationPolicy = Orientation.next(orientationPolicy)
|
||||
prefs().edit().putString(PREF_ORIENTATION, orientationPolicy.name).apply()
|
||||
text = Orientation.label(orientationPolicy)
|
||||
applyOrientation()
|
||||
lastTouchDbg = Orientation.label(orientationPolicy)
|
||||
refreshStatus()
|
||||
}
|
||||
}
|
||||
controlBar = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
addView(keyboardButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(modeButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(orientationButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(disconnectButton, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||
}
|
||||
val sensBar = LinearLayout(this).apply {
|
||||
sensBar = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
addView(sensDownButton, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
|
||||
addView(sensLabel, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
||||
@ -267,7 +415,7 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
// WRAP_CONTENT 可能被压成 0(实测 bounds=[0,0][0,0],状态栏不可见)
|
||||
addView(statusView, LayoutParams(LayoutParams.MATCH_PARENT, dp(64)))
|
||||
addView(sensBar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
addView(bar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
addView(controlBar, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,6 +438,13 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
connecting = false
|
||||
remotePanelVisible(false)
|
||||
connectStatus.text = msg
|
||||
// 4a/Q6=A:回表单就恢复竖屏(AUTO 策略下 remoteW/H 已被清零,
|
||||
// 但显式设 PORTRAIT 更稳:即使策略是 LANDSCAPE,表单也不该横着)。
|
||||
requestedOrientation = Orientation.REQUEST_PORTRAIT
|
||||
// 4a:表单页永远完整可见——收掉沉浸式、恢复控制条、撤掉隐藏计时
|
||||
ui.removeCallbacks(hideControlsTask)
|
||||
applyImmersive(false)
|
||||
setControlsVisible(true)
|
||||
}
|
||||
|
||||
private fun showRemoteState() {
|
||||
@ -299,6 +454,8 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
// 远控面板上的按钮第一下 tap 只被用来「丢焦点」而不触发点击——真机上表现
|
||||
// 为「按钮要点两次」(3a 的 D8 已实测到 4 轮全丢 tap)。
|
||||
clearFormFocus()
|
||||
// 4a:按当前方向应用形态(此刻可能还没转完,onConfigurationChanged 会再来一次)
|
||||
applyRemoteChrome()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -442,6 +599,20 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
updateKeyboardVisibility()
|
||||
// 4a:旋转(含 AUTO 策略触发的)会走到这里——探针实测 Activity 不重建、
|
||||
// surface 只 resize,所以这里只做形态切换,不碰会话。
|
||||
if (remotePanel.visibility == View.VISIBLE) {
|
||||
applyRemoteChrome()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 4a:任何用户交互都重置隐藏计时——系统栏临时唤出、面板按钮点击
|
||||
* (按钮在 SurfaceView 之外,onAnyTouch 覆盖不到)。
|
||||
*/
|
||||
override fun onUserInteraction() {
|
||||
super.onUserInteraction()
|
||||
if (remotePanel.visibility == View.VISIBLE) scheduleControlsHide()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -614,6 +785,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
if (w > 0 && ht > 0) {
|
||||
surfaceView.videoAspectRatio = w.toFloat() / ht
|
||||
}
|
||||
// 4a:拿到远端尺寸才谈得上 AUTO(Q1=A)。尺寸未知时 requestFor
|
||||
// 落回竖屏,不会把表单尺寸错当远端尺寸用。
|
||||
applyOrientation()
|
||||
refreshStatus()
|
||||
startDecodeLoop(h, gen)
|
||||
}
|
||||
@ -775,6 +949,9 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// 4a:撤掉隐藏计时,防止 Handler 持有已销毁 Activity(RemoteSurfaceView
|
||||
// 的长按任务有自己的 cancel,这里只管本 Activity 的)
|
||||
ui.removeCallbacks(hideControlsTask)
|
||||
disconnect()
|
||||
}
|
||||
|
||||
@ -808,6 +985,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener {
|
||||
private const val PREF_RELATIVE = "relative_mode"
|
||||
private const val PREF_SENSITIVITY = "sensitivity"
|
||||
private const val SENS_STEP = 0.25f
|
||||
/** 4a/Q4=A:方向策略落盘(存枚举名,未知值 fromName 落回 AUTO) */
|
||||
private const val PREF_ORIENTATION = "orientation_policy"
|
||||
/** 4a/Q7=A:横屏无操作隐藏控制条的延时 */
|
||||
private const val CONTROLS_HIDE_MS = 3000L
|
||||
|
||||
/**
|
||||
* 与协议版本保持一致:远控画面状态栏显示当前握手版本(方案 R5 要求)。
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package com.verify.remote
|
||||
|
||||
/** 4a:指针方向策略(Q1=A:默认 AUTO)。 */
|
||||
enum class OrientationPolicy { AUTO, LANDSCAPE, PORTRAIT, SYSTEM }
|
||||
|
||||
/**
|
||||
* 方向策略 → `Activity.requestedOrientation` 的纯映射(4a)。
|
||||
*
|
||||
* 抽成纯 Kotlin 的原因(与 WinScancode/ConnectErrorText 同一模式):
|
||||
* 映射表必须能在 JVM 单测里钉死,而 `ActivityInfo` 常量在 android.jar 桩上
|
||||
* 引用类加载有坑,这里直接落整数常量。
|
||||
*
|
||||
* 数值来源(不凭记忆,2026-11-01 `javap -constants` 对 android-34 android.jar 核实):
|
||||
* - SCREEN_ORIENTATION_LANDSCAPE = 0
|
||||
* - SCREEN_ORIENTATION_PORTRAIT = 1
|
||||
* - SCREEN_ORIENTATION_UNSPECIFIED = -1
|
||||
* 刻意只用这三个:探针在真机验证过 LANDSCAPE(0) 与 PORTRAIT 行为;
|
||||
* SENSOR_LANDSCAPE(6)(允许左右横)未在本机验证过,不引入——固定横屏对
|
||||
* 「看 16:9 桌面」的场景足够,翻转手机方向时系统不会再把画面翻到另一侧,
|
||||
* 这反而是更稳定的行为。
|
||||
*/
|
||||
object Orientation {
|
||||
|
||||
const val REQUEST_LANDSCAPE = 0
|
||||
const val REQUEST_PORTRAIT = 1
|
||||
const val REQUEST_UNSPECIFIED = -1
|
||||
|
||||
/**
|
||||
* AUTO 的语义:远端画面是横的(宽>高)就用横屏,否则竖屏。
|
||||
* 尺寸未知(0,连接前/断开后)按竖屏兜底——表单页本来就是竖屏。
|
||||
*/
|
||||
fun requestFor(policy: OrientationPolicy, remoteW: Int, remoteH: Int): Int = when (policy) {
|
||||
OrientationPolicy.AUTO ->
|
||||
if (remoteW > remoteH) REQUEST_LANDSCAPE else REQUEST_PORTRAIT
|
||||
OrientationPolicy.LANDSCAPE -> REQUEST_LANDSCAPE
|
||||
OrientationPolicy.PORTRAIT -> REQUEST_PORTRAIT
|
||||
OrientationPolicy.SYSTEM -> REQUEST_UNSPECIFIED
|
||||
}
|
||||
|
||||
/** 方向按钮的循环次序(Q4=A):AUTO→横→竖→系统→AUTO。 */
|
||||
fun next(policy: OrientationPolicy): OrientationPolicy = when (policy) {
|
||||
OrientationPolicy.AUTO -> OrientationPolicy.LANDSCAPE
|
||||
OrientationPolicy.LANDSCAPE -> OrientationPolicy.PORTRAIT
|
||||
OrientationPolicy.PORTRAIT -> OrientationPolicy.SYSTEM
|
||||
OrientationPolicy.SYSTEM -> OrientationPolicy.AUTO
|
||||
}
|
||||
|
||||
/** 按钮文案。每个策略都必须有非空、互不相同的文案(单测锁)。 */
|
||||
fun label(policy: OrientationPolicy): String = when (policy) {
|
||||
OrientationPolicy.AUTO -> "方向:自动"
|
||||
OrientationPolicy.LANDSCAPE -> "方向:横屏"
|
||||
OrientationPolicy.PORTRAIT -> "方向:竖屏"
|
||||
OrientationPolicy.SYSTEM -> "方向:系统"
|
||||
}
|
||||
|
||||
/** prefs 读写:未知字符串(版本回退/手改)必须落回 AUTO,不得抛。 */
|
||||
fun fromName(name: String?): OrientationPolicy =
|
||||
runCatching { OrientationPolicy.valueOf(name.orEmpty().uppercase()) }
|
||||
.getOrDefault(OrientationPolicy.AUTO)
|
||||
}
|
||||
@ -246,6 +246,22 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) {
|
||||
*/
|
||||
var onDebug: ((String) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* 4a/Q3=A:任何触摸都回调一次(Activity 用它重置自动隐藏计时)。
|
||||
*
|
||||
* 唤出区刻意**不在本视图内**(原底部 40dp 边缘带的设计已废弃):那条带会吃掉
|
||||
* 远端桌面底部约 11% 的高度——Windows 任务栏就在那里,点任务栏会变成唤出菜单
|
||||
* 而不是点击任务栏(2026-11-01 R7 回归实测暴露:frac=0.9 的 tap 被边缘带吞掉)。
|
||||
* 改为由 Activity 在 stage 上消费「画面黑边」的触摸:黑边不属于本视图,
|
||||
* 天然与远端操作零冲突。
|
||||
*/
|
||||
var onAnyTouch: (() -> Unit)? = null
|
||||
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||
onAnyTouch?.invoke()
|
||||
return super.dispatchTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val l = listener ?: run {
|
||||
onDebug?.invoke("触摸(未接线)")
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package com.verify.remote
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 4a:方向策略纯逻辑。
|
||||
*
|
||||
* 常量数值不是随手写的——`javap -constants` 对 android-34 android.jar 核实过
|
||||
* (LANDSCAPE=0、PORTRAIT=1、UNSPECIFIED=-1)。这里把它们钉成断言:
|
||||
* 若有人「顺手改成 SENSOR_LANDSCAPE=6」或调换数值,测试先红。
|
||||
*/
|
||||
class OrientationPolicyTest {
|
||||
|
||||
@Test
|
||||
fun android_constants_pinned_to_verified_values() {
|
||||
assertEquals(0, Orientation.REQUEST_LANDSCAPE)
|
||||
assertEquals(1, Orientation.REQUEST_PORTRAIT)
|
||||
assertEquals(-1, Orientation.REQUEST_UNSPECIFIED)
|
||||
}
|
||||
|
||||
// ---- AUTO 的判定 ----
|
||||
|
||||
@Test
|
||||
fun auto_picks_landscape_for_wider_remote() {
|
||||
// 本机被控端就是 1920x1080 —— 这正是 4a 需求的来源
|
||||
assertEquals(Orientation.REQUEST_LANDSCAPE, Orientation.requestFor(OrientationPolicy.AUTO, 1920, 1080))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun auto_picks_portrait_for_taller_remote() {
|
||||
assertEquals(Orientation.REQUEST_PORTRAIT, Orientation.requestFor(OrientationPolicy.AUTO, 1080, 1920))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun auto_picks_portrait_for_square_and_unknown() {
|
||||
// 方屏没有「更宽」的优势,按竖屏兜底
|
||||
assertEquals(Orientation.REQUEST_PORTRAIT, Orientation.requestFor(OrientationPolicy.AUTO, 1080, 1080))
|
||||
// 尺寸未知(连接前/断开后)必须竖屏,不能让表单页横掉
|
||||
assertEquals(Orientation.REQUEST_PORTRAIT, Orientation.requestFor(OrientationPolicy.AUTO, 0, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forced_policies_ignore_remote_geometry() {
|
||||
for (wh in listOf(1920 to 1080, 1080 to 1920, 0 to 0)) {
|
||||
assertEquals(Orientation.REQUEST_LANDSCAPE,
|
||||
Orientation.requestFor(OrientationPolicy.LANDSCAPE, wh.first, wh.second))
|
||||
assertEquals(Orientation.REQUEST_PORTRAIT,
|
||||
Orientation.requestFor(OrientationPolicy.PORTRAIT, wh.first, wh.second))
|
||||
assertEquals(Orientation.REQUEST_UNSPECIFIED,
|
||||
Orientation.requestFor(OrientationPolicy.SYSTEM, wh.first, wh.second))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 按钮循环(Q4=A)----
|
||||
|
||||
@Test
|
||||
fun button_cycles_all_four_in_order() {
|
||||
assertEquals(OrientationPolicy.LANDSCAPE, Orientation.next(OrientationPolicy.AUTO))
|
||||
assertEquals(OrientationPolicy.PORTRAIT, Orientation.next(OrientationPolicy.LANDSCAPE))
|
||||
assertEquals(OrientationPolicy.SYSTEM, Orientation.next(OrientationPolicy.PORTRAIT))
|
||||
assertEquals(OrientationPolicy.AUTO, Orientation.next(OrientationPolicy.SYSTEM))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun four_clicks_return_to_start() {
|
||||
var p = OrientationPolicy.AUTO
|
||||
repeat(4) { p = Orientation.next(p) }
|
||||
assertEquals(OrientationPolicy.AUTO, p)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun every_policy_has_distinct_nonempty_label() {
|
||||
val labels = OrientationPolicy.values().map { Orientation.label(it) }
|
||||
labels.forEach { assertNotEquals("", it) }
|
||||
assertEquals("四个策略文案必须互不相同", labels.size, labels.toSet().size)
|
||||
// 文案要让用户看出当前状态(含「方向:」前缀),不能只写「自动/横/竖」
|
||||
labels.forEach { assertEquals(true, it.startsWith("方向:")) }
|
||||
}
|
||||
|
||||
// ---- prefs 反序列化 ----
|
||||
|
||||
@Test
|
||||
fun from_name_roundtrips_all() {
|
||||
OrientationPolicy.values().forEach {
|
||||
assertEquals(it, Orientation.fromName(it.name))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun from_name_falls_back_to_auto_for_garbage() {
|
||||
// 未知值不得抛异常(会让 Activity 起不来),必须落回默认 AUTO
|
||||
assertEquals(OrientationPolicy.AUTO, Orientation.fromName(null))
|
||||
assertEquals(OrientationPolicy.AUTO, Orientation.fromName(""))
|
||||
assertEquals(OrientationPolicy.AUTO, Orientation.fromName("DIAGONAL"))
|
||||
// 大小写宽容:早期版本可能存过小写
|
||||
assertEquals(OrientationPolicy.LANDSCAPE, Orientation.fromName("landscape"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user