This commit is contained in:
2025-08-26 18:24:06 +09:00
parent 87fa624983
commit 57961d84de
572 changed files with 148866 additions and 252 deletions
@@ -1,7 +1,5 @@
package bums.lunatic.launcher.wall
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import android.os.Handler
import android.os.HandlerThread
@@ -10,104 +8,27 @@ import android.view.SurfaceHolder
import java.io.File
class MyWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine = NativeRenderEngine()
inner class NativeRenderEngine : Engine() {
private lateinit var holder: SurfaceHolder
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
private var running = false
private var nativeRenderer: NativeRenderer? = null
private var mediaFiles: List<File> = emptyList()
private var currentBitmap: Bitmap? = null
private var currentBitmapPixels: IntArray? = null
private var currentBitmapWidth = 0
private var currentBitmapHeight = 0
private val frameDelayMs = 16L // 약 60fps
private var screenWidth = 0
private var screenHeight = 0
private var renderWidth = 0f
private var renderHeight = 0f
private var maxOffset = 0f
private var moveXAxis = false
private var moveYAxis = false
private var offsetX = 0f
private var offsetY = 0f
private var movingForward = true
private var fadeAlpha = 1f
private val fadeDuration = 3000L // 3초
private val imageDisplayDuration = 20_000L // 20초
private var currentMediaStartTime = 0L
private val frameDelayMs = 16L
private val videoExtensions = listOf("mp4", "mkv", "avi")
private val imageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
private val renderRunnable = object : Runnable {
override fun run() {
if (!running) return
val now = System.currentTimeMillis()
val mediaElapsedMs = now - currentMediaStartTime
// 이동 애니메이션 계산
val travelDistance = maxOffset
val speed = if (travelDistance > 0f) (travelDistance * 2) / imageDisplayDuration.toFloat() else 0f
val deltaOffset = speed * frameDelayMs
if (moveXAxis) {
if (movingForward) {
offsetX += deltaOffset
if (offsetX >= maxOffset) offsetX = maxOffset.also { movingForward = false }
} else {
offsetX -= deltaOffset
if (offsetX <= 0f) offsetX = 0f.also { movingForward = true }
}
offsetY = 0f
} else if (moveYAxis) {
if (movingForward) {
offsetY += deltaOffset
if (offsetY >= maxOffset) offsetY = maxOffset.also { movingForward = false }
} else {
offsetY -= deltaOffset
if (offsetY <= 0f) offsetY = 0f.also { movingForward = true }
}
offsetX = 0f
} else {
offsetX = 0f
offsetY = 0f
}
// 페이드 인/아웃 계산
fadeAlpha = if (mediaElapsedMs >= imageDisplayDuration - fadeDuration) {
val elapsed = mediaElapsedMs - (imageDisplayDuration - fadeDuration)
1f - (elapsed.toFloat() / fadeDuration)
} else 1f
if (fadeAlpha < 0f) fadeAlpha = 0f
// 비트맵에서 픽셀 추출
currentBitmap?.let { bmp ->
if (currentBitmapPixels == null || currentBitmapWidth != bmp.width || currentBitmapHeight != bmp.height) {
currentBitmapWidth = bmp.width
currentBitmapHeight = bmp.height
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
}
bmp.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
}
// 네이티브 렌더 호출
currentBitmapPixels?.let { pixels ->
nativeRenderer?.nativeRender(pixels, currentBitmapWidth, currentBitmapHeight, offsetX, offsetY, fadeAlpha)
}
// 20초 후 미디어 교체
if (mediaElapsedMs >= imageDisplayDuration) {
loadNextMedia()
}
nativeRenderer?.nativeRender()
handler.postDelayed(this, frameDelayMs)
}
}
@@ -121,19 +42,10 @@ class MyWallpaperService : WallpaperService() {
super.onSurfaceCreated(holder)
nativeRenderer = NativeRenderer()
nativeRenderer?.nativeInit(holder.surface)
handlerThread = HandlerThread("NativeRenderThread").apply { start() }
handler = Handler(handlerThread.looper)
running = true
val wm = getSystemService(WINDOW_SERVICE) as android.view.WindowManager
val metrics = android.util.DisplayMetrics()
wm.defaultDisplay.getMetrics(metrics)
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
loadMediaFiles()
handler.post(renderRunnable)
}
@@ -145,97 +57,17 @@ class MyWallpaperService : WallpaperService() {
super.onSurfaceDestroyed(holder)
}
private var mediaFiles: List<File> = emptyList()
private var currentIndex = 0
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallpapers")
val extensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
val supportedExtensions = videoExtensions + imageExtensions
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
mediaDir.listFiles()?.filter { extensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
mediaDir.listFiles()?.filter { supportedExtensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
} else {
emptyList()
}
loadNextMedia()
}
private fun loadNextMedia() {
if (mediaFiles.isEmpty()) return
val file = mediaFiles[currentIndex]
currentIndex = (currentIndex + 1) % mediaFiles.size
loadBitmapFromFile(file)
currentMediaStartTime = System.currentTimeMillis()
offsetX = 0f
offsetY = 0f
movingForward = true
fadeAlpha = 1f
setupScaling(currentBitmapWidth, currentBitmapHeight)
}
private fun loadBitmapFromFile(file: File) {
val originalBitmap = BitmapFactory.decodeFile(file.absolutePath)
originalBitmap?.let {
currentBitmap?.recycle()
setupScaling(it.width, it.height)
val scaledWidth = renderWidth.toInt().coerceAtLeast(1)
val scaledHeight = renderHeight.toInt().coerceAtLeast(1)
val scaledBitmap = Bitmap.createScaledBitmap(it, scaledWidth, scaledHeight, true)
currentBitmap = scaledBitmap
currentBitmapWidth = scaledWidth
currentBitmapHeight = scaledHeight
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
scaledBitmap.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
if (it != scaledBitmap) {
it.recycle()
}
}
}
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
if (screenWidth == 0 || screenHeight == 0) return
val isLandscape = mediaWidth > mediaHeight
var scale = if (isLandscape) screenHeight.toFloat() / mediaHeight else screenWidth.toFloat() / mediaWidth
var targetWidth = mediaWidth * scale
var targetHeight = mediaHeight * scale
if (targetWidth < screenWidth) {
val scaleX = screenWidth.toFloat() / mediaWidth
if (scaleX > scale) {
scale = scaleX
targetWidth = mediaWidth * scale
targetHeight = mediaHeight * scale
}
}
if (targetHeight < screenHeight) {
val scaleY = screenHeight.toFloat() / mediaHeight
if (scaleY > scale) {
scale = scaleY
targetWidth = mediaWidth * scale
targetHeight = mediaHeight * scale
}
}
renderWidth = targetWidth
renderHeight = targetHeight
val diffWidth = renderWidth - screenWidth
val diffHeight = renderHeight - screenHeight
moveXAxis = diffWidth > 1f
moveYAxis = diffHeight > 1f
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
val mediaPaths = mediaFiles.map { it.absolutePath }
nativeRenderer?.nativeSetMediaList(mediaPaths.toTypedArray())
}
}
}
@@ -3,20 +3,16 @@ package bums.lunatic.launcher.wall
import android.view.Surface
class NativeRenderer {
companion object {
init {
System.loadLibrary("native-renderer")
System.loadLibrary("native_renderer")
}
}
external fun nativeInit(surface: Surface)
external fun nativeRender(
pixels: IntArray,
width: Int,
height: Int,
offsetX: Float,
offsetY: Float,
fadeAlpha: Float
)
external fun nativeInit(surface: Surface?): Boolean
external fun nativeDestroy()
external fun nativeRender()
external fun nativeSetMediaList(paths: Array<String?>?)
}