mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Android: library background = static XMB still, drop the MP4 (perf)
The looping MP4 wallpaper hurt in-library performance (sbro review). Replace it with a bundled static XMB-wave still (R.drawable.library_bg_xmb, drawn edge-to-edge via ArmsBackdrop's backgroundLayer). Remove VideoBackground.kt, res/raw/xmb_wave.mp4, and the video option from the background picker.
This commit is contained in:
@@ -6,8 +6,6 @@ import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
@@ -160,26 +158,23 @@ fun HomeScreen(
|
||||
|
||||
CompositionLocalProvider(LocalCustomCoverMap provides customCoverMap) {
|
||||
ArmsBackdrop(
|
||||
// Full-bleed wallpaper: the library video/image + readability scrim, drawn
|
||||
// edge-to-edge (behind the gesture bar) so it never leaves an exposed strip at
|
||||
// the bottom — that strip was the "blue bar" in landscape.
|
||||
// Full-bleed wallpaper: the library image + readability scrim, drawn edge-to-edge
|
||||
// (behind the gesture bar) so it never leaves an exposed strip at the bottom —
|
||||
// that strip was the "blue bar" in landscape.
|
||||
backgroundLayer = {
|
||||
val libraryBg = LibraryBackground.uri.value
|
||||
val bgVideoUri = when {
|
||||
// Default (no user pick): the bundled PS3 XMB-wave video background.
|
||||
libraryBg == null -> defaultBackgroundVideoUri(context)
|
||||
// User picked a video file — play it as a moving background.
|
||||
isVideoBackground(context, libraryBg) -> Uri.parse(libraryBg)
|
||||
// User picked a still image / GIF — Coil handles it below.
|
||||
else -> null
|
||||
}
|
||||
if (bgVideoUri != null) {
|
||||
// key() so swapping the URI rebuilds the player (the AndroidView
|
||||
// factory only runs once).
|
||||
key(bgVideoUri.toString()) {
|
||||
VideoBackground(bgVideoUri, Modifier.fillMaxSize())
|
||||
}
|
||||
if (libraryBg == null) {
|
||||
// Default: the bundled PS3 XMB-wave STILL. It used to be a looping MP4,
|
||||
// but the continuous video decode cost in-library performance (sbro
|
||||
// review), so it's a static image now.
|
||||
Image(
|
||||
painter = painterResource(R.drawable.library_bg_xmb),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
// User-picked still image / GIF (Coil handles both).
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(libraryBg).crossfade(true).build(),
|
||||
contentDescription = null,
|
||||
@@ -365,7 +360,7 @@ fun HomeScreen(
|
||||
onOpenNavigation = onOpenMenu,
|
||||
onSort = viewModel::setSort,
|
||||
onToggleCoverStyle = { CoverArtStyle.set(!CoverArtStyle.use3d.value) },
|
||||
onChooseBackground = { backgroundPicker.launch(arrayOf("image/*", "video/*")) },
|
||||
onChooseBackground = { backgroundPicker.launch(arrayOf("image/*")) },
|
||||
onClearBackground = LibraryBackground::clear,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import com.armsx2.runtime.MainActivityRuntime
|
||||
|
||||
/**
|
||||
* Optional user-chosen library background (#9). Stores a persisted content URI;
|
||||
* when unset the library falls back to the bundled default XMB-wave video (see
|
||||
* HomeScreen / [defaultBackgroundVideoUri]). The user can pick a still image,
|
||||
* an animated GIF/WebP (Coil handles those), or an MP4/other video (played via
|
||||
* [VideoBackground]); `clear()` reverts to the default video.
|
||||
* when unset the library falls back to the bundled default XMB-wave still image
|
||||
* (R.drawable.library_bg_xmb, drawn in HomeScreen). The user can pick a still image
|
||||
* or an animated GIF/WebP (Coil handles both); `clear()` reverts to the default.
|
||||
* (The background used to be a looping MP4 but that hurt performance, so it's a
|
||||
* static image now.)
|
||||
*/
|
||||
object LibraryBackground {
|
||||
private const val PREF = "library.background.uri"
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
package com.armsx2.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.armsx2.R
|
||||
|
||||
/** Resource URI for the bundled default XMB-wave library background video. */
|
||||
fun defaultBackgroundVideoUri(context: Context): Uri =
|
||||
Uri.parse("android.resource://${context.packageName}/${R.raw.xmb_wave}")
|
||||
|
||||
/**
|
||||
* True if the URI points at a video we should play as a moving background, vs a
|
||||
* still image (or animated GIF/WebP) that Coil handles. Prefers the resolver
|
||||
* MIME type, falls back to the file extension for providers that don't report one.
|
||||
*/
|
||||
fun isVideoBackground(context: Context, uriString: String): Boolean {
|
||||
val uri = runCatching { Uri.parse(uriString) }.getOrNull() ?: return false
|
||||
runCatching { context.contentResolver.getType(uri) }.getOrNull()?.let { mime ->
|
||||
if (mime.startsWith("video/")) return true
|
||||
if (mime.startsWith("image/")) return false
|
||||
}
|
||||
val path = uriString.substringBefore('?').lowercase()
|
||||
return path.endsWith(".mp4") || path.endsWith(".m4v") || path.endsWith(".webm") ||
|
||||
path.endsWith(".mkv") || path.endsWith(".3gp") || path.endsWith(".mov")
|
||||
}
|
||||
|
||||
/**
|
||||
* Looping, muted, centre-cropped video background. Mirrors the boot-splash
|
||||
* VideoView pattern but uses a TextureView so it can crop-to-fill (VideoView only
|
||||
* letterboxes) and sit behind Compose content. The MediaPlayer is released when
|
||||
* this leaves composition (e.g. a game launches), so no codec is held while a
|
||||
* title runs. Errors are swallowed — on failure the app backdrop shows through.
|
||||
*
|
||||
* Recreate the whole composable (wrap the call in `key(uri)`) when the URI
|
||||
* changes; the AndroidView factory only runs once.
|
||||
*/
|
||||
@Composable
|
||||
fun VideoBackground(uri: Uri, modifier: Modifier = Modifier) {
|
||||
val player = remember { MediaPlayer() }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
runCatching { player.stop() }
|
||||
runCatching { player.release() }
|
||||
}
|
||||
}
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { ctx ->
|
||||
TextureView(ctx).apply {
|
||||
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(st: SurfaceTexture, width: Int, height: Int) {
|
||||
runCatching {
|
||||
player.reset()
|
||||
player.setDataSource(ctx, uri)
|
||||
player.setSurface(Surface(st))
|
||||
player.isLooping = true
|
||||
player.setVolume(0f, 0f)
|
||||
player.setOnVideoSizeChangedListener { _, vw, vh -> applyCenterCrop(this@apply, vw, vh) }
|
||||
player.setOnPreparedListener { it.start() }
|
||||
player.setOnErrorListener { _, _, _ -> true }
|
||||
player.prepareAsync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, width: Int, height: Int) {
|
||||
applyCenterCrop(this@apply, player.videoWidth, player.videoHeight)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
|
||||
runCatching { player.stop() }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale the TextureView content so the video covers the whole view (centre-crop),
|
||||
* matching ContentScale.Crop for still images. TextureView stretches the video to
|
||||
* its bounds by default; we scale the over-fitting axis back up around the centre.
|
||||
*/
|
||||
private fun applyCenterCrop(view: TextureView, videoW: Int, videoH: Int) {
|
||||
val vw = view.width
|
||||
val vh = view.height
|
||||
if (videoW <= 0 || videoH <= 0 || vw == 0 || vh == 0) return
|
||||
val viewAspect = vw.toFloat() / vh.toFloat()
|
||||
val videoAspect = videoW.toFloat() / videoH.toFloat()
|
||||
val scaleX: Float
|
||||
val scaleY: Float
|
||||
if (videoAspect > viewAspect) {
|
||||
// Video is wider than the view: fill height, let width overflow (cropped).
|
||||
scaleX = videoAspect / viewAspect
|
||||
scaleY = 1f
|
||||
} else {
|
||||
// Video is taller than the view: fill width, let height overflow (cropped).
|
||||
scaleX = 1f
|
||||
scaleY = viewAspect / videoAspect
|
||||
}
|
||||
val matrix = Matrix()
|
||||
matrix.setScale(scaleX, scaleY, vw / 2f, vh / 2f)
|
||||
view.setTransform(matrix)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
Reference in New Issue
Block a user