...
This commit is contained in:
parent
e646a72418
commit
1e06d82015
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -171,6 +171,7 @@ class Novels : BaseToki(), PagedTextViewInterface {
|
||||
if (binding.pagedLayer.isVisible) {
|
||||
binding.pagedLayer.visibility = GONE
|
||||
}
|
||||
binding.menuWeb.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
// 2. U/V plane 복사, 각각 stride/2씩 적용
|
||||
val uOffsetBuf = sliceHeight * stride
|
||||
val vOffsetBuf = uOffsetBuf + (sliceHeight / 2) * (stride / 2)
|
||||
|
||||
for (y in 0 until height/2) {
|
||||
src.position(uOffsetBuf + y * (stride / 2))
|
||||
src.get(nv21, frameSize + y * width, width/2) // 임시: U만
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return nv21
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
running = true
|
||||
initMediaFiles()
|
||||
loadNextMedia()
|
||||
handler.post(renderRunnable)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running = false
|
||||
handler.removeCallbacks(renderRunnable)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user