From ee7cee76381439576bc70be6e80e607086d77507 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 24 Jul 2025 18:34:57 +0900 Subject: [PATCH] ... --- .../launcher/wall/MyWallpaperService.kt | 133 +++++++++++++----- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index 270370ac..00265ff3 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.Paint +import android.graphics.RectF import android.media.MediaCodec import android.media.MediaExtractor import android.media.MediaFormat @@ -13,7 +14,9 @@ import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicYuvToRGB import android.renderscript.Type import android.service.wallpaper.WallpaperService +import android.util.DisplayMetrics import android.view.SurfaceHolder +import android.view.WindowManager import bums.lunatic.launcher.R @@ -26,14 +29,33 @@ class MyWallpaperService : WallpaperService() { lateinit var holder: SurfaceHolder private var renderThread: RenderThread? = null + private var screenWidth: Int = 0 + private var screenHeight: Int = 0 + override fun onCreate(surfaceHolder: SurfaceHolder) { super.onCreate(surfaceHolder) this.holder = surfaceHolder + val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager + val metrics = DisplayMetrics() + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + // Android 11 이상 권장 방법 + val display = wm.currentWindowMetrics + // Insets(네비게이션바 등 제외 영역 계산 가능), 필요한 경우 처리 + val bounds = display.bounds + screenWidth = bounds.width() + screenHeight = bounds.height() + } else { + // 하위 버전 처리 방법 + @Suppress("DEPRECATION") + wm.defaultDisplay.getMetrics(metrics) + screenWidth = metrics.widthPixels + screenHeight = metrics.heightPixels + } } override fun onSurfaceCreated(holder: SurfaceHolder) { super.onSurfaceCreated(holder) - renderThread = RenderThread(holder, getApplicationContext()) + renderThread = RenderThread(holder, getApplicationContext(), screenWidth, screenHeight) renderThread?.start() } @@ -46,7 +68,9 @@ class MyWallpaperService : WallpaperService() { } class RenderThread( private val holder: SurfaceHolder, - private val context: Context + private val context: Context, + private val screenWidth: Int, + private val screenHeight: Int ) : Thread() { private var running = true @@ -59,14 +83,28 @@ class RenderThread( private var rs: RenderScript? = null private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null - private var width = 0 - private var height = 0 + private var videoWidth = 0 + private var videoHeight = 0 + + // 왕복 이동 관련 변수 + private var offset = 0f + private var direction = 1 + private val speed = 2.5f // 픽셀/프레임, 효과 속도 조절 + + // 비율 맞춤 렌더 사이즈와 경계 + private var renderWidth = 0f + private var renderHeight = 0f + private var renderStartX = 0f + private var renderStartY = 0f + private var maxOffset = 0f + private var moveXAxis = false override fun run() { try { // 1. MediaExtractor 초기화 extractor = MediaExtractor().apply { - val afd = context.resources.openRawResourceFd(R.raw.sample) + // 영상 소스 위치에 따라 아래 setDataSource 예시를 변경하십시오. + val afd = context.resources.openRawResourceFd(R.raw.sample) // ex) res/raw/sample.mp4 setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) afd.close() for (i in 0 until trackCount) { @@ -75,19 +113,16 @@ class RenderThread( if (mime.startsWith("video/")) { videoTrackIndex = i selectTrack(i) - width = format.getInteger(MediaFormat.KEY_WIDTH) - height = format.getInteger(MediaFormat.KEY_HEIGHT) + videoWidth = format.getInteger(MediaFormat.KEY_WIDTH) + videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT) break } } } - if (videoTrackIndex < 0) { - // 비디오 트랙 없음 종료 - return - } + if (videoTrackIndex < 0) return - // 2. MediaCodec 초기화 (Surface가 아닌 직접 버퍼 받을 것) + // 2. MediaCodec 초기화 extractor?.getTrackFormat(videoTrackIndex)?.let { format -> codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply { configure(format, null, null, 0) @@ -99,8 +134,26 @@ class RenderThread( rs = RenderScript.create(context) yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + // 4. 비율 맞춤 Scaling 및 이동 관련 변수 설정 + val scale = if (videoWidth > videoHeight) { + // 가로가 더 긴 경우: 화면 세로에 맞춤 + screenHeight.toFloat() / videoHeight + } else { + // 세로가 더 긴 경우: 화면 가로에 맞춤 + screenWidth.toFloat() / videoWidth + } + renderWidth = videoWidth * scale + renderHeight = videoHeight * scale + renderStartX = (screenWidth - renderWidth) / 2f + renderStartY = (screenHeight - renderHeight) / 2f + + moveXAxis = renderWidth > screenWidth + maxOffset = if (moveXAxis) renderWidth - screenWidth else if (renderHeight > screenHeight) renderHeight - screenHeight else 0f + var isEOS = false + while (running && !isInterrupted) { + // 5. MediaCodec 프레임 디코딩 if (!isEOS) { val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1 if (inputBufferIndex >= 0) { @@ -109,16 +162,11 @@ class RenderThread( val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1 if (sampleSize < 0) { codec?.queueInputBuffer( - inputBufferIndex, - 0, - 0, - 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM - ) + inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) isEOS = true } else { - val presentationTimeUs = extractor?.sampleTime ?: 0L - codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0) + val pts = extractor?.sampleTime ?: 0L + codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0) extractor?.advance() } } @@ -132,42 +180,51 @@ class RenderThread( val yuvData = ByteArray(bufferInfo.size) outputBuffer.get(yuvData) - // 4. RenderScript로 YUV → Bitmap 변환 - val bitmap = convertYUVToBitmap(yuvData, width, height) + val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight) - // 5. Canvas에 그리기 + // 6. 왕복 이동 애니메이션 offset 계산 + if (maxOffset > 0f) { + offset += direction * speed + if (offset < 0f) { offset = 0f; direction = 1 } + if (offset > maxOffset) { offset = maxOffset; direction = -1 } + } + + // 7. Canvas에 그리고 unlock val canvas = holder.lockCanvas() if (canvas != null) { - canvas.drawBitmap(bitmap, 0f, 0f, null) - - // 오버레이 예: 텍스트 그리기 + val drawX = if (moveXAxis) renderStartX - offset else renderStartX + val drawY = if (!moveXAxis) renderStartY - offset else renderStartY + val dstRect = RectF( + drawX, drawY, + drawX + renderWidth, drawY + renderHeight + ) + canvas.drawColor(Color.BLACK) + canvas.drawBitmap(bitmap, null, dstRect, null) + // 오버레이 예시 val paint = Paint().apply { color = Color.WHITE textSize = 40f + setShadowLayer(4f, 2f, 2f, Color.DKGRAY) } - canvas.drawText("Live Wallpaper Overlay", 50f, 50f, paint) - + canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paint) holder.unlockCanvasAndPost(canvas) } - outputBuffer.clear() } codec?.releaseOutputBuffer(outputBufferIndex, false) } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - // format 변경 시 처리할 것 (필요시) + // 필요하다면 포맷 변경 처리 } - - // 프레임 타이밍 조절 (예: 30fps 제한) - sleep(33) + sleep(1000L/45L) // 약 30fps } } catch (e: Exception) { e.printStackTrace() } finally { - // 리소스 해제 - codec?.stop() - codec?.release() - extractor?.release() - rs?.destroy() + try { + codec?.stop(); codec?.release() + extractor?.release() + rs?.destroy() + } catch (e: Exception) { } } }