This commit is contained in:
lunaticbum 2025-08-25 18:17:27 +09:00
parent e646a72418
commit 1e06d82015
7 changed files with 543 additions and 266 deletions

View File

@ -104,8 +104,6 @@ import java.util.Date
open class LauncherActivity : CommonActivity() {
private lateinit var binding: LauncherActivityBinding
private lateinit var settingsPrefs: SharedPreferences
// lateinit var viewPager: ViewPager2
companion object {
private var sRuntime: GeckoRuntime? = null
@ -143,10 +141,6 @@ open class LauncherActivity : CommonActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Blog.LOGE("onConfigurationChanged Configuration >> ${newConfig}")
Blog.LOGE("onConfigurationChanged newConfig?.screenWidthDp >> ${newConfig?.screenWidthDp}")
Blog.LOGE("onConfigurationChanged newConfig?.screenHeightDp >> ${newConfig?.screenHeightDp}")
isOpendFold = (newConfig.screenWidthDp * 1.1f) > newConfig.screenHeightDp
val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)
binding.root.setPointerIcon(nullCursor)
@ -289,25 +283,17 @@ open class LauncherActivity : CommonActivity() {
.setRssList(arrayListOf<String>().apply {
this.addAll(jjjj)})
.setRssId(origin)
// .webViewDesktopMode(true)
.showIconClose(true).showIconBack(false).showProgressBar(true).backPressToClose(false).webViewMixedContentMode(1)
.show(origin)
}
}
}
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
// Blog.LOGE("dispatchKeyEvent ev?.device?.name >>> ${ev?.device?.name}")
/// || ev?.device?.name?.contains("SM-031N Mouse") == true
if (ev?.device?.name?.contains("BLE-M3") == true) {
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}")
ev?.action?.let { action ->
when(action) {
MotionEvent.ACTION_HOVER_ENTER -> {
// if (onExit) {
// onExit = false
// return true
// }
return false
}
MotionEvent.ACTION_HOVER_MOVE ->{
@ -399,8 +385,6 @@ open class LauncherActivity : CommonActivity() {
}
}
}
// BLog.LOGE("onNewIntent intent?.dataString >> ${intent?.dataString}")
// BLog.LOGE("onNewIntent intent?.data >> ${intent?.data}")
} else if (intent?.action == Intent.ACTION_WEB_SEARCH) {
openWithIntent(intent)
} else {
@ -431,13 +415,6 @@ open class LauncherActivity : CommonActivity() {
}
}
}
// android.intent.extra.EXTRA_START_REASON :: value >> startDockOrHome
// binding.viewPager.invalidate()
// binding.viewPager.post {
// binding.viewPager?.adapter?.notifyDataSetChanged()
// }
super.onNewIntent(intent)
}
@ -462,7 +439,7 @@ open class LauncherActivity : CommonActivity() {
binding.currentAddress.text = str
}
@SuppressLint("NewApi", "MissingPermission")
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
@ -484,34 +461,22 @@ open class LauncherActivity : CommonActivity() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
lActivity = this
// DynamicColors.applyToActivityIfAvailable(this)
settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
DynamicColors.applyToActivityIfAvailable(this)
binding = LauncherActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
HeadsetActionButtonReceiver.register(this)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
// 시스템바 인셋 가져오기 (상단 상태바 + 하단 네비게이션바)
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// 뷰에 패딩 적용 (겹치지 않도록)
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
// 변경된 인셋 반환
WindowInsetsCompat.CONSUMED
}
/* handle navigation back events */
handleBackPress()
updateLocationService()
showContents(binding.feeds.id)
binding.floatingActionMenu.setOnTouchListener { v,e->
binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent ->
if (binding.floatingActionMenu.isOpened) {
binding.floatingActionMenu.close(true)
return@setOnTouchListener true

View File

@ -319,6 +319,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
override fun onPageStart(session: GeckoSession, url: String) {
super.onPageStart(session, url)
binding.progress.visibility = VISIBLE
if (aceptUrl(url)) {
@ -1263,6 +1264,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
activity?.runOnUiThread {
view.text = contents
view.visibility = VISIBLE
binding.menuWeb.visibility = View.GONE
}
view.forceUpdateUI()
lastedUrl?.let {

View File

@ -171,6 +171,7 @@ class Novels : BaseToki(), PagedTextViewInterface {
if (binding.pagedLayer.isVisible) {
binding.pagedLayer.visibility = GONE
}
binding.menuWeb.visibility = View.VISIBLE
}
}

View File

@ -197,7 +197,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
if (w != oldw || oldh != h) {
postDelayed(Runnable {
layoutChange(w > (h * 0.7f))
Blog.LOGD(log = "onSizeChanged>> ${w} ${h} ${h * 0.7}")
},20)
}else {
@ -216,13 +216,15 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
}
hiddenTextView?.visibility = GONE
}
var defaultAlpha = 171
fun setColorStyle(colors : Array<String>) {
setBackgroundColor(Color.parseColor(colors.get(1)))
mainTextView?.setBackgroundColor(Color.parseColor(colors.get(1)))
sencondTextView?.setBackgroundColor(Color.parseColor(colors.get(1)))
mainTextView?.setTextColor(Color.parseColor(colors.get(0)))
sencondTextView?.setTextColor(Color.parseColor(colors.get(0)))
var bgColor = Color.parseColor(colors.get(1))
val newColorInt = (bgColor and 0x00FFFFFF) or (defaultAlpha shl 24)
setBackgroundColor(newColorInt)
mainTextView?.setBackgroundColor(newColorInt)
sencondTextView?.setBackgroundColor(newColorInt)
mainTextView?.setTextColor(Color.parseColor("#FFFFFF"))
sencondTextView?.setTextColor(Color.parseColor("#FFFFFF"))
}
// fun setPagedTextViewInterface(pagedTextViewInterface: PagedTextViewInterface) = hiddenTextView?.setPagedTextViewInterface(pagedTextViewInterface)

View File

@ -2,6 +2,8 @@ package bums.lunatic.launcher.wall
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
@ -10,6 +12,9 @@ import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Environment
import android.os.Handler
import android.os.HandlerThread
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
@ -21,254 +26,495 @@ import android.view.SurfaceHolder
import android.view.WindowManager
import bums.lunatic.launcher.R
import bums.lunatic.launcher.utils.Blog
import java.nio.ByteBuffer
import java.io.File
import kotlin.random.Random
class MyWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine {
return VideoEngine()
}
inner class VideoEngine : Engine() {
lateinit var holder: SurfaceHolder
private var renderThread: RenderThread? = null
inner class VideoEngine : WallpaperService.Engine() {
private lateinit var holder: SurfaceHolder
private lateinit var renderLooper: RenderLooper
private lateinit var handlerThread: HandlerThread
private var screenWidth: Int = 0
private var screenHeight: Int = 0
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
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()
screenWidth = display.bounds.width()
screenHeight = display.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(), screenWidth, screenHeight)
renderThread?.start()
handlerThread = HandlerThread("RenderHandlerThread")
handlerThread.start()
renderLooper = RenderLooper(this@MyWallpaperService, holder, screenWidth, screenHeight, handlerThread)
renderLooper.start()
}
override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
super.onSurfaceDestroyed(holder)
renderThread?.interrupt()
renderThread = null
renderLooper.stop()
handlerThread.quitSafely()
}
override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
super.onSurfaceChanged(holder, format, width, height)
screenWidth = width
screenHeight = height
renderLooper.updateScreenSize(width, height)
}
}
}
class RenderThread(
private val holder: SurfaceHolder,
interface Renderer {
fun render(canvas: Canvas)
fun release()
fun updateScreenSize(width: Int, height: Int)
fun resetOffset()
fun update()
fun isCycleCompleted(): Boolean
}
class ImageRenderer(
private val context: Context,
private val screenWidth: Int,
private val screenHeight: Int
) : Thread() {
private var bitmap: Bitmap,
private var screenWidth: Int,
private var screenHeight: Int
) : Renderer {
private var running = true
private var extractor: MediaExtractor? = null
private var codec: MediaCodec? = null
private var videoTrackIndex: Int = -1
private val bufferInfo = MediaCodec.BufferInfo()
private var rs: RenderScript? = null
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
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
private var moveYAxis = false
private var maxOffset = 0f
private var offset = 0f
private var direction = 1
private val speed = 2.5f
private val holdDurationMs = 10_000L
private var noMoveStartTime: Long = 0L
private var moveCycleCompleted = false
private val paintOverlay = Paint().apply {
color = Color.WHITE
textSize = 40f
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
}
init {
setupScaling(bitmap.width, bitmap.height)
}
override fun render(canvas: Canvas) {
updateOffset()
val drawX = if (moveXAxis) renderStartX - offset else renderStartX
val drawY = if (moveYAxis) renderStartY - offset else renderStartY
val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight)
canvas.drawColor(Color.BLACK)
canvas.drawBitmap(bitmap, null, dstRect, null)
// canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay)
}
override fun release() {
bitmap.recycle()
}
override fun updateScreenSize(width: Int, height: Int) {
screenWidth = width
screenHeight = height
setupScaling(bitmap.width, bitmap.height)
resetOffset()
}
override fun resetOffset() {
offset = 0f
direction = 1
noMoveStartTime = 0L
moveCycleCompleted = false
}
override fun update() {
if (!moveXAxis && !moveYAxis) {
val now = System.currentTimeMillis()
if (noMoveStartTime == 0L) noMoveStartTime = now
else if (now - noMoveStartTime >= holdDurationMs) {
noMoveStartTime = 0L
moveCycleCompleted = true
}
} else {
if (moveCycleCompleted) {
moveCycleCompleted = false
}
noMoveStartTime = 0L
}
}
override fun isCycleCompleted(): Boolean = moveCycleCompleted
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
val isLandscape = mediaWidth > mediaHeight
var scale = if (isLandscape) {
// 가로가 긴 경우 => 세로 기준 스케일링
screenHeight.toFloat() / mediaHeight
} else {
// 세로가 긴 경우 => 가로 기준 스케일링
screenWidth.toFloat() / mediaWidth
}
var targetWidth = mediaWidth * scale
var targetHeight = mediaHeight * scale
// 2차 보정: 스케일 후 크기가 기기 해상도보다 작으면 크기 맞춤
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
renderStartX = 0f
renderStartY = 0f
val diffWidth = renderWidth - screenWidth
val diffHeight = renderHeight - screenHeight
moveXAxis = diffWidth > 1f
moveYAxis = diffHeight > 1f
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
}
private fun updateOffset() {
if (moveXAxis || moveYAxis) {
offset += direction * speed
if (offset < 0f) {
offset = 0f
direction = 1
}
if (offset > maxOffset) {
offset = maxOffset
direction = -1
}
}
}
}
class VideoRenderer(
private val context: Context,
private val extractor: MediaExtractor,
private var screenWidth: Int,
private var screenHeight: Int
) : Renderer, Runnable {
private var codec: MediaCodec? = null
private var videoTrackIndex: Int = -1
private val bufferInfo = MediaCodec.BufferInfo()
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 moveXAxis = false
private var moveYAxis = false
private var maxOffset = 0f
private var running = true
private var isEOS = false
private var keyFrameRate = 60
private var rs: RenderScript? = null
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
private var bitmap: Bitmap? = null
private val paintOverlay = Paint().apply {
color = Color.WHITE
textSize = 40f
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
}
private var noMoveStartTime: Long = 0L
private val holdDurationMs = 10_000L
private var moveCycleCompleted = false
private val lock = Object()
init {
setupDecoder()
}
override fun render(canvas: Canvas) {
synchronized(lock) {
if (bitmap != null) {
val drawX = if (moveXAxis) renderStartX - offset else renderStartX
val drawY = if (moveYAxis) renderStartY - offset else renderStartY
val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight)
canvas.drawColor(Color.BLACK)
canvas.drawBitmap(bitmap!!, null, dstRect, null)
// canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay)
}
}
}
override fun release() {
codec?.stop()
codec?.release()
extractor.release()
rs?.destroy()
bitmap?.recycle()
running = false
}
override fun updateScreenSize(width: Int, height: Int) {
synchronized(lock) {
screenWidth = width
screenHeight = height
setupScaling(videoWidth, videoHeight)
resetOffset()
}
}
override fun resetOffset() {
synchronized(lock) {
offset = 0f
direction = 1
noMoveStartTime = 0L
moveCycleCompleted = false
}
}
override fun update() {
synchronized(lock) {
if (!moveXAxis && !moveYAxis) {
val now = System.currentTimeMillis()
if (noMoveStartTime == 0L) noMoveStartTime = now
else if (now - noMoveStartTime >= holdDurationMs) {
noMoveStartTime = 0L
moveCycleCompleted = true
}
} else {
if (moveCycleCompleted) {
moveCycleCompleted = false
}
noMoveStartTime = 0L
}
}
}
override fun isCycleCompleted(): Boolean = moveCycleCompleted
operator fun invoke() = run()
override fun run() {
try {
// 1. MediaExtractor 초기화
extractor = MediaExtractor().apply {
// 영상 소스 위치에 따라 아래 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) {
val format = getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
val inputBuffersTimeoutUs = 10000L
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE)
} else {
keyFrameRate = 60
}
if (mime.startsWith("video/")) {
videoTrackIndex = i
selectTrack(i)
while (running && !Thread.currentThread().isInterrupted) {
if (isEOS) {
videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
break
}
continue
}
}
if (videoTrackIndex < 0) return
// 2. MediaCodec 초기화
extractor?.getTrackFormat(videoTrackIndex)?.let { format ->
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
configure(format, null, null, 0)
start()
}
}
// 3. RenderScript 초기화
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 = 0f;//(screenWidth - renderWidth) / 2f
renderStartY = 0f;//(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) {
val inputBuffer = codec?.getInputBuffer(inputBufferIndex)
if (inputBuffer != null) {
val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1
if (sampleSize < 0) {
// codec?.queueInputBuffer(
// inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
// isEOS = true
extractor?.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
isEOS = false // false로 재설정해서 계속 받게
continue
} else {
val pts = extractor?.sampleTime ?: 0L
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
extractor?.advance()
}
val inputBufferIndex = codec?.dequeueInputBuffer(inputBuffersTimeoutUs) ?: -1
if (inputBufferIndex >= 0) {
val inputBuffer = codec?.getInputBuffer(inputBufferIndex)
if (inputBuffer != null) {
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
codec?.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEOS = true
} else {
val pts = extractor.sampleTime
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
extractor.advance()
}
}
}
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, inputBuffersTimeoutUs) ?: -1
if (outputBufferIndex >= 0) {
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
Blog.LOGE("codec.outputFormat >>> ${codec?.outputFormat}")
if (bufferInfo.size > 0 && outputBuffer != null) {
val format = codec?.outputFormat
val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: /*기본값*/0
val stride = format?.getInteger(MediaFormat.KEY_STRIDE) ?: videoWidth
val sliceHeight = format?.getInteger(MediaFormat.KEY_SLICE_HEIGHT) ?: videoHeight
// outputBuffer.get(yuvData)
val expectedSize = videoWidth * videoHeight * 3 / 2
val yuvData = ByteArray(expectedSize)
val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: 0
val rawYUV = ByteArray(bufferInfo.size)
outputBuffer.get(rawYUV)
outputBuffer.position(0)
// 포맷별 변환
val nv21Data = when (colorFormat) {
COLOR_FormatYUV420SemiPlanar -> nv12ToNv21(rawYUV, videoWidth, videoHeight)
COLOR_FormatYUV420Planar -> i420ToNv21(rawYUV, videoWidth, videoHeight)
else -> rawYUV // 이미 NV21 등
else -> rawYUV
}
// NV21 데이터 → Bitmap
val bitmap = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
// outputBuffer.get(yuvData, 0, minOf(bufferInfo.size, expectedSize))
// getYUVDataWithStride(outputBuffer, videoWidth, videoHeight, stride, height)
// val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight)
val bmp = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
synchronized(lock) {
bitmap?.recycle()
bitmap = bmp
}
// 6. 왕복 이동 애니메이션 offset 계산
// Blog.LOGE("maxOffset >>> $maxOffset , offset >>> $offset , direction >> $direction , keyFrameRate >>> $keyFrameRate")
if (maxOffset > 0f) {
if (moveXAxis || moveYAxis) {
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) {
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)
if (offset < 0f) {
offset = 0f
direction = 1
}
if (moveXAxis && offset > maxOffset) {
offset = maxOffset
direction = -1
} else if (moveYAxis && offset > maxOffset) {
offset = maxOffset
direction = -1
}
noMoveStartTime = 0L
} else {
val now = System.currentTimeMillis()
if (noMoveStartTime == 0L) noMoveStartTime = now
else if (now - noMoveStartTime >= holdDurationMs) {
noMoveStartTime = 0L
moveCycleCompleted = true
}
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) {
// 필요하다면 포맷 변경 처리
// 필요 처리
}
sleep(1000L / keyFrameRate) // 약 30fps
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
codec?.stop(); codec?.release()
extractor?.release()
rs?.destroy()
} catch (e: Exception) { }
}
}
private fun setupDecoder() {
for (i in 0 until extractor.trackCount) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE)
} else {
keyFrameRate = 60
}
if (mime.startsWith("video/")) {
videoTrackIndex = i
extractor.selectTrack(i)
videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
break
}
}
if (videoTrackIndex < 0) return
codec?.stop()
codec?.release()
codec = MediaCodec.createDecoderByType(extractor.getTrackFormat(videoTrackIndex).getString(MediaFormat.KEY_MIME)!!).apply {
configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0)
start()
}
rs?.destroy()
rs = RenderScript.create(context)
yuvToRgb?.destroy()
yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
setupScaling(videoWidth, videoHeight)
resetOffset()
}
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
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
renderStartX = 0f
renderStartY = 0f
val diffWidth = renderWidth - screenWidth
val diffHeight = renderHeight - screenHeight
moveXAxis = diffWidth > 1f
moveYAxis = diffHeight > 1f
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
}
private fun convertYUVToBitmap(yuvByteArray: ByteArray, width: Int, height: Int): Bitmap {
// YUV420の場合, 바이트 수는 width * height * 3 / 2 여야 함
val minSize = width * height * 3 / 2
val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray
@ -291,33 +537,12 @@ class RenderThread(
return bitmap
}
private fun getYUVDataWithStride(
buffer: ByteBuffer,
width: Int,
height: Int,
stride: Int,
sliceHeight: Int
): ByteArray {
val yuvData = ByteArray(width * height * 3 / 2)
// Y plane 복사
for (y in 0 until height) {
buffer.position(y * stride)
buffer.get(yuvData, y * width, width)
}
// UV plane... (NV21 등 포맷 따라 별도 구현 필요)
// 이 영역은 포맷에 따라 다름, 기본은 Y만 참고!
return yuvData
}
// NV12 to NV21 변환
fun nv12ToNv21(nv12: ByteArray, width: Int, height: Int): ByteArray {
val frameSize = width * height
val nv21 = ByteArray(frameSize * 3 / 2)
// Y는 그대로 복사
System.arraycopy(nv12, 0, nv21, 0, frameSize)
// UV를 VU로 뒤집어서 복사
var i = 0
while (i < frameSize / 2 - 1) {
nv21[frameSize + i] = nv12[frameSize + i + 1]
@ -327,54 +552,135 @@ class RenderThread(
return nv21
}
// I420(YUV420Planar) to NV21 변환
fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray {
val frameSize = width * height
val qFrameSize = frameSize / 4
val nv21 = ByteArray(frameSize * 3 / 2)
// Y 복사
System.arraycopy(i420, 0, nv21, 0, frameSize)
// VU interleave
val u = frameSize
val v = frameSize + qFrameSize
for (i in 0 until qFrameSize) {
nv21[frameSize + i * 2] = i420[v + i] // V
nv21[frameSize + i * 2 + 1] = i420[u + i] // U
nv21[frameSize + i * 2] = i420[v + i]
nv21[frameSize + i * 2 + 1] = i420[u + i]
}
return nv21
}
fun i420ToNv21WithStride(src: ByteBuffer, width: Int, height: Int, stride: Int, sliceHeight: Int): ByteArray {
val frameSize = width * height
val qFrameSize = frameSize / 4
val nv21 = ByteArray(frameSize * 3 / 2)
}
// 1. Y plane 복사
for (y in 0 until height) {
src.position(y * stride)
src.get(nv21, y * width, width)
class RenderLooper(
private val context: Context,
private val holder: SurfaceHolder,
private var screenWidth: Int,
private var screenHeight: Int,
handlerThread: HandlerThread
) {
private val handler = Handler(handlerThread.looper)
@Volatile
private var running = false
private var renderer: Renderer? = null
private var mediaFiles: Array<File>? = null
private val frameDelayMs = 1000L / 33L // 약 60fps
private val renderRunnable = object : Runnable {
override fun run() {
if (!running) return
val canvas = holder.lockCanvas()
val currentRenderer = renderer
if (canvas != null && currentRenderer != null) {
synchronized(currentRenderer) {
currentRenderer.render(canvas)
}
holder.unlockCanvasAndPost(canvas)
}
renderer?.update()
if (renderer?.isCycleCompleted() == true) {
loadNextMedia()
handler.postDelayed(this, frameDelayMs)
}
handler.postDelayed(this, frameDelayMs)
}
}
// 2. U/V plane 복사, 각각 stride/2씩 적용
val uOffsetBuf = sliceHeight * stride
val vOffsetBuf = uOffsetBuf + (sliceHeight / 2) * (stride / 2)
fun start() {
running = true
initMediaFiles()
loadNextMedia()
handler.post(renderRunnable)
}
for (y in 0 until height/2) {
src.position(uOffsetBuf + y * (stride / 2))
src.get(nv21, frameSize + y * width, width/2) // 임시: U만
fun stop() {
running = false
handler.removeCallbacks(renderRunnable)
}
src.position(vOffsetBuf + y * (stride / 2))
for (x in 0 until width/2) {
// NV21 순서: V, U
val v = src.get()
val u = nv21[frameSize + y * width + x] // U
nv21[frameSize + y * width + x*2] = v
nv21[frameSize + y * width + x*2 + 1] = u
private fun initMediaFiles() {
val youtubeDLDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"wallPapers"
)
if (youtubeDLDir.exists() && youtubeDLDir.isDirectory && youtubeDLDir.listFiles().size > 2) {
val supportedImageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
val supportedVideoExtensions = listOf("mp4", "mkv", "avi")
mediaFiles = youtubeDLDir.listFiles { file ->
val ext = file.extension.lowercase()
ext in supportedImageExtensions || ext in supportedVideoExtensions
}
}
return nv21
Blog.LOGE("mediaFiles >>> ${mediaFiles?.size}")
}
}
fun loadNextMedia() {
renderer?.release()
if (mediaFiles == null || mediaFiles!!.isEmpty()) {
prepareRawVideo()
return
}
mediaFiles?.random()?.let {
loadMedia(it)
}
}
private fun loadMedia(file: File) {
val ext = file.extension.lowercase()
renderer?.resetOffset()
if (ext in listOf("jpg", "jpeg", "png", "bmp", "webp")) {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
renderer = ImageRenderer(context, bitmap, screenWidth, screenHeight)
} else {
try {
val extractor = MediaExtractor()
extractor.setDataSource(file.absolutePath)
renderer = VideoRenderer(context, extractor, screenWidth, screenHeight)
} catch (e: Exception) {
e.printStackTrace()
prepareRawVideo()
}
}
}
fun updateScreenSize(width: Int, height: Int) {
screenWidth = width
screenHeight = height
renderer?.updateScreenSize(width, height)
}
private fun prepareRawVideo() {
try {
val extractor = MediaExtractor()
val afd = context.resources.openRawResourceFd(R.raw.sample)
extractor.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
afd.close()
renderer = VideoRenderer(context, extractor, screenWidth, screenHeight)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@ -89,14 +89,14 @@
<bums.lunatic.launcher.tokiz.view.PagedTextLayout
android:id="@+id/paged_layer"
android:layout_margin="1dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:visibility="gone"
android:elevation="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/btn_setting"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_title" />
<ProgressBar

View File

@ -89,6 +89,7 @@
android:overScrollMode="never"
android:padding="@dimen/default_padding"
android:scrollbars="none"
android:background="#AB000000"
android:visibility="visible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"