...
This commit is contained in:
parent
e646a72418
commit
1e06d82015
@ -104,8 +104,6 @@ import java.util.Date
|
|||||||
open class LauncherActivity : CommonActivity() {
|
open class LauncherActivity : CommonActivity() {
|
||||||
|
|
||||||
private lateinit var binding: LauncherActivityBinding
|
private lateinit var binding: LauncherActivityBinding
|
||||||
private lateinit var settingsPrefs: SharedPreferences
|
|
||||||
// lateinit var viewPager: ViewPager2
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var sRuntime: GeckoRuntime? = null
|
private var sRuntime: GeckoRuntime? = null
|
||||||
@ -143,10 +141,6 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
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
|
isOpendFold = (newConfig.screenWidthDp * 1.1f) > newConfig.screenHeightDp
|
||||||
val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)
|
val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL)
|
||||||
binding.root.setPointerIcon(nullCursor)
|
binding.root.setPointerIcon(nullCursor)
|
||||||
@ -289,25 +283,17 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
.setRssList(arrayListOf<String>().apply {
|
.setRssList(arrayListOf<String>().apply {
|
||||||
this.addAll(jjjj)})
|
this.addAll(jjjj)})
|
||||||
.setRssId(origin)
|
.setRssId(origin)
|
||||||
// .webViewDesktopMode(true)
|
|
||||||
.showIconClose(true).showIconBack(false).showProgressBar(true).backPressToClose(false).webViewMixedContentMode(1)
|
.showIconClose(true).showIconBack(false).showProgressBar(true).backPressToClose(false).webViewMixedContentMode(1)
|
||||||
.show(origin)
|
.show(origin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
|
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) {
|
if (ev?.device?.name?.contains("BLE-M3") == true) {
|
||||||
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}")
|
Blog.LOGE("keyEvent >>>>> dispatchGenericMotionEvent ${ev}")
|
||||||
ev?.action?.let { action ->
|
ev?.action?.let { action ->
|
||||||
when(action) {
|
when(action) {
|
||||||
MotionEvent.ACTION_HOVER_ENTER -> {
|
MotionEvent.ACTION_HOVER_ENTER -> {
|
||||||
|
|
||||||
// if (onExit) {
|
|
||||||
// onExit = false
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_HOVER_MOVE ->{
|
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) {
|
} else if (intent?.action == Intent.ACTION_WEB_SEARCH) {
|
||||||
openWithIntent(intent)
|
openWithIntent(intent)
|
||||||
} else {
|
} 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)
|
super.onNewIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,7 +439,7 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
binding.currentAddress.text = str
|
binding.currentAddress.text = str
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi", "MissingPermission")
|
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -484,34 +461,22 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
|
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
lActivity = this
|
lActivity = this
|
||||||
// DynamicColors.applyToActivityIfAvailable(this)
|
DynamicColors.applyToActivityIfAvailable(this)
|
||||||
settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
|
|
||||||
|
|
||||||
|
|
||||||
binding = LauncherActivityBinding.inflate(layoutInflater)
|
binding = LauncherActivityBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
HeadsetActionButtonReceiver.register(this)
|
HeadsetActionButtonReceiver.register(this)
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||||
|
|
||||||
// 시스템바 인셋 가져오기 (상단 상태바 + 하단 네비게이션바)
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
|
||||||
// 뷰에 패딩 적용 (겹치지 않도록)
|
|
||||||
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
||||||
|
|
||||||
// 변경된 인셋 반환
|
|
||||||
WindowInsetsCompat.CONSUMED
|
WindowInsetsCompat.CONSUMED
|
||||||
}
|
}
|
||||||
|
|
||||||
/* handle navigation back events */
|
|
||||||
handleBackPress()
|
handleBackPress()
|
||||||
updateLocationService()
|
updateLocationService()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showContents(binding.feeds.id)
|
showContents(binding.feeds.id)
|
||||||
binding.floatingActionMenu.setOnTouchListener { v,e->
|
binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent ->
|
||||||
if (binding.floatingActionMenu.isOpened) {
|
if (binding.floatingActionMenu.isOpened) {
|
||||||
binding.floatingActionMenu.close(true)
|
binding.floatingActionMenu.close(true)
|
||||||
return@setOnTouchListener true
|
return@setOnTouchListener true
|
||||||
|
|||||||
@ -319,6 +319,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
|
|||||||
|
|
||||||
override fun onPageStart(session: GeckoSession, url: String) {
|
override fun onPageStart(session: GeckoSession, url: String) {
|
||||||
super.onPageStart(session, url)
|
super.onPageStart(session, url)
|
||||||
|
|
||||||
binding.progress.visibility = VISIBLE
|
binding.progress.visibility = VISIBLE
|
||||||
if (aceptUrl(url)) {
|
if (aceptUrl(url)) {
|
||||||
|
|
||||||
@ -1263,6 +1264,7 @@ abstract class BaseToki : Fragment(), PagedTextViewInterface {
|
|||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
view.text = contents
|
view.text = contents
|
||||||
view.visibility = VISIBLE
|
view.visibility = VISIBLE
|
||||||
|
binding.menuWeb.visibility = View.GONE
|
||||||
}
|
}
|
||||||
view.forceUpdateUI()
|
view.forceUpdateUI()
|
||||||
lastedUrl?.let {
|
lastedUrl?.let {
|
||||||
|
|||||||
@ -171,6 +171,7 @@ class Novels : BaseToki(), PagedTextViewInterface {
|
|||||||
if (binding.pagedLayer.isVisible) {
|
if (binding.pagedLayer.isVisible) {
|
||||||
binding.pagedLayer.visibility = GONE
|
binding.pagedLayer.visibility = GONE
|
||||||
}
|
}
|
||||||
|
binding.menuWeb.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -197,7 +197,7 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
|
|||||||
if (w != oldw || oldh != h) {
|
if (w != oldw || oldh != h) {
|
||||||
postDelayed(Runnable {
|
postDelayed(Runnable {
|
||||||
layoutChange(w > (h * 0.7f))
|
layoutChange(w > (h * 0.7f))
|
||||||
|
Blog.LOGD(log = "onSizeChanged>> ${w} ${h} ${h * 0.7}")
|
||||||
},20)
|
},20)
|
||||||
}else {
|
}else {
|
||||||
|
|
||||||
@ -216,13 +216,15 @@ class PagedTextLayout : ConstraintLayout , PagedTextGenerateInterface {
|
|||||||
}
|
}
|
||||||
hiddenTextView?.visibility = GONE
|
hiddenTextView?.visibility = GONE
|
||||||
}
|
}
|
||||||
|
var defaultAlpha = 171
|
||||||
fun setColorStyle(colors : Array<String>) {
|
fun setColorStyle(colors : Array<String>) {
|
||||||
setBackgroundColor(Color.parseColor(colors.get(1)))
|
var bgColor = Color.parseColor(colors.get(1))
|
||||||
mainTextView?.setBackgroundColor(Color.parseColor(colors.get(1)))
|
val newColorInt = (bgColor and 0x00FFFFFF) or (defaultAlpha shl 24)
|
||||||
sencondTextView?.setBackgroundColor(Color.parseColor(colors.get(1)))
|
setBackgroundColor(newColorInt)
|
||||||
mainTextView?.setTextColor(Color.parseColor(colors.get(0)))
|
mainTextView?.setBackgroundColor(newColorInt)
|
||||||
sencondTextView?.setTextColor(Color.parseColor(colors.get(0)))
|
sencondTextView?.setBackgroundColor(newColorInt)
|
||||||
|
mainTextView?.setTextColor(Color.parseColor("#FFFFFF"))
|
||||||
|
sencondTextView?.setTextColor(Color.parseColor("#FFFFFF"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun setPagedTextViewInterface(pagedTextViewInterface: PagedTextViewInterface) = hiddenTextView?.setPagedTextViewInterface(pagedTextViewInterface)
|
// fun setPagedTextViewInterface(pagedTextViewInterface: PagedTextViewInterface) = hiddenTextView?.setPagedTextViewInterface(pagedTextViewInterface)
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package bums.lunatic.launcher.wall
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
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.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||||
import android.media.MediaExtractor
|
import android.media.MediaExtractor
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
import android.renderscript.Allocation
|
import android.renderscript.Allocation
|
||||||
import android.renderscript.Element
|
import android.renderscript.Element
|
||||||
import android.renderscript.RenderScript
|
import android.renderscript.RenderScript
|
||||||
@ -21,254 +26,495 @@ import android.view.SurfaceHolder
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import bums.lunatic.launcher.R
|
import bums.lunatic.launcher.R
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import java.nio.ByteBuffer
|
import java.io.File
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class MyWallpaperService : WallpaperService() {
|
class MyWallpaperService : WallpaperService() {
|
||||||
override fun onCreateEngine(): Engine {
|
override fun onCreateEngine(): Engine {
|
||||||
return VideoEngine()
|
return VideoEngine()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class VideoEngine : Engine() {
|
inner class VideoEngine : WallpaperService.Engine() {
|
||||||
lateinit var holder: SurfaceHolder
|
private lateinit var holder: SurfaceHolder
|
||||||
private var renderThread: RenderThread? = null
|
private lateinit var renderLooper: RenderLooper
|
||||||
|
private lateinit var handlerThread: HandlerThread
|
||||||
|
|
||||||
private var screenWidth: Int = 0
|
private var screenWidth: Int = 0
|
||||||
private var screenHeight: Int = 0
|
private var screenHeight: Int = 0
|
||||||
|
|
||||||
override fun onCreate(surfaceHolder: SurfaceHolder) {
|
override fun onCreate(surfaceHolder: SurfaceHolder) {
|
||||||
super.onCreate(surfaceHolder)
|
super.onCreate(surfaceHolder)
|
||||||
this.holder = surfaceHolder
|
holder = surfaceHolder
|
||||||
val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
val metrics = DisplayMetrics()
|
val metrics = DisplayMetrics()
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
// Android 11 이상 권장 방법
|
|
||||||
val display = wm.currentWindowMetrics
|
val display = wm.currentWindowMetrics
|
||||||
// Insets(네비게이션바 등 제외 영역 계산 가능), 필요한 경우 처리
|
screenWidth = display.bounds.width()
|
||||||
val bounds = display.bounds
|
screenHeight = display.bounds.height()
|
||||||
screenWidth = bounds.width()
|
|
||||||
screenHeight = bounds.height()
|
|
||||||
} else {
|
} else {
|
||||||
// 하위 버전 처리 방법
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
wm.defaultDisplay.getMetrics(metrics)
|
wm.defaultDisplay.getMetrics(metrics)
|
||||||
screenWidth = metrics.widthPixels
|
screenWidth = metrics.widthPixels
|
||||||
screenHeight = metrics.heightPixels
|
screenHeight = metrics.heightPixels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceCreated(holder: SurfaceHolder) {
|
override fun onSurfaceCreated(holder: SurfaceHolder) {
|
||||||
super.onSurfaceCreated(holder)
|
super.onSurfaceCreated(holder)
|
||||||
renderThread = RenderThread(holder, getApplicationContext(), screenWidth, screenHeight)
|
handlerThread = HandlerThread("RenderHandlerThread")
|
||||||
renderThread?.start()
|
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)
|
super.onSurfaceDestroyed(holder)
|
||||||
renderThread?.interrupt()
|
renderLooper.stop()
|
||||||
renderThread = null
|
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 context: Context,
|
||||||
private val screenWidth: Int,
|
private var bitmap: Bitmap,
|
||||||
private val screenHeight: Int
|
private var screenWidth: Int,
|
||||||
) : Thread() {
|
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 renderWidth = 0f
|
||||||
private var renderHeight = 0f
|
private var renderHeight = 0f
|
||||||
private var renderStartX = 0f
|
private var renderStartX = 0f
|
||||||
private var renderStartY = 0f
|
private var renderStartY = 0f
|
||||||
private var maxOffset = 0f
|
|
||||||
private var moveXAxis = false
|
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 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() {
|
override fun run() {
|
||||||
try {
|
try {
|
||||||
// 1. MediaExtractor 초기화
|
val inputBuffersTimeoutUs = 10000L
|
||||||
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) ?: ""
|
|
||||||
|
|
||||||
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
|
while (running && !Thread.currentThread().isInterrupted) {
|
||||||
keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE)
|
if (isEOS) {
|
||||||
} else {
|
|
||||||
keyFrameRate = 60
|
|
||||||
}
|
|
||||||
if (mime.startsWith("video/")) {
|
|
||||||
videoTrackIndex = i
|
|
||||||
selectTrack(i)
|
|
||||||
|
|
||||||
videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
|
continue
|
||||||
videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (videoTrackIndex < 0) return
|
val inputBufferIndex = codec?.dequeueInputBuffer(inputBuffersTimeoutUs) ?: -1
|
||||||
|
if (inputBufferIndex >= 0) {
|
||||||
// 2. MediaCodec 초기화
|
val inputBuffer = codec?.getInputBuffer(inputBufferIndex)
|
||||||
extractor?.getTrackFormat(videoTrackIndex)?.let { format ->
|
if (inputBuffer != null) {
|
||||||
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
|
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
||||||
configure(format, null, null, 0)
|
if (sampleSize < 0) {
|
||||||
start()
|
codec?.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
||||||
}
|
isEOS = true
|
||||||
}
|
} else {
|
||||||
|
val pts = extractor.sampleTime
|
||||||
// 3. RenderScript 초기화
|
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
|
||||||
rs = RenderScript.create(context)
|
extractor.advance()
|
||||||
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 outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1
|
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, inputBuffersTimeoutUs) ?: -1
|
||||||
if (outputBufferIndex >= 0) {
|
if (outputBufferIndex >= 0) {
|
||||||
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
|
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
|
||||||
Blog.LOGE("codec.outputFormat >>> ${codec?.outputFormat}")
|
|
||||||
if (bufferInfo.size > 0 && outputBuffer != null) {
|
if (bufferInfo.size > 0 && outputBuffer != null) {
|
||||||
val format = codec?.outputFormat
|
val format = codec?.outputFormat
|
||||||
|
|
||||||
val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: /*기본값*/0
|
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 rawYUV = ByteArray(bufferInfo.size)
|
val rawYUV = ByteArray(bufferInfo.size)
|
||||||
outputBuffer.get(rawYUV)
|
outputBuffer.get(rawYUV)
|
||||||
outputBuffer.position(0)
|
outputBuffer.position(0)
|
||||||
|
|
||||||
// 포맷별 변환
|
|
||||||
val nv21Data = when (colorFormat) {
|
val nv21Data = when (colorFormat) {
|
||||||
COLOR_FormatYUV420SemiPlanar -> nv12ToNv21(rawYUV, videoWidth, videoHeight)
|
COLOR_FormatYUV420SemiPlanar -> nv12ToNv21(rawYUV, videoWidth, videoHeight)
|
||||||
COLOR_FormatYUV420Planar -> i420ToNv21(rawYUV, videoWidth, videoHeight)
|
COLOR_FormatYUV420Planar -> i420ToNv21(rawYUV, videoWidth, videoHeight)
|
||||||
else -> rawYUV // 이미 NV21 등
|
else -> rawYUV
|
||||||
}
|
}
|
||||||
|
|
||||||
// NV21 데이터 → Bitmap
|
val bmp = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
|
||||||
val bitmap = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
|
synchronized(lock) {
|
||||||
// outputBuffer.get(yuvData, 0, minOf(bufferInfo.size, expectedSize))
|
bitmap?.recycle()
|
||||||
// getYUVDataWithStride(outputBuffer, videoWidth, videoHeight, stride, height)
|
bitmap = bmp
|
||||||
// val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight)
|
}
|
||||||
|
|
||||||
// 6. 왕복 이동 애니메이션 offset 계산
|
if (moveXAxis || moveYAxis) {
|
||||||
// Blog.LOGE("maxOffset >>> $maxOffset , offset >>> $offset , direction >> $direction , keyFrameRate >>> $keyFrameRate")
|
|
||||||
if (maxOffset > 0f) {
|
|
||||||
offset += direction * speed
|
offset += direction * speed
|
||||||
if (offset < 0f) { offset = 0f; direction = 1 }
|
if (offset < 0f) {
|
||||||
if (offset > maxOffset) { offset = maxOffset; direction = -1 }
|
offset = 0f
|
||||||
}
|
direction = 1
|
||||||
|
}
|
||||||
// 7. Canvas에 그리고 unlock
|
if (moveXAxis && offset > maxOffset) {
|
||||||
val canvas = holder.lockCanvas()
|
offset = maxOffset
|
||||||
if (canvas != null) {
|
direction = -1
|
||||||
val drawX = if (moveXAxis) renderStartX - offset else renderStartX
|
} else if (moveYAxis && offset > maxOffset) {
|
||||||
val drawY = if (!moveXAxis) renderStartY - offset else renderStartY
|
offset = maxOffset
|
||||||
val dstRect = RectF(
|
direction = -1
|
||||||
drawX, drawY,
|
}
|
||||||
drawX + renderWidth, drawY + renderHeight
|
|
||||||
)
|
noMoveStartTime = 0L
|
||||||
canvas.drawColor(Color.BLACK)
|
} else {
|
||||||
canvas.drawBitmap(bitmap, null, dstRect, null)
|
val now = System.currentTimeMillis()
|
||||||
// 오버레이 예시
|
if (noMoveStartTime == 0L) noMoveStartTime = now
|
||||||
val paint = Paint().apply {
|
else if (now - noMoveStartTime >= holdDurationMs) {
|
||||||
color = Color.WHITE
|
noMoveStartTime = 0L
|
||||||
textSize = 40f
|
moveCycleCompleted = true
|
||||||
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
|
|
||||||
}
|
}
|
||||||
canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paint)
|
|
||||||
holder.unlockCanvasAndPost(canvas)
|
|
||||||
}
|
}
|
||||||
outputBuffer.clear()
|
|
||||||
}
|
}
|
||||||
codec?.releaseOutputBuffer(outputBufferIndex, false)
|
codec?.releaseOutputBuffer(outputBufferIndex, false)
|
||||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||||
// 필요하다면 포맷 변경 처리
|
// 필요시 처리
|
||||||
}
|
}
|
||||||
sleep(1000L / keyFrameRate) // 약 30fps
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
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 {
|
private fun convertYUVToBitmap(yuvByteArray: ByteArray, width: Int, height: Int): Bitmap {
|
||||||
// YUV420の場合, 바이트 수는 width * height * 3 / 2 여야 함
|
|
||||||
val minSize = width * height * 3 / 2
|
val minSize = width * height * 3 / 2
|
||||||
val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray
|
val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray
|
||||||
|
|
||||||
@ -291,33 +537,12 @@ class RenderThread(
|
|||||||
return bitmap
|
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 {
|
fun nv12ToNv21(nv12: ByteArray, width: Int, height: Int): ByteArray {
|
||||||
val frameSize = width * height
|
val frameSize = width * height
|
||||||
val nv21 = ByteArray(frameSize * 3 / 2)
|
val nv21 = ByteArray(frameSize * 3 / 2)
|
||||||
|
|
||||||
// Y는 그대로 복사
|
|
||||||
System.arraycopy(nv12, 0, nv21, 0, frameSize)
|
System.arraycopy(nv12, 0, nv21, 0, frameSize)
|
||||||
|
|
||||||
// UV를 VU로 뒤집어서 복사
|
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < frameSize / 2 - 1) {
|
while (i < frameSize / 2 - 1) {
|
||||||
nv21[frameSize + i] = nv12[frameSize + i + 1]
|
nv21[frameSize + i] = nv12[frameSize + i + 1]
|
||||||
@ -327,54 +552,135 @@ class RenderThread(
|
|||||||
return nv21
|
return nv21
|
||||||
}
|
}
|
||||||
|
|
||||||
// I420(YUV420Planar) to NV21 변환
|
|
||||||
fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray {
|
fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray {
|
||||||
val frameSize = width * height
|
val frameSize = width * height
|
||||||
val qFrameSize = frameSize / 4
|
val qFrameSize = frameSize / 4
|
||||||
val nv21 = ByteArray(frameSize * 3 / 2)
|
val nv21 = ByteArray(frameSize * 3 / 2)
|
||||||
|
|
||||||
// Y 복사
|
|
||||||
System.arraycopy(i420, 0, nv21, 0, frameSize)
|
System.arraycopy(i420, 0, nv21, 0, frameSize)
|
||||||
|
|
||||||
// VU interleave
|
|
||||||
val u = frameSize
|
val u = frameSize
|
||||||
val v = frameSize + qFrameSize
|
val v = frameSize + qFrameSize
|
||||||
|
|
||||||
for (i in 0 until qFrameSize) {
|
for (i in 0 until qFrameSize) {
|
||||||
nv21[frameSize + i * 2] = i420[v + i] // V
|
nv21[frameSize + i * 2] = i420[v + i]
|
||||||
nv21[frameSize + i * 2 + 1] = i420[u + i] // U
|
nv21[frameSize + i * 2 + 1] = i420[u + i]
|
||||||
}
|
}
|
||||||
return nv21
|
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 복사
|
class RenderLooper(
|
||||||
for (y in 0 until height) {
|
private val context: Context,
|
||||||
src.position(y * stride)
|
private val holder: SurfaceHolder,
|
||||||
src.get(nv21, y * width, width)
|
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씩 적용
|
fun start() {
|
||||||
val uOffsetBuf = sliceHeight * stride
|
running = true
|
||||||
val vOffsetBuf = uOffsetBuf + (sliceHeight / 2) * (stride / 2)
|
initMediaFiles()
|
||||||
|
loadNextMedia()
|
||||||
|
handler.post(renderRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
for (y in 0 until height/2) {
|
fun stop() {
|
||||||
src.position(uOffsetBuf + y * (stride / 2))
|
running = false
|
||||||
src.get(nv21, frameSize + y * width, width/2) // 임시: U만
|
handler.removeCallbacks(renderRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
src.position(vOffsetBuf + y * (stride / 2))
|
private fun initMediaFiles() {
|
||||||
for (x in 0 until width/2) {
|
val youtubeDLDir = File(
|
||||||
// NV21 순서: V, U
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
val v = src.get()
|
"wallPapers"
|
||||||
val u = nv21[frameSize + y * width + x] // U
|
)
|
||||||
nv21[frameSize + y * width + x*2] = v
|
if (youtubeDLDir.exists() && youtubeDLDir.isDirectory && youtubeDLDir.listFiles().size > 2) {
|
||||||
nv21[frameSize + y * width + x*2 + 1] = u
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Blog.LOGE("mediaFiles >>> ${mediaFiles?.size}")
|
||||||
return nv21
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -89,14 +89,14 @@
|
|||||||
<bums.lunatic.launcher.tokiz.view.PagedTextLayout
|
<bums.lunatic.launcher.tokiz.view.PagedTextLayout
|
||||||
android:id="@+id/paged_layer"
|
android:id="@+id/paged_layer"
|
||||||
android:layout_margin="1dp"
|
android:layout_margin="1dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:elevation="5dp"
|
android:elevation="5dp"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="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" />
|
app:layout_constraintTop_toBottomOf="@id/textview_title" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@ -89,6 +89,7 @@
|
|||||||
android:overScrollMode="never"
|
android:overScrollMode="never"
|
||||||
android:padding="@dimen/default_padding"
|
android:padding="@dimen/default_padding"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
|
android:background="#AB000000"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user