Skip to content

Commit

Permalink
Add support for 'Colortemperaturepicker' widget
Browse files Browse the repository at this point in the history
Signed-off-by: Danny Baumann <[email protected]>
  • Loading branch information
maniac103 committed Nov 20, 2024
1 parent ec93eec commit f965def
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 10 deletions.
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
51 changes: 45 additions & 6 deletions mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ImageView>(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
Expand Down Expand Up @@ -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)
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 @@ -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() {
Expand Down Expand Up @@ -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<WidgetSlider>(R.id.slider).apply {
Expand Down Expand Up @@ -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<WidgetSlider>(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)
Expand Down
27 changes: 27 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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
18 changes: 18 additions & 0 deletions mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?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_iconvaluetext" />

<ImageView
android:id="@+id/current_temperature"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
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_iconvaluetext_compact" />

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

</LinearLayout>

0 comments on commit f965def

Please sign in to comment.