fix(ui): improve sensor unit layout and resolve dialog rendering errors
All checks were successful
Update Wiki Documentation / generate-docs (push) Successful in 2m20s
All checks were successful
Update Wiki Documentation / generate-docs (push) Successful in 2m20s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
package com.example.retroha.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
@@ -31,17 +32,27 @@ class LightControlDialog(
|
||||
private val initialBrightness: Int,
|
||||
private val onBrightnessChanged: (Int) -> Unit
|
||||
) : Dialog(context) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
// Disable hardware acceleration for this dialog window to avoid EGL errors on old hardware
|
||||
window?.setFlags(
|
||||
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
|
||||
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||
)
|
||||
|
||||
val density = context.resources.displayMetrics.density
|
||||
fun dp(v: Int) = (v * density + 0.5f).toInt()
|
||||
|
||||
val root = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(Colors.WHITE)
|
||||
setPadding(dp(24), dp(24), dp(24), dp(24))
|
||||
layoutParams = LinearLayout.LayoutParams(dp(320), ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
root.addView(TextView(context).apply {
|
||||
text = entityName.uppercase()
|
||||
typeface = android.graphics.Typeface.MONOSPACE
|
||||
@@ -49,6 +60,7 @@ class LightControlDialog(
|
||||
setTextColor(Colors.BLACK)
|
||||
setPadding(0, 0, 0, dp(16))
|
||||
})
|
||||
|
||||
root.addView(View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(2))
|
||||
setBackgroundColor(Colors.BLACK)
|
||||
@@ -80,11 +92,15 @@ class LightControlDialog(
|
||||
})
|
||||
}
|
||||
root.addView(seekBar)
|
||||
|
||||
root.addView(View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(1, dp(16))
|
||||
})
|
||||
|
||||
setContentView(root)
|
||||
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
// Use a simple solid background for the window to avoid match errors on old drivers
|
||||
window?.setBackgroundDrawable(ColorDrawable(Colors.WHITE))
|
||||
setCanceledOnTouchOutside(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package com.example.retroha.ui
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
@@ -17,6 +18,7 @@ import com.example.retroha.model.WidgetConfig
|
||||
import com.example.retroha.model.WidgetInteraction
|
||||
import com.example.retroha.model.toWidgetInteraction
|
||||
import com.example.retroha.theme.Colors
|
||||
|
||||
/**
|
||||
* A highly optimized custom [View] that renders a single Home Assistant entity card.
|
||||
* Part of the "Bauhaus Canvas" engine, it avoids XML overhead by manually drawing everything.
|
||||
@@ -51,16 +53,19 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
var onToggle: ((WidgetConfig) -> Unit)? = null
|
||||
/** Callback for long clicks (more info / brightness dialog). */
|
||||
var onLongToggle: ((WidgetConfig) -> Unit)? = null
|
||||
|
||||
private var config: WidgetConfig? = null
|
||||
private var currentInteraction: WidgetInteraction = WidgetInteraction.READ_ONLY
|
||||
private var lLabel: StaticLayout? = null
|
||||
private var lValue: StaticLayout? = null
|
||||
private var lSecondary: StaticLayout? = null
|
||||
private var pulseAnim: ObjectAnimator? = null
|
||||
|
||||
private var cachedLabel = ""
|
||||
private var cachedValue = ""
|
||||
private var cachedSecondary = ""
|
||||
private var cachedTextW = 0
|
||||
|
||||
init {
|
||||
isFocusable = true
|
||||
setOnClickListener {
|
||||
@@ -73,15 +78,18 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val w = MeasureSpec.getSize(widthMeasureSpec)
|
||||
setMeasuredDimension(w, cardHeightPx + shadowPx)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
updateExecIconPath(w - shadowPx)
|
||||
rebuildLayoutsIfNeeded(w)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds new data to the widget and updates the visual state.
|
||||
* Triggers layout recalculation if text content or view width has changed.
|
||||
@@ -93,12 +101,17 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
currentInteraction = cfg.domain.toWidgetInteraction()
|
||||
isClickable = currentInteraction != WidgetInteraction.READ_ONLY
|
||||
&& cfg.state != EntityState.UNAVAILABLE
|
||||
&& cfg.state != EntityState.TOGGLING
|
||||
tpValue.color = if (cfg.state == EntityState.UNAVAILABLE) Colors.GRAY_MID else Colors.BLACK
|
||||
if (cfg.state == EntityState.TOGGLING) startPulse() else stopPulse()
|
||||
if (width > 0) rebuildLayoutsIfNeeded(width)
|
||||
|
||||
if (cfg.state == EntityState.TOGGLING) {
|
||||
startPulse()
|
||||
} else {
|
||||
stopPulse()
|
||||
}
|
||||
|
||||
rebuildLayoutsIfNeeded(width)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun updateExecIconPath(cardW: Int) {
|
||||
val sz = execIconSize
|
||||
val right = cardW.toFloat() - dp(8)
|
||||
@@ -109,37 +122,59 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
execIconPath.lineTo(right - sz, top + sz)
|
||||
execIconPath.close()
|
||||
}
|
||||
|
||||
private fun rebuildLayoutsIfNeeded(viewW: Int) {
|
||||
val cfg = config ?: return
|
||||
val cardW = viewW - shadowPx
|
||||
val textW = (cardW - textLeft - textRight).coerceAtLeast(1)
|
||||
|
||||
val labelChanged = cfg.label != cachedLabel
|
||||
val textChanged = labelChanged || cfg.value != cachedValue || cfg.secondary != cachedSecondary
|
||||
val widthChanged = textW != cachedTextW
|
||||
|
||||
if (!textChanged && !widthChanged && lLabel != null) return
|
||||
|
||||
val labelUp = if (labelChanged) cfg.label.uppercase() else cachedLabel
|
||||
|
||||
// For sensors/numbers, append unit to value to prevent line wrapping
|
||||
val valueText = if (cfg.secondary.length <= 3 && cfg.secondary.isNotEmpty()) {
|
||||
"${cfg.value} ${cfg.secondary}"
|
||||
} else {
|
||||
cfg.value
|
||||
}
|
||||
|
||||
val secondaryText = if (cfg.secondary.length <= 3) "" else cfg.secondary
|
||||
|
||||
lLabel = makeLayout(labelUp, tpLabel, textW)
|
||||
lValue = makeLayout(cfg.value, tpValue, textW)
|
||||
lSecondary = makeLayout(cfg.secondary, tpSecondary, textW)
|
||||
lValue = makeLayout(valueText, tpValue, textW)
|
||||
lSecondary = makeLayout(secondaryText, tpSecondary, textW)
|
||||
|
||||
cachedLabel = cfg.label
|
||||
cachedValue = cfg.value
|
||||
cachedSecondary = cfg.secondary
|
||||
cachedTextW = textW
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val cfg = config ?: return
|
||||
val cardW = (width - shadowPx).toFloat()
|
||||
val cardH = cardHeightPx.toFloat()
|
||||
val b = borderPx.toFloat()
|
||||
val s = shadowPx.toFloat()
|
||||
|
||||
// Shadow
|
||||
paintFill.color = Colors.BLACK
|
||||
canvas.drawRect(s, s, width.toFloat(), cardH + s, paintFill)
|
||||
|
||||
// Border
|
||||
paintFill.color = when (cfg.state) {
|
||||
EntityState.TOGGLING -> Colors.BORDER_TOGGLING
|
||||
EntityState.UNAVAILABLE -> Colors.BORDER_UNAVAILABLE
|
||||
else -> Colors.BORDER_DEFAULT
|
||||
}
|
||||
canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
|
||||
|
||||
// Background
|
||||
paintFill.color = when (cfg.state) {
|
||||
EntityState.ON -> Colors.STATUS_ON
|
||||
EntityState.OFF -> Colors.STATUS_OFF
|
||||
@@ -147,16 +182,24 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
EntityState.TOGGLING -> Colors.STATUS_TOGGLING
|
||||
}
|
||||
canvas.drawRect(b, b, cardW - b, cardH - b, paintFill)
|
||||
|
||||
// Domain Stripe
|
||||
paintFill.color = stripeColor(cfg.domain)
|
||||
canvas.drawRect(b, b, b + stripePx, cardH - b, paintFill)
|
||||
|
||||
// Icon
|
||||
val iconSize = dp(16).toFloat()
|
||||
val iconX = cardW - iconSize - dp(8)
|
||||
val iconY = dp(8).toFloat()
|
||||
HaIcons.draw(canvas, cfg.domain, iconX, iconY, iconSize, paintFill.color)
|
||||
|
||||
// Execute Icon (triangle)
|
||||
if (currentInteraction == WidgetInteraction.EXECUTE && cfg.state != EntityState.UNAVAILABLE) {
|
||||
paintFill.color = Colors.GRAY_MID
|
||||
canvas.drawPath(execIconPath, paintFill)
|
||||
}
|
||||
|
||||
// Text Layouts
|
||||
val tl = lLabel; val tv = lValue; val ts = lSecondary
|
||||
if (tl != null && tv != null && ts != null) {
|
||||
canvas.save()
|
||||
@@ -164,19 +207,25 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
tl.draw(canvas)
|
||||
canvas.translate(0f, (tl.height + lineGap1).toFloat())
|
||||
tv.draw(canvas)
|
||||
canvas.translate(0f, (tv.height + lineGap2).toFloat())
|
||||
ts.draw(canvas)
|
||||
if (ts.text.isNotEmpty()) {
|
||||
canvas.translate(0f, (tv.height + lineGap2).toFloat())
|
||||
ts.draw(canvas)
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
// Press overlay
|
||||
if (isPressed) {
|
||||
paintFill.color = 0x22000000
|
||||
canvas.drawRect(0f, 0f, cardW, cardH, paintFill)
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawableStateChanged() {
|
||||
super.drawableStateChanged()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun startPulse() {
|
||||
if (pulseAnim?.isRunning == true) return
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
@@ -188,12 +237,14 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopPulse() {
|
||||
pulseAnim?.cancel()
|
||||
pulseAnim = null
|
||||
setLayerType(LAYER_TYPE_NONE, null)
|
||||
alpha = 1f
|
||||
setLayerType(LAYER_TYPE_NONE, null)
|
||||
}
|
||||
|
||||
private fun stripeColor(domain: String): Int = when (domain) {
|
||||
"light" -> Colors.STRIPE_LIGHT
|
||||
"switch" -> Colors.STRIPE_SWITCH
|
||||
@@ -203,15 +254,19 @@ class WidgetCardView(context: Context) : View(context) {
|
||||
"automation" -> Colors.STRIPE_AUTOMATION
|
||||
else -> Colors.STRIPE_DEFAULT
|
||||
}
|
||||
|
||||
private fun dp(v: Int) = (v * density + 0.5f).toInt()
|
||||
private fun sp(v: Int) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, v.toFloat(), resources.displayMetrics)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun makeLayout(text: String, paint: TextPaint, width: Int): StaticLayout =
|
||||
if (Build.VERSION.SDK_INT >= 23)
|
||||
StaticLayout.Builder
|
||||
.obtain(text, 0, text.length, paint, width)
|
||||
.setMaxLines(1)
|
||||
private fun makeLayout(text: CharSequence, paint: TextPaint, width: Int): StaticLayout =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
|
||||
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
||||
.setLineSpacing(0f, 1f)
|
||||
.setIncludePad(false)
|
||||
.setEllipsize(TextUtils.TruncateAt.END)
|
||||
.setMaxLines(1)
|
||||
.build()
|
||||
else
|
||||
StaticLayout(
|
||||
|
||||
Reference in New Issue
Block a user