diff --git a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt index 1dd933e..af1c79a 100644 --- a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt +++ b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/MainActivity.kt @@ -37,11 +37,17 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { @Volatile private var remoteH = 0 + /** 最近一次触摸事件(诊断用:logcat 被限流,状态栏是唯一可靠通道) */ + @Volatile + private var lastTouchDbg = "-" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Log.i(TAG, "onCreate start") surfaceView = RemoteSurfaceView(this).apply { listener = this@MainActivity + onDebug = { msg -> lastTouchDbg = msg; refreshStatus() } holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) {} override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {} @@ -58,14 +64,20 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { setTextColor(0xFFFFFFFF.toInt()) } - // 画面居中、黑边填充:LinearLayout 竖直 + gravity center + // 画面居中、黑边填充。 + // 用 FrameLayout + MATCH_PARENT(不是 WRAP_CONTENT):FrameLayout 会把 + // 子视图的 WRAP_CONTENT 当 MATCH_PARENT 处理,onMeasure 里拿到的 + // MeasureSpec 就成了 EXACTLY 全尺寸,letterbox 比例收缩被直接忽略 + // (2026-10-30 实测:视图被撑成 1080x2001,触摸按 2001 归一化 → 坐标全错)。 + // MATCH_PARENT + gravity=center 时父容器传 AT_MOST,onMeasure 的 + // setMeasuredDimension 才真正决定尺寸,再被 gravity 居中摆放。 val stage = android.widget.FrameLayout(this).apply { setBackgroundColor(0xFF000000.toInt()) addView( surfaceView, android.widget.FrameLayout.LayoutParams( - android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, - android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, + android.widget.FrameLayout.LayoutParams.MATCH_PARENT, + android.widget.FrameLayout.LayoutParams.MATCH_PARENT, android.view.Gravity.CENTER ) ) @@ -74,23 +86,33 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { orientation = android.widget.LinearLayout.VERTICAL addView(stage, android.widget.LinearLayout.LayoutParams( android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)) + // 固定高度而非 WRAP_CONTENT:与 weight=1 的兄弟共存时, + // WRAP_CONTENT 可能被压成 0(实测 bounds=[0,0][0,0],状态栏不可见) addView(statusView, android.widget.LinearLayout.LayoutParams( android.widget.LinearLayout.LayoutParams.MATCH_PARENT, - android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)) + (48 * resources.displayMetrics.density).toInt())) } setContentView(root) - - showHint() + refreshStatus() intent?.getStringExtra("host")?.let { host -> connect(host, intent.getIntExtra("port", 47800), intent.getStringExtra("code") ?: "123456") } } - private fun showHint() { - statusView.text = if (handle != 0L) { - "已连接 ${remoteW}x$remoteH|单指=左键 双指轻点=右键 双指滑动=滚轮 返回键=断开" - } else { - "未连接|am start -n com.verify.remote/.MainActivity --es host --ei port 47800 --es code t" + /** 状态栏统一刷新:连接态 + 帧数/RTT + 最近触摸诊断(logcat 被限流,这是唯一通道) */ + @Volatile + private var lastFrames = 0L + @Volatile + private var lastRtt = 0L + + private fun refreshStatus() { + if (handle == 0L) { + statusView.text = "未连接|am start ... --es host --ei port 47800 --es code t" + return + } + statusView.text = buildString { + append("已连接 ${remoteW}x$remoteH 帧=$lastFrames RTT=${lastRtt}ms\n") + append("触摸: $lastTouchDbg") } } @@ -135,8 +157,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { remoteW = w remoteH = ht runOnUiThread { - if (w > 0 && ht > 0) surfaceView.videoAspectRatio = w.toFloat() / ht - showHint() + if (w > 0 && ht > 0) { + surfaceView.videoAspectRatio = w.toFloat() / ht + } + refreshStatus() startDecodeLoop(h) } } catch (e: Exception) { @@ -173,19 +197,14 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { } frames++ if (frames % 120 == 0L) { - val rtt = RustBridge.rttMs(h) - runOnUiThread { - statusView.text = - "已连接 ${remoteW}x$remoteH 帧=$frames RTT=${rtt}ms|返回键断开" - } + lastFrames = frames + lastRtt = RustBridge.rttMs(h) + runOnUiThread { refreshStatus() } } } dec.release() Log.i(TAG, "decode loop ended, frames=$frames") - runOnUiThread { - statusView.text = "会话结束(帧 $frames)" - showHint() - } + runOnUiThread { refreshStatus() } } } @@ -200,8 +219,10 @@ class MainActivity : Activity(), RemoteSurfaceView.Listener { handle = 0L remoteW = 0 remoteH = 0 + lastFrames = 0 + lastRtt = 0 RustBridge.destroy(h) - showHint() + refreshStatus() } override fun onDestroy() { diff --git a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/RemoteSurfaceView.kt b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/RemoteSurfaceView.kt index 22305a9..d8326e8 100644 --- a/apps/android-viewer/app/src/main/kotlin/com/verify/remote/RemoteSurfaceView.kt +++ b/apps/android-viewer/app/src/main/kotlin/com/verify/remote/RemoteSurfaceView.kt @@ -62,15 +62,33 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) { isFocusable = true } + /** + * letterbox 测量:在父容器给出的可用空间内按视频宽高比取最大内接矩形。 + * + * 关键:父容器给 WRAP_CONTENT 时 MeasureSpec 是 AT_MOST,getSize() 是 + * **上限**而非实际可用尺寸。据此算出的尺寸必须自己 setMeasuredDimension, + * 否则父容器会把视图撑满(2026-10-30 实测:视图变成 1080x2001 而非 + * 1080x607,触摸按 2001 归一化 → 坐标全错,点中心落到画面外)。 + */ override fun onMeasure(widthSpec: Int, heightSpec: Int) { - super.onMeasure(widthSpec, heightSpec) val ar = videoAspectRatio - if (ar <= 0f) return - var w = measuredWidth - var h = measuredHeight - if (w <= 0 || h <= 0) return - if (w.toFloat() > h * ar) w = (h * ar).toInt() else h = (w / ar).toInt() - setMeasuredDimension(w, h) + if (ar <= 0f) { + super.onMeasure(widthSpec, heightSpec) + return + } + val availW = MeasureSpec.getSize(widthSpec) + val availH = MeasureSpec.getSize(heightSpec) + if (availW <= 0 || availH <= 0) { + super.onMeasure(widthSpec, heightSpec) + return + } + var w = availW + var h = (availW / ar).toInt() + if (h > availH) { + h = availH + w = (availH * ar).toInt() + } + setMeasuredDimension(w.coerceAtLeast(1), h.coerceAtLeast(1)) } private fun normX(x: Float) = (x / width * 65535).toInt().coerceIn(0, 65535) @@ -85,9 +103,28 @@ class RemoteSurfaceView(context: Context) : SurfaceView(context) { private fun midY(e: MotionEvent) = if (e.pointerCount >= 2) (e.getY(0) + e.getY(1)) / 2f else e.y + /** 诊断回调:把触摸情况显示到状态栏(本机 logcat 被系统限流,日志不可靠) */ + var onDebug: ((String) -> Unit)? = null + override fun onTouchEvent(event: MotionEvent): Boolean { - val l = listener ?: return false + val l = listener ?: run { + onDebug?.invoke("touch: listener=null") + return false + } if (width == 0 || height == 0) return false + val act = when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> "DOWN" + MotionEvent.ACTION_MOVE -> "MOVE" + MotionEvent.ACTION_UP -> "UP" + MotionEvent.ACTION_POINTER_DOWN -> "P2DOWN" + MotionEvent.ACTION_POINTER_UP -> "P2UP" + MotionEvent.ACTION_CANCEL -> "CANCEL" + else -> event.actionMasked.toString() + } + onDebug?.invoke( + "$act view=(${event.x.toInt()},${event.y.toInt()}) " + + "norm=(${normX(event.x)},${normY(event.y)}) ${width}x$height" + ) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> {