Skip to content

Commit

Permalink
Add support for 'Colortemperaturepicker' widget (#3830)
Browse files Browse the repository at this point in the history
Signed-off-by: Danny Baumann <[email protected]>
  • Loading branch information
maniac103 authored Dec 2, 2024
1 parent ec93eec commit 5f58bf2
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.util.Locale
import java.util.regex.Pattern
import kotlin.math.roundToInt
import kotlinx.parcelize.Parcelize
import org.openhab.habdroid.util.asColorTemperatureToKelvin

@Parcelize
data class HsvState internal constructor(val hue: Float, val saturation: Float, val value: Float) : Parcelable {
Expand Down Expand Up @@ -206,6 +207,9 @@ fun ParsedState.NumberState?.withValue(value: Float): ParsedState.NumberState {
return ParsedState.NumberState(value, this?.unit, this?.format)
}

fun ParsedState.NumberState.toColorTemperatureInKelvin() =
ParsedState.NumberState(value.asColorTemperatureToKelvin(), "K", "%.0f %unit%")

/**
* Parses a state string into the parsed representation.
*
Expand Down
1 change: 1 addition & 0 deletions mobile/src/main/java/org/openhab/habdroid/model/Widget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ data class Widget(
Input,
Buttongrid,
Button,
Colortemperaturepicker,
Unknown
}

Expand Down
52 changes: 45 additions & 7 deletions mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import org.openhab.habdroid.model.Item
import org.openhab.habdroid.model.LabeledValue
import org.openhab.habdroid.model.ParsedState
import org.openhab.habdroid.model.Widget
import org.openhab.habdroid.model.toColorTemperatureInKelvin
import org.openhab.habdroid.model.withValue
import org.openhab.habdroid.ui.widget.AutoHeightPlayerView
import org.openhab.habdroid.ui.widget.ContextMenuAwareRecyclerView
Expand All @@ -112,6 +113,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.asColorTemperatureInKelvinToColor
import org.openhab.habdroid.util.beautify
import org.openhab.habdroid.util.determineDataUsagePolicy
import org.openhab.habdroid.util.getChartTheme
Expand All @@ -120,6 +122,7 @@ import org.openhab.habdroid.util.getImageWidgetScalingType
import org.openhab.habdroid.util.getPrefs
import org.openhab.habdroid.util.orDefaultIfEmpty
import org.openhab.habdroid.util.resolveThemedColor
import org.openhab.habdroid.util.toColoredRoundedRect

/**
* This class provides openHAB widgets adapter for list view.
Expand Down Expand Up @@ -239,6 +242,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)
Expand Down Expand Up @@ -389,6 +393,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
Expand Down Expand Up @@ -1643,6 +1648,7 @@ class WidgetAdapter(
View.OnLongClickListener {
private val upButton = itemView.findViewById<View>(R.id.up_button)
private val downButton = itemView.findViewById<View>(R.id.down_button)
private val selectColorButton = itemView.findViewById<ImageView>(R.id.select_color_button)

data class UpDownButtonState(
val item: Item?,
Expand All @@ -1656,7 +1662,6 @@ class WidgetAdapter(
b.setOnClickListener(this)
b.setOnLongClickListener(this)
}
val selectColorButton = itemView.findViewById<View>(R.id.select_color_button)
selectColorButton.setOnClickListener { handleRowClick() }
}

Expand All @@ -1670,6 +1675,14 @@ class WidgetAdapter(
downButton.tag = UpDownButtonState(widget.item, "OFF", "DECREASE")
}
super.bind(widget)

val hsv = widget.state?.asHsv
val color = hsv?.toColor()
if (color == null || hsv.value == 0F) {
selectColorButton.setImageResource(R.drawable.ic_palette_outline_themed_24dp)
} else {
selectColorButton.setImageDrawable(color.toColoredRoundedRect(selectColorButton.context))
}
}

override fun onClick(view: View) {
Expand Down Expand Up @@ -1702,6 +1715,30 @@ class WidgetAdapter(
}
}

class ColorTemperatureViewHolder internal constructor(initData: ViewHolderInitData) : LabeledItemBaseViewHolder(
initData,
R.layout.widgetlist_colortemperatureitem,
R.layout.widgetlist_colortemperatureitem_compact
) {
private val previewImage = itemView.findViewById<ImageView>(R.id.current_temperature)

override fun bind(widget: Widget) {
super.bind(widget)
val drawable = (widget.state ?: widget.item?.state)
?.asNumber
?.toColorTemperatureInKelvin()
?.value
?.asColorTemperatureInKelvinToColor()
?.toColoredRoundedRect(previewImage.context)
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
Expand Down Expand Up @@ -1808,12 +1845,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,16 +32,20 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.Widget
import org.openhab.habdroid.model.toColorTemperatureInKelvin
import org.openhab.habdroid.model.withValue
import org.openhab.habdroid.ui.widget.WidgetSlider
import org.openhab.habdroid.util.ColorPickerHelper
import org.openhab.habdroid.util.asColorTemperatureInKelvinToColor
import org.openhab.habdroid.util.asColorTemperatureToKelvin
import org.openhab.habdroid.util.parcelable

open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() {
Expand Down Expand Up @@ -67,10 +74,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<WidgetSlider>(R.id.slider).apply {
Expand Down Expand Up @@ -98,6 +103,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<WidgetSlider>(R.id.slider).apply {
addOnLayoutChangeListener(this@ColorTemperatureSliderBottomSheet)
setLabelFormatter { value -> "${value.roundToInt()} K" }
}
return v
}

override suspend fun onValueUpdate(value: Float) {
val item = widget.item ?: return
val state = widget.state?.asNumber?.toColorTemperatureInKelvin()?.withValue(value)
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)
}

private fun applyColorTemperatureGradientToTrack(slider: WidgetSlider, width: Int, height: Int) {
val min = widget.minValue.asColorTemperatureToKelvin()
val max = widget.maxValue.asColorTemperatureToKelvin()
val steps = 20
val positions = (0 until steps).map { 1F * it / steps }.toFloatArray()
val colors = positions
.map { it * (max - min) + min }
.map { it.asColorTemperatureInKelvinToColor() }
.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)
Expand Down
45 changes: 45 additions & 0 deletions mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
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
Expand All @@ -53,6 +55,9 @@ import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.ShapeAppearanceModel
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
Expand All @@ -68,8 +73,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
Expand Down Expand Up @@ -151,6 +158,44 @@ fun HttpUrl.toRelativeUrl(): String {
return this.toString().substring(base.toString().length - 1)
}

fun Float.asColorTemperatureToKelvin(): Float = if (this < 1000) {
// likely Mirek
1000000F / this
} else {
this
}

fun Float.asColorTemperatureInKelvinToColor(): Int {
// For algorithm, see https://web.archive.org/web/20151024031939/http://www.zombieprototypes.com/?p=210
val temp = (this.coerceIn(1000F, 10000F) / 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())
}

fun Int.toColoredRoundedRect(context: Context) = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = ColorStateList.valueOf(this@toColoredRoundedRect)
shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(RelativeCornerSize(0.15f))
.build()
}

/**
* This method converts dp unit to equivalent pixels, depending on device density.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?colorOnBackground">
<path
android:fillColor="#FF000000"
android:pathData="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6 -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06 0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22 0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38 1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z"/>
Expand Down
8 changes: 5 additions & 3 deletions mobile/src/main/res/layout/widgetlist_coloritem_buttons.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
android:contentDescription="@string/content_description_color_up"
app:icon="@drawable/ic_keyboard_arrow_up_themed_24dp" />

<Button
<ImageView
style="@style/openHAB.ActionButton"
android:id="@+id/select_color_button"
android:contentDescription="@string/content_description_open_color_wheel"
app:icon="@drawable/ic_palette_outline_themed_24dp" />
android:background="@null"
android:padding="8dp"
android:scaleType="centerInside"
android:contentDescription="@string/content_description_open_color_wheel" />

<Button
style="@style/openHAB.ActionButton"
Expand Down
20 changes: 20 additions & 0 deletions mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
style="@style/WidgetListItemContainer">

<include layout="@layout/widgetlist_icontext" />

<ImageView
android:id="@+id/current_temperature"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:scaleType="centerInside"
tools:src="@android:color/black" />

</LinearLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
style="@style/WidgetListItemContainerCompact">

<include layout="@layout/widgetlist_icontext_compact" />

<ImageView
android:id="@+id/current_temperature"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
tools:src="@android:color/black" />

</LinearLayout>

0 comments on commit 5f58bf2

Please sign in to comment.