From c7d455553541b99771a2aebd89b5fd8693e8d7d2 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 15 Jun 2026 14:19:06 +0900 Subject: [PATCH] =?UTF-8?q?Sensor=20Alignment:=20=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=ED=99=94=EC=82=B4=ED=91=9C=20=E2=86=92=20arrow=5Ftransparent.p?= =?UTF-8?q?ng=20=EA=B5=90=EC=B2=B4=20(rotate=20+=20tint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자산: - drawable/arrow_transparent.png 신규 (132×112 RGBA, 우향 thin arrow) 코드: - drawLine 3개로 구성하던 화살표 → drawImage(arrowBitmap) 한 번으로 교체 - 4방향은 rotate(degrees, pivot) 로 처리: right=0° / down=90° / left=180° / up=-90° - ColorFilter.tint(#E64A19 × arrowAlpha) — 일러스트 톤 매칭 + blink 유지 - arrowDrawW=90f, 높이는 icon aspect(132:112)로 비례 - center 위치는 bladder oval edge + sensorHalfH + gap + arrowSize/2 기존 blink 로직(scanCount % 2 → animateFloatAsState 180ms)은 그대로 동작 — tint color 의 alpha 에 arrowAlpha 를 곱하므로 PNG 도 매 refresh 깜빡임. Co-Authored-By: Claude Opus 4.7 --- .../ui/views/monitoring/PlacementGuideView.kt | 89 +++++++++++------- .../main/res/drawable/arrow_transparent.png | Bin 0 -> 1511 bytes 2 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_transparent.png diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index c06194d..8d9b8c6 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -368,6 +368,13 @@ fun PlacementGuideView(appState: AppState) { com.example.medilightv2android.R.drawable.sensor_device, ).asImageBitmap() } + // 방향 화살표 PNG (132x112, 우향). 회전 + tint 로 4방향 + 색감 통일. + val arrowBitmap = remember { + android.graphics.BitmapFactory.decodeResource( + context.resources, + com.example.medilightv2android.R.drawable.arrow_transparent, + ).asImageBitmap() + } Canvas(modifier = Modifier.fillMaxSize()) { val w = size.width val h = size.height @@ -416,41 +423,59 @@ fun PlacementGuideView(appState: AppState) { alpha = 0.95f, ) - // ── 방향 화살표 (센서/타원 바깥) — 매 refresh 마다 alpha 깜빡임 ── - // 색감 #FF5722 → #E64A19 (deep orange 700, 일러스트와 톤 맞춤) - // StrokeCap.Round로 끝부분 둥글게 + 굵기 9→10 (가독성) + // ── 방향 화살표 (PNG icon, rotate + tint) — 매 refresh 마다 alpha 깜빡임 ── + // arrow_transparent.png 는 우향(→) 132×112. 회전으로 4방향 처리. + // tint = deep orange 700 (#E64A19) — 일러스트 톤 맞춤 + alpha 로 blink. if (!greenZone) { - val arrowColor = Color(0xFFE64A19).copy(alpha = arrowAlpha) - val arrowLen = 70f - val arrowHead = 28f - val sw = 10f - val cap = androidx.compose.ui.graphics.StrokeCap.Round + val arrowDrawW = 90f + val arrowDrawH = arrowDrawW * (arrowBitmap.height.toFloat() / arrowBitmap.width.toFloat()) val sensorHalfH = h * 0.08f + val gap = 20f - when { - directionIcon.contains("up") -> { - val ay = bladderCy - bladderRy - sensorHalfH - 20f - drawLine(arrowColor, Offset(cx, ay + arrowLen), Offset(cx, ay), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(cx - arrowHead, ay + arrowHead), Offset(cx, ay), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(cx + arrowHead, ay + arrowHead), Offset(cx, ay), strokeWidth = sw, cap = cap) - } - directionIcon.contains("down") -> { - val ay = bladderCy + bladderRy + sensorHalfH + 20f - drawLine(arrowColor, Offset(cx, ay - arrowLen), Offset(cx, ay), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(cx - arrowHead, ay - arrowHead), Offset(cx, ay), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(cx + arrowHead, ay - arrowHead), Offset(cx, ay), strokeWidth = sw, cap = cap) - } - directionIcon.contains("left") -> { - val ax = cx - bladderRx - 30f - drawLine(arrowColor, Offset(ax + arrowLen, bladderCy), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(ax + arrowHead, bladderCy - arrowHead), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(ax + arrowHead, bladderCy + arrowHead), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) - } - directionIcon.contains("right") -> { - val ax = cx + bladderRx + 30f - drawLine(arrowColor, Offset(ax - arrowLen, bladderCy), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(ax - arrowHead, bladderCy - arrowHead), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) - drawLine(arrowColor, Offset(ax - arrowHead, bladderCy + arrowHead), Offset(ax, bladderCy), strokeWidth = sw, cap = cap) + // 화살표 center 위치 + 회전 각도 (icon 이 우향이므로 right=0°) + data class ArrowPlacement(val cxa: Float, val cya: Float, val rot: Float) + val placement: ArrowPlacement? = when { + directionIcon.contains("up") -> ArrowPlacement( + cx, + bladderCy - bladderRy - sensorHalfH - gap - arrowDrawH / 2f, + -90f, + ) + directionIcon.contains("down") -> ArrowPlacement( + cx, + bladderCy + bladderRy + sensorHalfH + gap + arrowDrawH / 2f, + 90f, + ) + directionIcon.contains("left") -> ArrowPlacement( + cx - bladderRx - gap - arrowDrawW / 2f, + bladderCy, + 180f, + ) + directionIcon.contains("right") -> ArrowPlacement( + cx + bladderRx + gap + arrowDrawW / 2f, + bladderCy, + 0f, + ) + else -> null + } + if (placement != null) { + val tint = androidx.compose.ui.graphics.ColorFilter.tint( + Color(0xFFE64A19).copy(alpha = arrowAlpha) + ) + rotate(degrees = placement.rot, pivot = Offset(placement.cxa, placement.cya)) { + drawImage( + image = arrowBitmap, + srcOffset = androidx.compose.ui.unit.IntOffset.Zero, + srcSize = androidx.compose.ui.unit.IntSize(arrowBitmap.width, arrowBitmap.height), + dstOffset = androidx.compose.ui.unit.IntOffset( + (placement.cxa - arrowDrawW / 2f).toInt(), + (placement.cya - arrowDrawH / 2f).toInt(), + ), + dstSize = androidx.compose.ui.unit.IntSize( + arrowDrawW.toInt(), + arrowDrawH.toInt(), + ), + colorFilter = tint, + ) } } } diff --git a/app/src/main/res/drawable/arrow_transparent.png b/app/src/main/res/drawable/arrow_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..7b4a6b810fa9eb02cc646fc52302bbc2281f494a GIT binary patch literal 1511 zcmV zL1-RD7>1viG(}8I6RnmM54Gw+sv)JIP?3O2QA9+o2dfvU^q@jPs{{qXi-@9vdeW0U z6|11A7eT=YMQ@>{s0b>hq1sZ3C`iHTVcMk||NVFN+u5DjKkozYDf97vyYpsu=iAwt zAz%n0&T&!f1VmcM5F&>RA#%tNB8Lnia>x)OhYTTdjKof$!;Aut0GojQ!1r;}3BwR7 zOjTt9_zhUo<8P1Q%yFjGi~^!tMl~)5ey6t?0Nw$fz+F8$i40MVhdkQ6dam7Y!?`qq zZW+}$TcPa^;0xfNhH_~H8KN3h3;{3$9Bv|!rjQ}3QPmIt)4++)i8O_tA*%7RA!aFf z77d~+L^Y;>Q--)jU^Z|rji4(;HBJLt4RJHTdx3Llj3KGAwgLYdG@n53Ws06aS2fqC zLEjfVIy$N7@L$Sbjt|Caa_k*ID(r~kpwbCIF16h8e<^piHo8UbD? z>HH$Ci;;8Y3egzDz#G8hhS)WqqB=x- zECbgWW4{B|2h61q8KPZ&1a3FPT?u@Io5I&Pv7f|OqpXFi1HgF0xkW!Yt5GAsizS_3 zq*rLRni>7%tX8eXeHd&p#U7lgWQYONIPP;liz)7GBB#t1A{tY`H^62?Z1Bl7GDI3| z0Dhr(nSp*x60k-gL!`q7Y@hx8g}e0xL3N09IDz|bugWFBhoO>?A<|+QTc6wuyhMOS zN@R#MxzrHzEU=FNrieZCl6q?n+i>(r>eVQsAGp+8Bfv8yonNHgHGVhxflI|22M(5W ze(1TG(GObcuPbnGNn5FAX0E8ndfd3VlS}V)kWIh`#@L&IZ>t3Ox@R5kO`FB3Qe0i@ z+t0pNhzZ;T4eO_BA}+^pYh0Z!Yv@~tcmSE9%UlKAf2a_sliw9!qiGsx^9;`rR(j$p(+8fU*1Ul)#}SF5;$O~SbF^}WvVklW>1aZ zjeg)#Z_Rmhd@t=qQ|Jax;~pK~OV3mDo6pP@A}K7Jy}gdR#nf7pN`^>_32e&_eFoee zA^{m99ju@7oyFZIf}ppZX>k5yUkjIoOhkr=&lGOSLd%!mTL`>5oeU9|G2ne{YeU>c zLmRKe9%8LlX-Od|lNKG5a?-MdQl{HHJAb7P;YJjV=9jrbw8AyO4~Dp3aht!M44cmX zks;c{T7`rb7e5bH;S!~MVvK9GG@v>}^9%!TV%sajQmm?l4ACsZz;oDk)0w2@-E__q z`>A`7rA^qROxmVV%A}24rA+sEcK%9zPNc0FFI46V5rkWSuMBZZz)isMu<2Ash6u;? zz!78YX5goQxtNh5!mtsyj+2GpdtTH*b%^?mVXs2YkdPTbhN#OhZWa0&L+n=IaM*Oh z5c{dT%i4u)L@}E{?nR28Kvy;A(eb^sGlAS?h``$B+4(CqpFr+1L}1N&cK%B3PayYF zWv&ojSSPtG;*69$8f8>tpE0fzW`GOs8^R0ASJ~5etPhzAx`89$Pi7kgT$(k zKFAQ&c-XVuqqt4pqfCLTgPtL(F$(-$(&lO4E#OS^>4ZmysLmMhN5S771U{ytW&jzY z4x_*wxUZ=yZE}_}GK9#Xhf0w{h7dVq2$4gE5IJNBkwb