...
This commit is contained in:
parent
e9e585bad0
commit
dc19ff5339
@ -311,27 +311,57 @@ void Renderer::renderFrame(ANativeWindow* window) {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(weatherMutex_);
|
std::lock_guard<std::mutex> lock(weatherMutex_);
|
||||||
if (!weatherPixels_.empty()) {
|
if (!weatherPixels_.empty()) {
|
||||||
// 1. 위치 업데이트 (픽셀 단위 이동)
|
// ======== ⬇️ 유기적인 움직임(Drift) 추가 ⬇️ ========
|
||||||
|
std::uniform_real_distribution<float> driftDist(-0.3f, 0.3f); // 방향 전환 폭을 살짝 키움
|
||||||
|
weatherVelX_ += driftDist(randomEngine_);
|
||||||
|
weatherVelY_ += driftDist(randomEngine_);
|
||||||
|
|
||||||
|
// 2. 현재 속도(벡터의 길이) 계산
|
||||||
|
float speed = std::sqrt(weatherVelX_ * weatherVelX_ + weatherVelY_ * weatherVelY_);
|
||||||
|
|
||||||
|
// 💡 여기서 최소/최대 이동 픽셀을 강제로 지정합니다.
|
||||||
|
float maxSpeed = 3.0f; // 프레임당 최대 5픽셀 이동 (답답하지 않게 상향)
|
||||||
|
float minSpeed = 1.0f; // 프레임당 최소 2픽셀 이동 보장 (절대 멈추거나 느려지지 않음)
|
||||||
|
|
||||||
|
// 속도 보정 (Normalization & Scaling)
|
||||||
|
if (speed < 0.001f) {
|
||||||
|
// 완전히 멈췄을 때의 방어 코드
|
||||||
|
weatherVelX_ = minSpeed;
|
||||||
|
weatherVelY_ = minSpeed;
|
||||||
|
} else if (speed > maxSpeed) {
|
||||||
|
// 한계 속도 초과 시 maxSpeed로 깎아냄
|
||||||
|
weatherVelX_ = (weatherVelX_ / speed) * maxSpeed;
|
||||||
|
weatherVelY_ = (weatherVelY_ / speed) * maxSpeed;
|
||||||
|
} else if (speed < minSpeed) {
|
||||||
|
// 🚀 속도가 너무 줄어들면 강제로 최소 속도(minSpeed) 픽셀만큼 끌어올림
|
||||||
|
weatherVelX_ = (weatherVelX_ / speed) * minSpeed;
|
||||||
|
weatherVelY_ = (weatherVelY_ / speed) * minSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 위치 업데이트 (최소 2px ~ 최대 5px 이동 보장)
|
||||||
weatherX_ += weatherVelX_;
|
weatherX_ += weatherVelX_;
|
||||||
weatherY_ += weatherVelY_;
|
weatherY_ += weatherVelY_;
|
||||||
|
|
||||||
// 2. 화면 경계 검사 및 튕기기 (Bounce)
|
// 4. 화면 경계 검사 및 튕기기 (Bounce)
|
||||||
|
// 튕길 때 속도가 너무 크게 변하지 않도록 난수 폭을 안정적으로(0.9~1.1) 조절
|
||||||
|
std::uniform_real_distribution<float> bounceDist(0.9f, 1.1f);
|
||||||
|
|
||||||
// 가로 경계
|
// 가로 경계
|
||||||
if (weatherX_ < 0) {
|
if (weatherX_ < 0) {
|
||||||
weatherX_ = 0;
|
weatherX_ = 0;
|
||||||
weatherVelX_ *= -1.0f;
|
weatherVelX_ = std::abs(weatherVelX_) * bounceDist(randomEngine_);
|
||||||
} else if (weatherX_ + weatherWidth_ > surfaceWidth) {
|
} else if (weatherX_ + weatherWidth_ > surfaceWidth) {
|
||||||
weatherX_ = (float)surfaceWidth - weatherWidth_;
|
weatherX_ = (float)surfaceWidth - weatherWidth_;
|
||||||
weatherVelX_ *= -1.0f;
|
weatherVelX_ = -std::abs(weatherVelX_) * bounceDist(randomEngine_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 세로 경계
|
// 세로 경계
|
||||||
if (weatherY_ < 0) {
|
if (weatherY_ < 0) {
|
||||||
weatherY_ = 0;
|
weatherY_ = 0;
|
||||||
weatherVelY_ *= -1.0f;
|
weatherVelY_ = std::abs(weatherVelY_) * bounceDist(randomEngine_);
|
||||||
} else if (weatherY_ + weatherHeight_ > surfaceHeight) {
|
} else if (weatherY_ + weatherHeight_ > surfaceHeight) {
|
||||||
weatherY_ = (float)surfaceHeight - weatherHeight_;
|
weatherY_ = (float)surfaceHeight - weatherHeight_;
|
||||||
weatherVelY_ *= -1.0f;
|
weatherVelY_ = -std::abs(weatherVelY_) * bounceDist(randomEngine_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 그리기 (기존의 블렌딩 로직 사용)
|
// 3. 그리기 (기존의 블렌딩 로직 사용)
|
||||||
@ -352,16 +382,18 @@ void Renderer::renderFrame(ANativeWindow* window) {
|
|||||||
|
|
||||||
uint32_t* dst = &dstPixels[targetY * dstStride + targetX];
|
uint32_t* dst = &dstPixels[targetY * dstStride + targetX];
|
||||||
|
|
||||||
// 단순 합성을 위해 하드코딩된 ARGB/RGBA 순서 주의
|
// ======== ⬇️ 이 부분을 수정합니다 ⬇️ ========
|
||||||
uint8_t dr = (*dst >> 16) & 0xFF;
|
// Red와 Blue 채널의 비트 시프트(Shift) 위치를 안드로이드 Bitmap(RGBA) 규격에 맞게 수정
|
||||||
uint8_t dg = (*dst >> 8) & 0xFF;
|
uint8_t dr = (*dst) & 0xFF; // 배경의 Red
|
||||||
uint8_t db = (*dst) & 0xFF;
|
uint8_t dg = (*dst >> 8) & 0xFF; // 배경의 Green
|
||||||
|
uint8_t db = (*dst >> 16) & 0xFF; // 배경의 Blue
|
||||||
|
|
||||||
|
// 날씨 아이콘(src)의 R(0), G(1), B(2)와 배경(dst)을 알파 블렌딩
|
||||||
uint8_t r = (src[0] * alpha + dr * (255 - alpha)) / 255;
|
uint8_t r = (src[0] * alpha + dr * (255 - alpha)) / 255;
|
||||||
uint8_t g = (src[1] * alpha + dg * (255 - alpha)) / 255;
|
uint8_t g = (src[1] * alpha + dg * (255 - alpha)) / 255;
|
||||||
uint8_t b = (src[2] * alpha + db * (255 - alpha)) / 255;
|
uint8_t b = (src[2] * alpha + db * (255 - alpha)) / 255;
|
||||||
|
|
||||||
*dst = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
*dst = (0xFF << 24) | (b << 16) | (g << 8) | r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,16 +8,33 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.ColorFilter
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.PathDashPathEffect
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.graphics.RadialGradient
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.Shader
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.Typeface.BOLD
|
import android.graphics.Typeface.BOLD
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.service.wallpaper.WallpaperService
|
import android.service.wallpaper.WallpaperService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.graphics.alpha
|
||||||
|
import bums.lunatic.launcher.R
|
||||||
import bums.lunatic.launcher.model.Translation
|
import bums.lunatic.launcher.model.Translation
|
||||||
import bums.lunatic.launcher.model.WallContentGroup
|
import bums.lunatic.launcher.model.WallContentGroup
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
@ -39,17 +56,59 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
Log.d(TAG, "onCreateEngine: New engine instance created.")
|
Log.d(TAG, "onCreateEngine: New engine instance created.")
|
||||||
return NativeRenderEngine()
|
return NativeRenderEngine()
|
||||||
}
|
}
|
||||||
private fun getBatteryStatus(): Pair<Int, Boolean> {
|
|
||||||
|
data class BatteryInfo(
|
||||||
|
val percent: Int,
|
||||||
|
val isCharging: Boolean,
|
||||||
|
val speedText: String,
|
||||||
|
val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getBatteryStatus(): BatteryInfo {
|
||||||
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
|
|
||||||
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
||||||
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
|
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
|
||||||
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
||||||
|
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
|
||||||
|
|
||||||
val batteryPct = (level / scale.toFloat() * 100).toInt()
|
val batteryPct = (level / scale.toFloat() * 100).toInt()
|
||||||
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||||
status == BatteryManager.BATTERY_STATUS_FULL
|
status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
|
||||||
return Pair(batteryPct, isCharging)
|
var speedText = ""
|
||||||
|
var detailsText = ""
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
speedText = when (plugged) {
|
||||||
|
BatteryManager.BATTERY_PLUGGED_AC -> "고속"
|
||||||
|
BatteryManager.BATTERY_PLUGGED_USB -> "저속"
|
||||||
|
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
|
||||||
|
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0
|
||||||
|
val voltageV = voltageMv / 1000.0f
|
||||||
|
|
||||||
|
// 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회
|
||||||
|
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
|
||||||
|
// 안드로이드 표준 단위는 마이크로암페어(µA) 입니다.
|
||||||
|
val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
|
||||||
|
|
||||||
|
// 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환
|
||||||
|
val currentA = Math.abs(currentUa) / 1000000.0f
|
||||||
|
|
||||||
|
// 3. 전력(Watt) 계산: W = V * A
|
||||||
|
val wattage = voltageV * currentA
|
||||||
|
|
||||||
|
// 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어)
|
||||||
|
if (voltageV > 0f && currentA > 0f) {
|
||||||
|
detailsText = String.format("%.1fV,%.1fA,(%.1fW)", voltageV, currentA, wattage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BatteryInfo(batteryPct, isCharging, speedText, detailsText)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class NativeRenderEngine : Engine() {
|
inner class NativeRenderEngine : Engine() {
|
||||||
@ -269,7 +328,6 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
|
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||||
Blog.LOGE("requiredSize ${requiredSize} w :${options.outWidth} , h : ${options.outHeight}")
|
|
||||||
if (options.outWidth >= requiredSize && options.outHeight >= requiredSize) {
|
if (options.outWidth >= requiredSize && options.outHeight >= requiredSize) {
|
||||||
mediaFiles.add(file)
|
mediaFiles.add(file)
|
||||||
} else {
|
} else {
|
||||||
@ -293,7 +351,7 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
val totalMegs = mi.totalMem / 1048576L
|
val totalMegs = mi.totalMem / 1048576L
|
||||||
val usedMegs = totalMegs - availableMegs
|
val usedMegs = totalMegs - availableMegs
|
||||||
val percent = (usedMegs.toDouble() / totalMegs.toDouble() * 100).toInt()
|
val percent = (usedMegs.toDouble() / totalMegs.toDouble() * 100).toInt()
|
||||||
return "RAM: $usedMegs / ${totalMegs}MB ($percent%)"
|
return "RAM: $percent%"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 배터리/기기 온도 가져오기 (Celsius)
|
// 배터리/기기 온도 가져오기 (Celsius)
|
||||||
@ -310,116 +368,218 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val materialIconFont: Typeface by lazy {
|
||||||
|
resources.getFont(R.font.material_symbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날씨 텍스트를 분석하여 Material Symbol 아이콘 문자열로 변환하는 함수
|
||||||
|
private fun getWeatherIconString(condition: String?): String {
|
||||||
|
if (condition == null) return "thermostat"
|
||||||
|
val lower = condition.lowercase()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// 1. 천둥/번개 (우선순위 최상: 비나 눈이 와도 번개가 치면 뇌우 아이콘)
|
||||||
|
lower.contains("thunder") -> "thunderstorm"
|
||||||
|
|
||||||
|
// 2. 눈, 우박, 진눈깨비, 빙결 (Snow, Ice pellets, Sleet, Blizzard, Freezing)
|
||||||
|
lower.contains("snow") || lower.contains("blizzard") ||
|
||||||
|
lower.contains("ice") || lower.contains("sleet") ||
|
||||||
|
lower.contains("freezing") -> "snowing"
|
||||||
|
|
||||||
|
// 3. 비, 소나기, 이슬비 (Rain, Drizzle, Shower)
|
||||||
|
lower.contains("rain") || lower.contains("drizzle") ||
|
||||||
|
lower.contains("shower") -> "rainy"
|
||||||
|
|
||||||
|
// 4. 안개 (Fog, Mist)
|
||||||
|
lower.contains("fog") || lower.contains("mist") -> "foggy"
|
||||||
|
|
||||||
|
// 5. 구름 (Overcast, Cloudy, Partly cloudy)
|
||||||
|
lower.contains("partly cloudy") -> "partly_cloudy_day"
|
||||||
|
lower.contains("cloudy") || lower.contains("overcast") -> "cloud"
|
||||||
|
|
||||||
|
// 6. 맑음 (Sunny, Clear)
|
||||||
|
lower.contains("sunny") || lower.contains("clear") -> "sunny"
|
||||||
|
|
||||||
|
// 7. 예외 처리 (매핑되지 않은 기본값)
|
||||||
|
else -> "thermostat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
|
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
|
||||||
|
// 1. 데이터 준비
|
||||||
val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date())
|
val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date())
|
||||||
val ramInfo = getRamUsage()
|
val ramInfo = getRamUsage()
|
||||||
val tempInfo = getDeviceTemperature()
|
val tempInfo = getDeviceTemperature()
|
||||||
val batteryInfo = getBatteryStatus().let { "${it.first}% ${if(it.second) "⚡" else ""}" }
|
val batteryInfo = getBatteryStatus().let { info ->
|
||||||
|
if (info.isCharging && info.detailsText.isNotEmpty()) {
|
||||||
val finalLines = mutableListOf<String>()
|
"${info.percent}% ⚡${info.speedText} [${info.detailsText}]"
|
||||||
|
} else if (info.isCharging) {
|
||||||
// 1. 날씨 정보 (최상단)
|
"${info.percent}% ⚡${info.speedText}"
|
||||||
if (weatherLines.isNotEmpty()) finalLines.add(weatherLines[0])
|
} else {
|
||||||
|
"${info.percent}%"
|
||||||
// 2. 날짜 (중간 강조)
|
|
||||||
finalLines.add(dateFull)
|
|
||||||
|
|
||||||
// 3. 시스템 정보 (온도, 메모리, 배터리 통합 한 줄)
|
|
||||||
finalLines.add("$tempInfo | $ramInfo | $batteryInfo")
|
|
||||||
|
|
||||||
// 4. 주소 정보 (최하단 작게)
|
|
||||||
if (weatherLines.size > 1) {
|
|
||||||
finalLines.add(weatherLines.last())
|
|
||||||
}
|
|
||||||
|
|
||||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
||||||
color = Color.WHITE
|
|
||||||
textAlign = Paint.Align.CENTER
|
|
||||||
typeface = Typeface.create(Typeface.MONOSPACE, BOLD) // 가독성을 위해 BOLD
|
|
||||||
setShadowLayer(12f, 4f, 4f, Color.argb(180, 0, 0, 0)) // 가독성을 위한 그림자 강화
|
|
||||||
}
|
|
||||||
|
|
||||||
// 영역별 폰트 사이즈 설정
|
|
||||||
val titleSize = 75f // 날씨
|
|
||||||
val dateSize = 55f // 날짜
|
|
||||||
val systemSize = 35f // 온도/메모리
|
|
||||||
val addressSize = 28f // 주소
|
|
||||||
val lineSpacingMultiplier = 0.3f
|
|
||||||
|
|
||||||
var maxWidth = 0f
|
|
||||||
var totalHeight = 50f
|
|
||||||
val lineBaselines = mutableListOf<Float>()
|
|
||||||
|
|
||||||
finalLines.forEachIndexed { index, line ->
|
|
||||||
paint.textSize = when (index) {
|
|
||||||
0 -> titleSize
|
|
||||||
1 -> dateSize
|
|
||||||
2 -> systemSize
|
|
||||||
else -> addressSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val width = paint.measureText(line)
|
|
||||||
if (width > maxWidth) maxWidth = width
|
|
||||||
|
|
||||||
val metrics = paint.fontMetrics
|
|
||||||
val fontHeight = metrics.descent - metrics.ascent
|
|
||||||
val leading = paint.textSize * lineSpacingMultiplier
|
|
||||||
|
|
||||||
lineBaselines.add(totalHeight - metrics.ascent)
|
|
||||||
totalHeight += (fontHeight + leading)
|
|
||||||
}
|
}
|
||||||
totalHeight += 30f
|
|
||||||
|
|
||||||
val bitmap = Bitmap.createBitmap((maxWidth + 120).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888)
|
val weatherCondition = if (weatherLines.isNotEmpty()) weatherLines[0] else ""
|
||||||
|
val weatherTemp = if (weatherLines.size > 1) weatherLines[1] else ""
|
||||||
|
val weatherHum = if (weatherLines.size > 2) weatherLines[2] else ""
|
||||||
|
val address = if (weatherLines.size > 3) weatherLines[3] else ""
|
||||||
|
|
||||||
|
val wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
val monoBold = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
|
||||||
|
|
||||||
|
// 2. 뷰 생성 헬퍼
|
||||||
|
fun createTextView(textToSet: String, textSizeDp: Float, font: Typeface, alphaValue: Float = 1.0f): TextView {
|
||||||
|
return TextView(this@MyWallpaperService).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
|
||||||
|
text = textToSet
|
||||||
|
textSize = textSizeDp
|
||||||
|
typeface = font
|
||||||
|
setTextColor(Color.WHITE)
|
||||||
|
alpha = alphaValue
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
setShadowLayer(8f, 2f, 2f, Color.parseColor("#99000000"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHorizontalLayout(): LinearLayout {
|
||||||
|
return LinearLayout(this@MyWallpaperService).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 각 줄(Row)을 생성하여 리스트에 담기
|
||||||
|
val rowsToArrange = mutableListOf<View>()
|
||||||
|
|
||||||
|
// ① 날씨 (아이콘 + 상태)
|
||||||
|
if (weatherCondition.isNotEmpty()) {
|
||||||
|
val weatherRow = createHorizontalLayout()
|
||||||
|
weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), 50f, materialIconFont))
|
||||||
|
weatherRow.addView(createTextView(weatherCondition, 28f, monoBold).apply { setPadding(20, 0, 0, 0) })
|
||||||
|
rowsToArrange.add(weatherRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 온도 & 습도
|
||||||
|
if (weatherTemp.isNotEmpty()) {
|
||||||
|
val tempRow = createHorizontalLayout()
|
||||||
|
|
||||||
|
val tempStr = weatherTemp.trim()
|
||||||
|
val humStr = weatherHum.trim()
|
||||||
|
|
||||||
|
tempRow.addView(createTextView("thermostat", 20f, materialIconFont, 0.9f))
|
||||||
|
tempRow.addView(createTextView(tempStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 30, 0) })
|
||||||
|
|
||||||
|
if (humStr.isNotEmpty()) {
|
||||||
|
tempRow.addView(createTextView("water_drop", 20f, materialIconFont, 0.9f))
|
||||||
|
tempRow.addView(createTextView(humStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 0, 0) })
|
||||||
|
}
|
||||||
|
rowsToArrange.add(tempRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 주소
|
||||||
|
if (address.isNotEmpty()) {
|
||||||
|
rowsToArrange.add(createTextView(address, 16f, monoBold, 0.95f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ④ 날짜
|
||||||
|
rowsToArrange.add(createTextView(dateFull, 14f, monoBold, 0.8f))
|
||||||
|
|
||||||
|
// ⑤ 시스템 정보 (한 줄로 묶음)
|
||||||
|
rowsToArrange.add(createTextView("$ramInfo | $tempInfo | Battery $batteryInfo", 12f, monoBold, 0.6f))
|
||||||
|
|
||||||
|
// 4. 가로 길이 측정 후 다이아몬드 형태로 재정렬
|
||||||
|
val unspecifiedSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
rowsToArrange.forEach { it.measure(unspecifiedSpec, unspecifiedSpec) }
|
||||||
|
|
||||||
|
val sortedRows = rowsToArrange.sortedBy { it.measuredWidth }
|
||||||
|
val arrangedRows = arrayOfNulls<View>(sortedRows.size)
|
||||||
|
var topIndex = 0
|
||||||
|
var bottomIndex = sortedRows.size - 1
|
||||||
|
|
||||||
|
for (i in sortedRows.indices) {
|
||||||
|
if (i % 2 == 0) arrangedRows[topIndex++] = sortedRows[i]
|
||||||
|
else arrangedRows[bottomIndex--] = sortedRows[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 최종 부모 레이아웃 조립 (원형/타원형 배경)
|
||||||
|
val rootLayout = LinearLayout(this@MyWallpaperService).apply {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent)
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
// 💡 원형 안에 글씨가 넉넉히 들어가도록 패딩을 충분히 줍니다.
|
||||||
|
setPadding(20, 20, 20, 20)
|
||||||
|
background = WavyOvalDrawable(
|
||||||
|
bgColor = Color.parseColor("#33000000"),
|
||||||
|
strokeColor = Color.parseColor("#22000000"), // 물결이 잘 보이도록 불투명도를 살짝 올림
|
||||||
|
strokeThick = 30f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
arrangedRows.forEach { rowView ->
|
||||||
|
if (rowView != null) {
|
||||||
|
val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply {
|
||||||
|
setMargins(0, 10, 0, 10)
|
||||||
|
}
|
||||||
|
rootLayout.addView(rowView, lp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 💡 완벽한 정원형(Perfect Circle)을 위한 2단계 측정(Measure) 로직
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val maxWidthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, View.MeasureSpec.AT_MOST)
|
||||||
|
|
||||||
|
// 1차 측정: 내용물들이 자연스럽게 늘어났을 때의 크기(너비, 높이)를 구합니다.
|
||||||
|
rootLayout.measure(maxWidthSpec, unspecifiedSpec)
|
||||||
|
|
||||||
|
// 너비와 높이 중 '더 큰 값'을 찾아 원의 지름(Size)으로 삼습니다.
|
||||||
|
val circleSize = Math.max(rootLayout.measuredWidth, rootLayout.measuredHeight)
|
||||||
|
|
||||||
|
if (circleSize <= 0) return null
|
||||||
|
|
||||||
|
// 2차 측정: 너비와 높이를 원의 지름 크기로 '정확히(EXACTLY)' 덮어씌웁니다.
|
||||||
|
val exactSpec = View.MeasureSpec.makeMeasureSpec(circleSize, View.MeasureSpec.EXACTLY)
|
||||||
|
rootLayout.measure(exactSpec, exactSpec)
|
||||||
|
|
||||||
|
// 레이아웃 배치
|
||||||
|
rootLayout.layout(0, 0, circleSize, circleSize)
|
||||||
|
|
||||||
|
// 7. 비트맵 캡처
|
||||||
|
val bitmap = Bitmap.createBitmap(circleSize, circleSize, Bitmap.Config.ARGB_8888)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
val centerX = bitmap.width / 2f
|
rootLayout.draw(canvas)
|
||||||
|
|
||||||
finalLines.forEachIndexed { index, line ->
|
|
||||||
paint.textSize = when (index) {
|
|
||||||
0 -> titleSize
|
|
||||||
1 -> dateSize
|
|
||||||
2 -> systemSize
|
|
||||||
else -> addressSize
|
|
||||||
}
|
|
||||||
// 시스템 정보 줄(index 2)은 약간 불투명하게 처리하여 대비를 줄 수도 있습니다.
|
|
||||||
paint.alpha = if (index == 2) 200 else 255
|
|
||||||
|
|
||||||
canvas.drawText(line, centerX, lineBaselines[index], paint)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updateWeatherRunnable = object : Runnable {
|
private val updateWeatherRunnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
// if (!isVisible) return
|
if (!isVisible) return
|
||||||
//
|
|
||||||
// val (batteryLevel, isCharging) = getBatteryStatus()
|
val (batteryLevel, isCharging) = getBatteryStatus()
|
||||||
//
|
|
||||||
// // --- ⬇️ 제안하신 조건별 로직 적용 ⬇️ ---
|
// --- ⬇️ 제안하신 조건별 로직 적용 ⬇️ ---
|
||||||
// var nextDelay = 30000L // 기본 20초
|
var nextDelay = 30000L // 기본 20초
|
||||||
//
|
|
||||||
// if (isCharging) {
|
if (isCharging) {
|
||||||
// nextDelay = 1000L // 충전 중이거나 70% 이상이면 10초
|
nextDelay = 5000L // 충전 중이거나 70% 이상이면 10초
|
||||||
// nativeRenderer?.setImageRenderFrame(45)
|
nativeRenderer?.setImageRenderFrame(45)
|
||||||
// } else if (batteryLevel >= 80) {
|
} else if (batteryLevel >= 80) {
|
||||||
// nativeRenderer?.setImageRenderFrame(70)
|
nativeRenderer?.setImageRenderFrame(70)
|
||||||
// } else {
|
} else {
|
||||||
// handler.removeCallbacks(this)
|
handler.removeCallbacks(this)
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// // 비트맵 생성 및 전달
|
// 비트맵 생성 및 전달
|
||||||
// val weather = LocationUpdateService.lastWeather
|
val weather = LocationUpdateService.lastWeather
|
||||||
// val bitmap = drawBitmapFromText(weather,isCharging)
|
val bitmap = drawBitmapFromText(weather,isCharging)
|
||||||
// bitmap?.let {
|
bitmap?.let {
|
||||||
// nativeRenderer?.setWeatherBitmap(it)
|
nativeRenderer?.setWeatherBitmap(it)
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// Log.d(TAG, "Next update in ${nextDelay / 1000}s (Battery: $batteryLevel%, Charging: $isCharging)")
|
handler.postDelayed(this, nextDelay)
|
||||||
// handler.postDelayed(this, nextDelay)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,9 +613,10 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
|
|
||||||
val (batteryLevel, isCharging) = getBatteryStatus()
|
val (batteryLevel, isCharging) = getBatteryStatus()
|
||||||
|
|
||||||
if (batteryLevel >= 70) {
|
if (batteryLevel >= 70 || isCharging) {
|
||||||
val bitmap = drawBitmapFromText(LocationUpdateService.lastWeather)
|
val bitmap = drawBitmapFromText(LocationUpdateService.lastWeather)
|
||||||
bitmap?.let { nativeRenderer?.setWeatherBitmap(it) }
|
bitmap?.let { nativeRenderer?.setWeatherBitmap(it) }
|
||||||
|
nativeRenderer?.setImageRenderFrame(55)
|
||||||
Log.d(TAG, "Battery Low: Weather updated only on media change.")
|
Log.d(TAG, "Battery Low: Weather updated only on media change.")
|
||||||
} else {
|
} else {
|
||||||
nativeRenderer?.setImageRenderFrame(99)
|
nativeRenderer?.setImageRenderFrame(99)
|
||||||
@ -503,3 +664,81 @@ class MyWallpaperService : WallpaperService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WavyOvalDrawable(
|
||||||
|
private val bgColor: Int,
|
||||||
|
private val strokeColor: Int,
|
||||||
|
private val strokeThick: Float
|
||||||
|
) : Drawable() {
|
||||||
|
|
||||||
|
private val waveLength = 40f
|
||||||
|
private val amplitude = 6f
|
||||||
|
|
||||||
|
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
// 단일 색상이 아닌 Shader(그라데이션)를 사용할 것이므로 style만 지정
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = strokeColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = strokeThick
|
||||||
|
strokeCap = Paint.Cap.ROUND
|
||||||
|
strokeJoin = Paint.Join.ROUND
|
||||||
|
|
||||||
|
val wavePath = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
quadTo(waveLength / 4, amplitude, waveLength / 2, 0f)
|
||||||
|
quadTo(waveLength * 3 / 4, -amplitude, waveLength, 0f)
|
||||||
|
}
|
||||||
|
pathEffect = PathDashPathEffect(wavePath, waveLength, 0f, PathDashPathEffect.Style.MORPH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 뷰의 크기(Bounds)가 결정되거나 변경될 때 호출됩니다.
|
||||||
|
// 여기서 크기에 맞는 그라데이션을 계산하여 페인트에 입힙니다.
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
super.onBoundsChange(bounds)
|
||||||
|
|
||||||
|
val centerX = bounds.exactCenterX()
|
||||||
|
val centerY = bounds.exactCenterY()
|
||||||
|
val radius = Math.max(bounds.width(), bounds.height()) / 2f
|
||||||
|
|
||||||
|
if (radius > 0) {
|
||||||
|
// 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색
|
||||||
|
// 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다.
|
||||||
|
val transparentColor = bgColor and 0x11FFFFFF
|
||||||
|
|
||||||
|
bgPaint.shader = RadialGradient(
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
radius,
|
||||||
|
intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열
|
||||||
|
floatArrayOf(0.4f, 1.0f), // 중심에서 40% 지점부터 투명해지기 시작
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
val rectF = RectF(bounds)
|
||||||
|
|
||||||
|
// 1. 방사형 그라데이션 배경 채우기
|
||||||
|
canvas.drawOval(rectF, bgPaint)
|
||||||
|
|
||||||
|
// 2. 선 잘림 방지용 여백 계산
|
||||||
|
val inset = amplitude + 3f
|
||||||
|
rectF.inset(inset, inset)
|
||||||
|
|
||||||
|
// 3. 물결 테두리 선 그리기
|
||||||
|
canvas.drawOval(rectF, strokePaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
bgPaint.alpha = alpha
|
||||||
|
strokePaint.alpha = alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||||
|
}
|
||||||
@ -266,7 +266,15 @@ open class CurrentWeather {
|
|||||||
var addr: String? = null
|
var addr: String? = null
|
||||||
var current: Current? = null
|
var current: Current? = null
|
||||||
|
|
||||||
fun getSummaryInfo() = listOf<String>("${this.current?.condition?.text}",
|
// 아이콘 URL을 배열의 4번째(index 3) 요소로 추가 전달합니다.
|
||||||
"${this.current?.temp_c}℃ ${this.current?.humidity}%"
|
fun getSummaryInfo(): List<String> {
|
||||||
,"${addr}")
|
val iconUrl = current?.condition?.icon?.let { "https:$it" } ?: ""
|
||||||
|
return listOf(
|
||||||
|
"${current?.condition?.text}", // 0: 날씨 상태
|
||||||
|
"${current?.temp_c}", // 1: 온도 및 습도
|
||||||
|
"${current?.humidity}",
|
||||||
|
"$addr", // 2: 주소
|
||||||
|
iconUrl // 3: 아이콘 URL
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,7 @@ import android.net.*
|
|||||||
import android.os.*
|
import android.os.*
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import bums.lunatic.launcher.utils.Blog
|
||||||
import com.frostwire.jlibtorrent.*
|
import com.frostwire.jlibtorrent.*
|
||||||
import com.frostwire.jlibtorrent.alerts.*
|
import com.frostwire.jlibtorrent.alerts.*
|
||||||
import com.frostwire.jlibtorrent.swig.error_code
|
import com.frostwire.jlibtorrent.swig.error_code
|
||||||
@ -89,6 +90,7 @@ class TorrentService : Service() {
|
|||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction(Intent.ACTION_POWER_CONNECTED)
|
addAction(Intent.ACTION_POWER_CONNECTED)
|
||||||
addAction(Intent.ACTION_POWER_DISCONNECTED)
|
addAction(Intent.ACTION_POWER_DISCONNECTED)
|
||||||
|
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||||
}
|
}
|
||||||
registerReceiver(batteryReceiver, filter)
|
registerReceiver(batteryReceiver, filter)
|
||||||
}
|
}
|
||||||
@ -100,6 +102,51 @@ class TorrentService : Service() {
|
|||||||
Intent.ACTION_POWER_DISCONNECTED -> false
|
Intent.ACTION_POWER_DISCONNECTED -> false
|
||||||
else -> isCharging
|
else -> isCharging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
|
|
||||||
|
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
||||||
|
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
|
||||||
|
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
||||||
|
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
|
||||||
|
|
||||||
|
val batteryPct = (level / scale.toFloat() * 100).toInt()
|
||||||
|
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||||
|
status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
|
||||||
|
var speedText = ""
|
||||||
|
var detailsText = ""
|
||||||
|
|
||||||
|
if (isCharging) {
|
||||||
|
speedText = when (plugged) {
|
||||||
|
BatteryManager.BATTERY_PLUGGED_AC -> "고속"
|
||||||
|
BatteryManager.BATTERY_PLUGGED_USB -> "저속"
|
||||||
|
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
|
||||||
|
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0
|
||||||
|
val voltageV = voltageMv / 1000.0f
|
||||||
|
|
||||||
|
// 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회
|
||||||
|
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
|
||||||
|
// 안드로이드 표준 단위는 마이크로암페어(µA) 입니다.
|
||||||
|
val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
|
||||||
|
|
||||||
|
// 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환
|
||||||
|
val currentA = Math.abs(currentUa) / 1000000.0f
|
||||||
|
|
||||||
|
// 3. 전력(Watt) 계산: W = V * A
|
||||||
|
val wattage = voltageV * currentA
|
||||||
|
|
||||||
|
// 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어)
|
||||||
|
if (voltageV > 0f && currentA > 0f) {
|
||||||
|
detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Blog.LOGE("detailsText >>> $detailsText")
|
||||||
|
|
||||||
updateSessionState()
|
updateSessionState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user