fix(app-glasses): панель статуса — по центру в оба глаза, процент цветной
Binocular-контент растягивался на весь 1280 (fillMaxSize внутри PositionedBox) — панель оказывалась по центру экрана, половина в каждом глазу. Теперь явная ширина половины (как у видео-Row) — панель по центру каждой половины, бинокулярно сливается в одну. ConnectionScreen тоже обёрнут в Binocular. Квадратик-индикатор убран — процент окрашивается цветом статуса (зелёный/белый/красный). Скрин: обе половины идентичны.
This commit is contained in:
@@ -17,29 +17,22 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -51,10 +44,11 @@ import com.rayneo.arsdk.android.ui.activity.BaseEventActivity
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import pw.binom.viewmate.glasses.gestures.actionName
|
import pw.binom.viewmate.glasses.gestures.actionName
|
||||||
import pw.binom.viewmate.glasses.gestures.templeActionToCommand
|
import pw.binom.viewmate.glasses.gestures.templeActionToCommand
|
||||||
import pw.binom.viewmate.glasses.status.CHARGE_COLOR_DISCHARGING
|
import pw.binom.viewmate.glasses.status.StatusPanelCompose
|
||||||
import pw.binom.viewmate.glasses.status.StatusPanelData
|
import pw.binom.viewmate.glasses.ui.Binocular
|
||||||
import pw.binom.viewmate.glasses.status.StatusPanelProvider
|
import pw.binom.viewmate.glasses.ui.BinocularScreenSize
|
||||||
import pw.binom.viewmate.glasses.ui.GlassesMovieScreen
|
import pw.binom.viewmate.glasses.ui.GlassesMovieScreen
|
||||||
|
import pw.binom.viewmate.glasses.ui.px
|
||||||
|
|
||||||
class MainActivity : BaseEventActivity() {
|
class MainActivity : BaseEventActivity() {
|
||||||
private var resumeAfterScreenOn = false
|
private var resumeAfterScreenOn = false
|
||||||
@@ -258,21 +252,13 @@ fun ConnectionScreen() {
|
|||||||
val status by connection.status.collectAsState()
|
val status by connection.status.collectAsState()
|
||||||
val messages by connection.messages.collectAsState()
|
val messages by connection.messages.collectAsState()
|
||||||
|
|
||||||
val context = LocalContext.current
|
// Бинокулярно: контент дублируется в оба глаза, панель — по центру каждой
|
||||||
var statusData by remember {
|
// половины (явная ширина половины, иначе fillMaxSize растянет на 1280).
|
||||||
mutableStateOf(StatusPanelData("--:--", 0, CHARGE_COLOR_DISCHARGING))
|
Binocular(scale = false) {
|
||||||
}
|
val screenWidthPx = BinocularScreenSize.current.width
|
||||||
val statusProvider = remember(context) {
|
Box(modifier = Modifier.width(screenWidthPx.px).fillMaxHeight().background(Color.Black)) {
|
||||||
StatusPanelProvider(context.applicationContext) { statusData = it }
|
|
||||||
}
|
|
||||||
DisposableEffect(statusProvider) {
|
|
||||||
statusProvider.start()
|
|
||||||
onDispose { statusProvider.stop() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
modifier = Modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 56.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium)
|
Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium)
|
||||||
@@ -286,30 +272,9 @@ fun ConnectionScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusPanelOverlay(
|
StatusPanelCompose(
|
||||||
data = statusData,
|
modifier = Modifier.align(Alignment.TopCenter).padding(top = 12.dp),
|
||||||
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Маленькая панель статуса (время + цветной квадрат + заряд) в правом верхнем углу. */
|
|
||||||
@Composable
|
|
||||||
private fun StatusPanelOverlay(data: StatusPanelData, modifier: Modifier = Modifier) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
.background(Color(0x80000000.toInt()))
|
|
||||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(12.dp)
|
|
||||||
.background(Color(data.chargeColorArgb))
|
|
||||||
)
|
|
||||||
Text(data.time, color = Color.White, style = MaterialTheme.typography.bodySmall)
|
|
||||||
Text("${data.percent}%", color = Color.White, style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package pw.binom.viewmate.glasses.status
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Композ-панель статуса (время + заряд). Вызывается внутри [Binocular] content,
|
||||||
|
* который рендерится дважды (левый/правый глаз), поэтому панель автоматически
|
||||||
|
* дублируется и бинокулярно сливается в одну.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun StatusPanelCompose(modifier: Modifier = Modifier) {
|
||||||
|
var data by remember { mutableStateOf<StatusPanelData?>(null) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
val provider = StatusPanelProvider(context) { data = it }
|
||||||
|
provider.start()
|
||||||
|
onDispose { provider.stop() }
|
||||||
|
}
|
||||||
|
val d = data ?: return
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.background(Color(0x99000000)) // полупрозрачный чёрный
|
||||||
|
.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(d.time, color = Color.White, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
// Процент — цветом статуса заряда (зелёный/белый/красный), без квадратика.
|
||||||
|
Text("${d.percent}%", color = Color(d.chargeColorArgb), style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
package pw.binom.viewmate.glasses.ui
|
package pw.binom.viewmate.glasses.ui
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.SurfaceTexture
|
import android.graphics.SurfaceTexture
|
||||||
import android.opengl.GLES20
|
import android.opengl.GLES20
|
||||||
import android.text.TextPaint
|
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
@@ -17,14 +13,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
import com.google.android.exoplayer2.video.VideoSize
|
import com.google.android.exoplayer2.video.VideoSize
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import pw.binom.viewmate.glasses.log
|
import pw.binom.viewmate.glasses.log
|
||||||
import pw.binom.viewmate.glasses.render.Drawable2d
|
|
||||||
import pw.binom.viewmate.glasses.render.EglCore
|
import pw.binom.viewmate.glasses.render.EglCore
|
||||||
import pw.binom.viewmate.glasses.render.FullFrameRect
|
import pw.binom.viewmate.glasses.render.FullFrameRect
|
||||||
import pw.binom.viewmate.glasses.render.GlUtil
|
|
||||||
import pw.binom.viewmate.glasses.render.Texture2dProgram
|
import pw.binom.viewmate.glasses.render.Texture2dProgram
|
||||||
import pw.binom.viewmate.glasses.render.WindowSurface
|
import pw.binom.viewmate.glasses.render.WindowSurface
|
||||||
|
|
||||||
@@ -72,7 +64,6 @@ fun BinocularVideoPlayer(
|
|||||||
*/
|
*/
|
||||||
class EglVideoRenderer(
|
class EglVideoRenderer(
|
||||||
private val player: SimpleExoPlayer,
|
private val player: SimpleExoPlayer,
|
||||||
private val density: Float = 1f,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var eglCore: EglCore? = null
|
private var eglCore: EglCore? = null
|
||||||
@@ -84,15 +75,6 @@ class EglVideoRenderer(
|
|||||||
|
|
||||||
private val outputs = mutableListOf<Output>()
|
private val outputs = mutableListOf<Output>()
|
||||||
|
|
||||||
// Overlay-панель статуса: рисуется в правом верхнем углу поверх видео.
|
|
||||||
private var overlayText: String? = null
|
|
||||||
private var overlayColorArgb: Int = 0
|
|
||||||
private var overlayBitmap: Bitmap? = null
|
|
||||||
private var overlayProgram: Texture2dProgram? = null
|
|
||||||
private var overlayDrawable: Drawable2d? = null
|
|
||||||
private var overlayTextureId = 0
|
|
||||||
private var overlayNeedsUpload = false
|
|
||||||
|
|
||||||
// Размер видео (0,0 пока неизвестен) — нужен для letterbox-вьюпорта.
|
// Размер видео (0,0 пока неизвестен) — нужен для letterbox-вьюпорта.
|
||||||
private var videoWidth = 0
|
private var videoWidth = 0
|
||||||
private var videoHeight = 0
|
private var videoHeight = 0
|
||||||
@@ -189,134 +171,9 @@ class EglVideoRenderer(
|
|||||||
GLES20.glViewport(rect.x0, rect.y0, rect.w, rect.h)
|
GLES20.glViewport(rect.x0, rect.y0, rect.w, rect.h)
|
||||||
}
|
}
|
||||||
blit.drawFrame(textureId, transformMatrix)
|
blit.drawFrame(textureId, transformMatrix)
|
||||||
drawOverlay(viewWidth, viewHeight)
|
|
||||||
windowSurface.swapBuffers()
|
windowSurface.swapBuffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Задаёт текст и цвет индикатора overlay-панели (обычно из StatusPanelProvider,
|
|
||||||
* вызывается на главном потоке). При неизменных значениях — no-op.
|
|
||||||
*/
|
|
||||||
fun setOverlay(text: String?, colorArgb: Int) {
|
|
||||||
if (text == overlayText && colorArgb == overlayColorArgb) return
|
|
||||||
overlayText = text
|
|
||||||
overlayColorArgb = colorArgb
|
|
||||||
overlayBitmap?.recycle()
|
|
||||||
overlayBitmap = text?.let { buildOverlayBitmap(it, colorArgb) }
|
|
||||||
overlayNeedsUpload = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Компактная панель ~110×34px: полупрозрачный чёрный фон, цветной квадрат слева, текст справа. */
|
|
||||||
private fun buildOverlayBitmap(text: String, colorArgb: Int): Bitmap {
|
|
||||||
val panelHeight = OVERLAY_PANEL_HEIGHT
|
|
||||||
val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
||||||
color = 0xFFFFFFFF.toInt()
|
|
||||||
textSize = OVERLAY_TEXT_SIZE_SP * density
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
val textWidth = kotlin.math.ceil(textPaint.measureText(text).toDouble()).toInt()
|
|
||||||
val panelWidth = OVERLAY_PADDING + OVERLAY_COLOR_SQUARE + OVERLAY_PADDING + textWidth + OVERLAY_PADDING
|
|
||||||
|
|
||||||
val bitmap = Bitmap.createBitmap(panelWidth, panelHeight, Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
val bgPaint = Paint().apply { color = 0x80000000.toInt() }
|
|
||||||
canvas.drawRect(0f, 0f, panelWidth.toFloat(), panelHeight.toFloat(), bgPaint)
|
|
||||||
|
|
||||||
val squareTop = (panelHeight - OVERLAY_COLOR_SQUARE) / 2f
|
|
||||||
val squarePaint = Paint().apply { color = colorArgb }
|
|
||||||
canvas.drawRect(
|
|
||||||
OVERLAY_PADDING.toFloat(), squareTop,
|
|
||||||
(OVERLAY_PADDING + OVERLAY_COLOR_SQUARE).toFloat(), squareTop + OVERLAY_COLOR_SQUARE,
|
|
||||||
squarePaint,
|
|
||||||
)
|
|
||||||
|
|
||||||
val baseline = panelHeight / 2f - (textPaint.ascent() + textPaint.descent()) / 2f
|
|
||||||
canvas.drawText(
|
|
||||||
text,
|
|
||||||
(OVERLAY_PADDING + OVERLAY_COLOR_SQUARE + OVERLAY_PADDING).toFloat(),
|
|
||||||
baseline,
|
|
||||||
textPaint,
|
|
||||||
)
|
|
||||||
return bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Рисует overlay-панель в правом верхнем углу (после блита видео, перед swapBuffers). */
|
|
||||||
private fun drawOverlay(viewWidth: Int, viewHeight: Int) {
|
|
||||||
val bitmap = overlayBitmap ?: return
|
|
||||||
if (overlayProgram == null) {
|
|
||||||
overlayProgram = Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_2D)
|
|
||||||
overlayDrawable = Drawable2d(Drawable2d.Prefab.RECTANGLE)
|
|
||||||
}
|
|
||||||
if (overlayTextureId == 0) {
|
|
||||||
val textures = IntArray(1)
|
|
||||||
GLES20.glGenTextures(1, textures, 0)
|
|
||||||
overlayTextureId = textures[0]
|
|
||||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, overlayTextureId)
|
|
||||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
|
||||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
|
||||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
|
||||||
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
|
||||||
overlayNeedsUpload = true
|
|
||||||
}
|
|
||||||
if (overlayNeedsUpload) {
|
|
||||||
val w = bitmap.width
|
|
||||||
val h = bitmap.height
|
|
||||||
val buffer = ByteBuffer.allocateDirect(w * h * 4).order(ByteOrder.nativeOrder())
|
|
||||||
bitmap.copyPixelsToBuffer(buffer)
|
|
||||||
buffer.position(0)
|
|
||||||
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, overlayTextureId)
|
|
||||||
GLES20.glTexImage2D(
|
|
||||||
GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
|
|
||||||
w, h, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer,
|
|
||||||
)
|
|
||||||
overlayNeedsUpload = false
|
|
||||||
}
|
|
||||||
|
|
||||||
GLES20.glViewport(0, 0, viewWidth, viewHeight)
|
|
||||||
GLES20.glEnable(GLES20.GL_BLEND)
|
|
||||||
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
|
|
||||||
|
|
||||||
val program = overlayProgram!!
|
|
||||||
val drawable = overlayDrawable!!
|
|
||||||
program.draw(
|
|
||||||
mvpMatrix = overlayMvp(
|
|
||||||
viewWidth, viewHeight, bitmap.width, bitmap.height, OVERLAY_MARGIN,
|
|
||||||
),
|
|
||||||
vertexBuffer = drawable.mVertexArray,
|
|
||||||
firstVertex = 0,
|
|
||||||
vertexCount = drawable.mVertexCount,
|
|
||||||
coordsPerVertex = drawable.mCoordsPerVertex,
|
|
||||||
vertexStride = drawable.mVertexStride,
|
|
||||||
texMatrix = GlUtil.IDENTITY_MATRIX,
|
|
||||||
texBuffer = drawable.mTexCoordArray,
|
|
||||||
textureId = overlayTextureId,
|
|
||||||
texStride = drawable.mTexCoordStride,
|
|
||||||
)
|
|
||||||
GLES20.glDisable(GLES20.GL_BLEND)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MVP для квада панели: прямоугольник RECTANGLE (центр в 0, размер 1×1)
|
|
||||||
* смещаем/масштабируем так, чтобы панель w×h оказалась в правом верхнем
|
|
||||||
* углу вьюпорта с отступом [margin].
|
|
||||||
*/
|
|
||||||
private fun overlayMvp(viewWidth: Int, viewHeight: Int, w: Int, h: Int, margin: Int): FloatArray {
|
|
||||||
val left = viewWidth - w - margin
|
|
||||||
val top = viewHeight - h - margin
|
|
||||||
val cx = left + w / 2f
|
|
||||||
val cy = top + h / 2f
|
|
||||||
val tx = 2f * cx / viewWidth - 1f
|
|
||||||
val ty = 1f - 2f * cy / viewHeight
|
|
||||||
val sx = 2f * w / viewWidth
|
|
||||||
val sy = 2f * h / viewHeight
|
|
||||||
return floatArrayOf(
|
|
||||||
sx, 0f, 0f, 0f,
|
|
||||||
0f, sy, 0f, 0f,
|
|
||||||
0f, 0f, 1f, 0f,
|
|
||||||
tx, ty, 0f, 1f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
if (released) return
|
if (released) return
|
||||||
released = true
|
released = true
|
||||||
@@ -339,18 +196,6 @@ class EglVideoRenderer(
|
|||||||
videoSurfaceTexture?.release()
|
videoSurfaceTexture?.release()
|
||||||
videoSurfaceTexture = null
|
videoSurfaceTexture = null
|
||||||
|
|
||||||
// Overlay-текстура/программа живут в GL-контексте — удаляем, пока он ещё текущий.
|
|
||||||
outputs.firstOrNull()?.let { it.surface.makeCurrent() }
|
|
||||||
if (overlayTextureId != 0) {
|
|
||||||
GLES20.glDeleteTextures(1, intArrayOf(overlayTextureId), 0)
|
|
||||||
overlayTextureId = 0
|
|
||||||
}
|
|
||||||
overlayProgram?.release()
|
|
||||||
overlayProgram = null
|
|
||||||
overlayDrawable = null
|
|
||||||
overlayBitmap?.recycle()
|
|
||||||
overlayBitmap = null
|
|
||||||
|
|
||||||
outputs.forEach { it.surface.release() }
|
outputs.forEach { it.surface.release() }
|
||||||
outputs.clear()
|
outputs.clear()
|
||||||
|
|
||||||
@@ -360,14 +205,6 @@ class EglVideoRenderer(
|
|||||||
eglCore?.release()
|
eglCore?.release()
|
||||||
eglCore = null
|
eglCore = null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val OVERLAY_PADDING = 6
|
|
||||||
private const val OVERLAY_COLOR_SQUARE = 12
|
|
||||||
private const val OVERLAY_PANEL_HEIGHT = 34
|
|
||||||
private const val OVERLAY_TEXT_SIZE_SP = 18f
|
|
||||||
private const val OVERLAY_MARGIN = 8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Прямоугольник вписывания видео в поверхность (letterbox): левый-нижний угол и размеры. */
|
/** Прямоугольник вписывания видео в поверхность (letterbox): левый-нижний угол и размеры. */
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import android.app.Activity
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -13,14 +17,16 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
import com.google.android.exoplayer2.video.VideoSize
|
import com.google.android.exoplayer2.video.VideoSize
|
||||||
import pw.binom.viewmate.glasses.log
|
import pw.binom.viewmate.glasses.log
|
||||||
import pw.binom.viewmate.glasses.status.StatusPanelProvider
|
import pw.binom.viewmate.glasses.status.StatusPanelCompose
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Экран кино: один плеер, вывод — дважды (левый/правый глаз) через [Binocular].
|
* Экран кино: один плеер, вывод — дважды (левый/правый глаз) через [Binocular].
|
||||||
@@ -48,19 +54,7 @@ fun GlassesMovieScreen(
|
|||||||
// Один рендерер на экран: оба экземпляра BinocularVideoPlayer (левый/правый
|
// Один рендерер на экран: оба экземпляра BinocularVideoPlayer (левый/правый
|
||||||
// глаз из Binocular) регистрируют в нём свои SurfaceView.
|
// глаз из Binocular) регистрируют в нём свои SurfaceView.
|
||||||
val renderer = remember {
|
val renderer = remember {
|
||||||
EglVideoRenderer(player, density = context.resources.displayMetrics.density)
|
EglVideoRenderer(player)
|
||||||
}
|
|
||||||
|
|
||||||
// Панель статуса (время + заряд) рисуется в GL-рендерере поверх видео —
|
|
||||||
// на обе поверхности, т.к. рендерер общий.
|
|
||||||
val statusProvider = remember {
|
|
||||||
StatusPanelProvider(context.applicationContext) { data ->
|
|
||||||
renderer.setOverlay("${data.time} ${data.percent}%", data.chargeColorArgb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
statusProvider.start()
|
|
||||||
onDispose { statusProvider.stop() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(player) {
|
DisposableEffect(player) {
|
||||||
@@ -97,6 +91,10 @@ fun GlassesMovieScreen(
|
|||||||
Side.RIGHT -> Arrangement.Start
|
Side.RIGHT -> Arrangement.Start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ВАЖНО: явная ширина половины (как у Row ниже) — иначе fillMaxSize
|
||||||
|
// растянет контент на весь 1280, и панель окажется по центру экрана
|
||||||
|
// (половина в каждом глазу) вместо центра каждой половины.
|
||||||
|
Box(modifier = Modifier.width((screenWidthPx).px).fillMaxHeight()) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
@@ -109,6 +107,13 @@ fun GlassesMovieScreen(
|
|||||||
modifier = Modifier.size(playerWidth.px, playerHeight.px),
|
modifier = Modifier.size(playerWidth.px, playerHeight.px),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Панель по центру верхней letterbox-полосы: в каждом глазу — в центре
|
||||||
|
// своей половины, бинокулярно сливается в одну по центру.
|
||||||
|
StatusPanelCompose(
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter).padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user