From f965def6ce1478bde5598ac27cd3909fb7244533 Mon Sep 17 00:00:00 2001 From: Danny Baumann Date: Wed, 20 Nov 2024 20:29:48 +0100 Subject: [PATCH] Add support for 'Colortemperaturepicker' widget Signed-off-by: Danny Baumann --- .../java/org/openhab/habdroid/model/Widget.kt | 1 + .../org/openhab/habdroid/ui/WidgetAdapter.kt | 51 +++++++++++++++-- .../habdroid/ui/WidgetDetailBottomSheets.kt | 56 +++++++++++++++++-- .../openhab/habdroid/util/ExtensionFuncs.kt | 27 +++++++++ .../widgetlist_colortemperatureitem.xml | 18 ++++++ ...idgetlist_colortemperatureitem_compact.xml | 19 +++++++ 6 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml create mode 100644 mobile/src/main/res/layout/widgetlist_colortemperatureitem_compact.xml diff --git a/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt b/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt index 290f72b41c..d540172815 100644 --- a/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt +++ b/mobile/src/main/java/org/openhab/habdroid/model/Widget.kt @@ -129,6 +129,7 @@ data class Widget( Input, Buttongrid, Button, + Colortemperaturepicker, Unknown } diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt index 93451cefd5..85a42529d2 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt @@ -70,6 +70,9 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.materialswitch.MaterialSwitch +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.RelativeCornerSize +import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.google.android.material.timepicker.MaterialTimePicker @@ -112,6 +115,7 @@ import org.openhab.habdroid.util.IconBackground import org.openhab.habdroid.util.ImageConversionPolicy import org.openhab.habdroid.util.MjpegStreamer import org.openhab.habdroid.util.PrefKeys +import org.openhab.habdroid.util.asColorTemperatureToColor import org.openhab.habdroid.util.beautify import org.openhab.habdroid.util.determineDataUsagePolicy import org.openhab.habdroid.util.getChartTheme @@ -239,6 +243,7 @@ class WidgetAdapter( TYPE_VIDEO -> VideoViewHolder(initData) TYPE_WEB -> WebViewHolder(initData) TYPE_COLOR -> ColorViewHolder(initData) + TYPE_COLORTEMPERATURE-> ColorTemperatureViewHolder(initData) TYPE_VIDEO_MJPEG -> MjpegVideoViewHolder(initData) TYPE_LOCATION -> MapViewHelper.createViewHolder(initData) TYPE_INPUT -> InputViewHolder(initData) @@ -389,6 +394,7 @@ class WidgetAdapter( } Widget.Type.Webview -> TYPE_WEB Widget.Type.Colorpicker -> TYPE_COLOR + Widget.Type.Colortemperaturepicker -> TYPE_COLORTEMPERATURE Widget.Type.Mapview -> TYPE_LOCATION Widget.Type.Input -> if (widget.shouldUseDateTimePickerForInput()) TYPE_DATETIMEINPUT else TYPE_INPUT Widget.Type.Buttongrid -> TYPE_BUTTONGRID @@ -1702,6 +1708,38 @@ class WidgetAdapter( } } + class ColorTemperatureViewHolder internal constructor(initData: ViewHolderInitData) : LabeledItemBaseViewHolder( + initData, + R.layout.widgetlist_colortemperatureitem, + R.layout.widgetlist_colortemperatureitem_compact + ) { + private val previewImage = itemView.findViewById(R.id.current_temperature) + + override fun bind(widget: Widget) { + super.bind(widget) + val state = (widget.state ?: widget.item?.state)?.asNumber + if (state == null) { + // TODO: question mark + } else { + // if not Kelvin, assume Mirek + val kelvin = if (state.unit == "K") state.value else 1000000F / state.value + val color = kelvin.asColorTemperatureToColor() + val drawable = MaterialShapeDrawable.createWithElevationOverlay(previewImage.context).apply { + fillColor = ColorStateList.valueOf(color) + shapeAppearanceModel = ShapeAppearanceModel.Builder() + .setAllCornerSizes(RelativeCornerSize(0.15f)) + .build() + } + previewImage.setImageDrawable(drawable) + } + } + + override fun handleRowClick() { + val widget = boundWidget ?: return + fragmentPresenter.showBottomSheet(ColorTemperatureSliderBottomSheet(), widget) + } + } + class MjpegVideoViewHolder internal constructor(initData: ViewHolderInitData) : HeavyDataViewHolder(initData, R.layout.widgetlist_videomjpegitem) { private val imageView = widgetContentView as WidgetImageView @@ -1808,12 +1846,13 @@ class WidgetAdapter( private const val TYPE_VIDEO = 15 private const val TYPE_WEB = 16 private const val TYPE_COLOR = 17 - private const val TYPE_VIDEO_MJPEG = 18 - private const val TYPE_LOCATION = 19 - private const val TYPE_INPUT = 20 - private const val TYPE_DATETIMEINPUT = 21 - private const val TYPE_BUTTONGRID = 22 - private const val TYPE_INVISIBLE = 23 + private const val TYPE_COLORTEMPERATURE = 18 + private const val TYPE_VIDEO_MJPEG = 19 + private const val TYPE_LOCATION = 20 + private const val TYPE_INPUT = 21 + private const val TYPE_DATETIMEINPUT = 22 + private const val TYPE_BUTTONGRID = 23 + private const val TYPE_INVISIBLE = 24 private fun toInternalViewType(viewType: Int, compactMode: Boolean): Int { return viewType or (if (compactMode) 0x100 else 0) diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetDetailBottomSheets.kt b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetDetailBottomSheets.kt index 26d4f9520d..36beaeef60 100644 --- a/mobile/src/main/java/org/openhab/habdroid/ui/WidgetDetailBottomSheets.kt +++ b/mobile/src/main/java/org/openhab/habdroid/ui/WidgetDetailBottomSheets.kt @@ -13,6 +13,9 @@ package org.openhab.habdroid.ui +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.Shader import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -35,10 +38,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import org.openhab.habdroid.R import org.openhab.habdroid.core.connection.Connection +import org.openhab.habdroid.model.ParsedState import org.openhab.habdroid.model.Widget import org.openhab.habdroid.model.withValue import org.openhab.habdroid.ui.widget.WidgetSlider import org.openhab.habdroid.util.ColorPickerHelper +import org.openhab.habdroid.util.asColorTemperatureToColor import org.openhab.habdroid.util.parcelable open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() { @@ -67,10 +72,8 @@ open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() { } } -class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListener { - private var updateJob: Job? = null - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { +open class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListener { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = inflater.inflate(R.layout.bottom_sheet_setpoint, container, false) view.findViewById(R.id.slider).apply { @@ -98,6 +101,51 @@ class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListen } } +class ColorTemperatureSliderBottomSheet : SliderBottomSheet(), View.OnLayoutChangeListener { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val v = super.onCreateView(inflater, container, savedInstanceState) + v.findViewById(R.id.slider).apply { + addOnLayoutChangeListener(this@ColorTemperatureSliderBottomSheet) + } + return v + } + + override suspend fun onValueUpdate(value: Float) { + val item = widget.item ?: return + val state = ParsedState.NumberState(value, "K", "%.0f %unit%") + Log.d(TAG, "Send state $state for ${item.name}") + connection?.httpClient?.sendItemUpdate(item, state) + } + + override fun onLayoutChange(view: View, l: Int, t: Int, r: Int, b: Int, ol: Int, ot: Int, or: Int, ob: Int) { + applyColorTemperatureGradientToTrack(view as WidgetSlider, r - l, b - t) + view.removeOnLayoutChangeListener(this) + } + + private fun applyColorTemperatureGradientToTrack(slider: WidgetSlider, width: Int, height: Int) { + val min = widget.minValue + val max = widget.maxValue + val steps = 20 + val positions = (0 until steps).map { 1F * it / steps }.toFloatArray() + val colors = positions + .map { it * (max - min) + min } + .map { it.asColorTemperatureToColor() } + .toIntArray() + val shader = LinearGradient(0F, 0F, width.toFloat(), height.toFloat(), colors, positions, Shader.TileMode.CLAMP) + + listOf("activeTrackPaint", "inactiveTrackPaint") + .map { Slider::class.java.superclass.getDeclaredField(it) } + .forEach { field -> + field.isAccessible = true + (field.get(slider) as Paint).shader = shader + } + } + + companion object { + private val TAG = SliderBottomSheet::class.java.simpleName + } +} + class SelectionBottomSheet : AbstractWidgetBottomSheet() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.bottom_sheet_selection, container, false) diff --git a/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt b/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt index 978fe6e66d..ee359318d3 100644 --- a/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt +++ b/mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt @@ -28,6 +28,7 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas +import android.graphics.Color import android.net.ConnectivityManager import android.net.Network import android.net.Uri @@ -68,8 +69,10 @@ import javax.jmdns.ServiceInfo import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException +import kotlin.math.ln import kotlin.math.max import kotlin.math.round +import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -151,6 +154,30 @@ fun HttpUrl.toRelativeUrl(): String { return this.toString().substring(base.toString().length - 1) } +fun Float.asColorTemperatureToColor(): Int { + // For algorithm, see https://web.archive.org/web/20151024031939/http://www.zombieprototypes.com/?p=210 + val temp = (this / 100F).toDouble() + // calculates a + bx + c * ln(x) + val approximate = { x: Double, a: Double, b: Double, c: Double -> a + b * x + c * ln(x) } + val red = when { + temp <= 66 -> 255.0 + else -> approximate(temp - 55, 351.97690566805693, 0.114206453784165, -40.25366309332127) + }.coerceIn(0.0, 255.0) + + val green = when { + temp <= 66 -> approximate(temp - 2, -155.25485562709179, -0.44596950469579133, 104.49216199393888) + else -> approximate(temp - 50, 325.4494125711974, 0.07943456536662342, -28.0852963507957) + }.coerceIn(0.0, 255.0) + + val blue = when { + temp < 20 -> 0.0 + temp > 66 -> 255.0 + else -> approximate(temp - 10, -254.76935184120902, 0.8274096064007395, 115.67994401066147) + }.coerceIn(0.0, 255.0) + + return Color.argb(255, red.roundToInt(), green.roundToInt(), blue.roundToInt()) +} + /** * This method converts dp unit to equivalent pixels, depending on device density. * diff --git a/mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml b/mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml new file mode 100644 index 0000000000..b07df6ca8f --- /dev/null +++ b/mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/mobile/src/main/res/layout/widgetlist_colortemperatureitem_compact.xml b/mobile/src/main/res/layout/widgetlist_colortemperatureitem_compact.xml new file mode 100644 index 0000000000..8f85339a63 --- /dev/null +++ b/mobile/src/main/res/layout/widgetlist_colortemperatureitem_compact.xml @@ -0,0 +1,19 @@ + + + + + + + +