Merge pull request #2857 from Philoul/wear/new_custom_watchface

Wear CWFNew customization capabilities with Dyn Datas
This commit is contained in:
Milos Kozak 2023-10-04 18:55:49 +02:00 committed by GitHub
commit ed4c9792d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 191 additions and 34 deletions

View file

@ -238,7 +238,20 @@ enum class JsonKeys(val key: String) {
ALLCAPS("allCaps"),
DAYNAMEFORMAT("dayNameFormat"),
MONTHFORMAT("monthFormat"),
BACKGROUND("background")
BACKGROUND("background"), // Background image for textView
LEFTOFFSET("leftOffset"),
TOPOFFSET("topOffset"),
ROTATIONOFFSET("rotationOffset"),
DYNDATA("dynData"), //Bloc of DynDatas definition, and DynData keyValue within view
VALUEKEY("valueKey"), // Indentify which value (default is View Value)
MINDATA("minData"), // Min data Value (default defined for each value, note unit mg/dl for all bg, deltas)
MAXDATA("maxData"), // Max data idem min data (note all value below min or above max will be considered as equal min or mas)
MINVALUE("minValue"), // min returned value (when data value equals minData
MAXVALUE("maxValue"), //
INVALIDVALUE("invalidValue"),
IMAGE("image"),
INVALIDIMAGE("invalidImage"),
INVALIDCOLOR("invalidColor")
}
enum class JsonKeyValues(val key: String) {
@ -260,7 +273,8 @@ enum class JsonKeyValues(val key: String) {
BOLD("bold"),
BOLD_ITALIC("bold_italic"),
ITALIC("italic"),
BGCOLOR("bgColor")
BGCOLOR("bgColor"),
SGVLEVEL("sgvLevel")
}
class ZipWatchfaceFormat {

View file

@ -48,10 +48,12 @@ import app.aaps.core.interfaces.rx.weardata.isEquals
import app.aaps.wear.R
import app.aaps.wear.databinding.ActivityCustomBinding
import app.aaps.wear.watchfaces.utils.BaseWatchFace
import org.joda.time.DateTime
import org.joda.time.TimeOfDay
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import javax.inject.Inject
import kotlin.math.floor
@SuppressLint("UseCompatLoadingForDrawables")
class CustomWatchface : BaseWatchFace() {
@ -63,6 +65,7 @@ class CustomWatchface : BaseWatchFace() {
private val TEMPLATE_RESOLUTION = 400
private var lowBatColor = Color.RED
private var resDataMap: CwfResDataMap = mutableMapOf()
private var jsonString = ""
private val bgColor: Int
get() = when (singleBg.sgvLevel) {
1L -> highColor
@ -102,13 +105,15 @@ class CustomWatchface : BaseWatchFace() {
override fun setColorDark() {
setWatchfaceStyle()
if ((ViewMap.SGV.dynData?.stepColor ?: 0) == 0)
binding.sgv.setTextColor(bgColor)
if ((ViewMap.DIRECTION.dynData?.stepColor ?: 0) == 0)
binding.direction2.colorFilter = changeDrawableColor(bgColor)
if (ageLevel != 1)
if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepColor ?: 0) == 0)
binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld))
if (status.batteryLevel != 1)
if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepColor ?: 0) == 0)
binding.uploaderBattery.setTextColor(lowBatColor)
if ((ViewMap.LOOP.dynData?.stepDraw ?: 0) == 0) // Apply automatic background image only if no dynData or no step images
when (loopLevel) {
-1 -> binding.loop.setBackgroundResource(R.drawable.loop_grey_25)
1 -> binding.loop.setBackgroundResource(R.drawable.loop_green_25)
@ -147,11 +152,13 @@ class CustomWatchface : BaseWatchFace() {
updatePref(it.customWatchfaceData.metadata)
try {
val json = JSONObject(it.customWatchfaceData.json)
if (!resDataMap.isEquals(it.customWatchfaceData.resDatas)) {
if (!resDataMap.isEquals(it.customWatchfaceData.resDatas) || jsonString != it.customWatchfaceData.json) {
resDataMap = it.customWatchfaceData.resDatas
jsonString = it.customWatchfaceData.json
FontMap.init(this)
ViewMap.init(this)
TrendArrowMap.init(this)
DynProvider.init(json.optJSONObject(DYNDATA.key))
}
enableSecond = json.optBoolean(ENABLESECOND.key) && sp.getBoolean(R.string.key_show_seconds, true)
highColor = getColor(json.optString(HIGHCOLOR.key), ContextCompat.getColor(this, R.color.dark_highColor))
@ -174,6 +181,7 @@ class CustomWatchface : BaseWatchFace() {
when (view) {
is TextView -> viewMap.customizeTextView(view, viewJson)
is ImageView -> viewMap.customizeImageView(view, viewJson)
is lecho.lib.hellocharts.view.LineChartView -> viewMap.customizeGraphView(view, viewJson)
else -> viewMap.customizeViewCommon(view, viewJson)
}
} ?: apply {
@ -446,6 +454,7 @@ class CustomWatchface : BaseWatchFace() {
var height = 0
var left = 0
var top = 0
var dynData: DynProvider? = null
var rangeCustom: Drawable? = null
get() = field ?: customDrawable?.let { cd -> cwf.resDataMap[cd.fileName]?.toDrawable(cwf.resources).also { rangeCustom = it } }
var highCustom: Drawable? = null
@ -454,7 +463,7 @@ class CustomWatchface : BaseWatchFace() {
get() = field ?: customLow?.let { cd -> cwf.resDataMap[cd.fileName]?.toDrawable(cwf.resources).also { lowCustom = it } }
var textDrawable: Drawable? = null
val drawable: Drawable?
get() = when (cwf.singleBg.sgvLevel) {
get() = dynData?.getDrawable() ?: when (cwf.singleBg.sgvLevel) {
1L -> highCustom ?: rangeCustom
0L -> rangeCustom
-1L -> lowCustom ?: rangeCustom
@ -472,58 +481,68 @@ class CustomWatchface : BaseWatchFace() {
left = (viewJson.optInt(LEFTMARGIN.key) * cwf.zoomFactor).toInt()
top = (viewJson.optInt(TOPMARGIN.key) * cwf.zoomFactor).toInt()
val params = FrameLayout.LayoutParams(width, height)
params.topMargin = top
params.leftMargin = left
dynData = DynProvider.getDyn(cwf, viewJson.optString(DYNDATA.key),width, height, key)
val topOffset = if (viewJson.optBoolean(TOPOFFSET.key, false)) dynData?.getTopOffset() ?:0 else 0
params.topMargin = top + topOffset
val leftOffset = if (viewJson.optBoolean(LEFTOFFSET.key, false)) dynData?.getLeftOffset() ?:0 else 0
params.leftMargin = left + leftOffset
view.layoutParams = params
view.visibility = cwf.setVisibility(viewJson.optString(VISIBILITY.key, JsonKeyValues.GONE.key), visibility())
val rotationOffset = if (viewJson.optBoolean(ROTATIONOFFSET.key, false)) dynData?.getRotationOffset()?.toFloat() ?:0F else 0F
view.rotation = viewJson.optInt(ROTATION.key).toFloat() + rotationOffset
}
fun customizeTextView(view: TextView, viewJson: JSONObject) {
customizeViewCommon(view, viewJson)
view.rotation = viewJson.optInt(ROTATION.key).toFloat()
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, (viewJson.optInt(TEXTSIZE.key, 22) * cwf.zoomFactor).toFloat())
view.gravity = GravityMap.gravity(viewJson.optString(GRAVITY.key, GravityMap.CENTER.key))
view.setTypeface(
FontMap.font(viewJson.optString(FONT.key, FontMap.DEFAULT.key)),
StyleMap.style(viewJson.optString(FONTSTYLE.key, StyleMap.NORMAL.key))
)
view.setTextColor(cwf.getColor(viewJson.optString(FONTCOLOR.key)))
view.setTextColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key)))
view.isAllCaps = viewJson.optBoolean(ALLCAPS.key)
if (viewJson.has(TEXTVALUE.key))
view.text = viewJson.optString(TEXTVALUE.key)
view.background = textDrawable(viewJson)
view.background = dynData?.getDrawable() ?: textDrawable(viewJson)
}
fun customizeImageView(view: ImageView, viewJson: JSONObject) {
customizeViewCommon(view, viewJson)
view.clearColorFilter()
drawable?.let {
if (viewJson.has(COLOR.key)) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files
it.colorFilter = cwf.changeDrawableColor(cwf.getColor(viewJson.optString(COLOR.key)))
if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files
it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key)))
else
it.clearColorFilter()
view.setImageDrawable(it)
} ?: apply {
view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) })
if (viewJson.has(COLOR.key))
view.setColorFilter(cwf.getColor(viewJson.optString(COLOR.key)))
if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0)
view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key)))
else
view.clearColorFilter()
}
}
fun customizeGraphView(view: lecho.lib.hellocharts.view.LineChartView, viewJson: JSONObject) {
customizeViewCommon(view, viewJson)
view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"),Color.TRANSPARENT))
view.background = dynData?.getDrawable() ?: textDrawable(viewJson)
}
}
private enum class TrendArrowMap(val symbol: String, @DrawableRes val icon: Int, val customDrawable: ResFileMap?) {
NONE("??", R.drawable.ic_invalid, ResFileMap.ARROW_NONE),
TRIPLE_UP("X", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP),
DOUBLE_UP("\u21c8", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP),
SINGLE_UP("\u2191", R.drawable.ic_singleup, ResFileMap.ARROW_SINGLE_UP),
FORTY_FIVE_UP("\u2197", R.drawable.ic_fortyfiveup, ResFileMap.ARROW_FORTY_FIVE_UP),
FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT),
FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN),
SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN),
DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN),
TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN);
private enum class TrendArrowMap(val symbol: String, @DrawableRes val icon: Int, val customDrawable: ResFileMap?, val dynValue: Double) {
NONE("??", R.drawable.ic_invalid, ResFileMap.ARROW_NONE, 0.0),
TRIPLE_UP("X", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP, 7.0),
DOUBLE_UP("\u21c8", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP, 7.0),
SINGLE_UP("\u2191", R.drawable.ic_singleup, ResFileMap.ARROW_SINGLE_UP, 6.0),
FORTY_FIVE_UP("\u2197", R.drawable.ic_fortyfiveup, ResFileMap.ARROW_FORTY_FIVE_UP, 5.0),
FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT, 4.0),
FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN, 3.0),
SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN, 2.0),
DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 2.0),
TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0);
companion object {
@ -532,6 +551,7 @@ class CustomWatchface : BaseWatchFace() {
it.arrowCustom = null
}
fun drawable() = values().firstOrNull { it.symbol == it.cwf.singleBg.slopeArrow }?.arrowCustom ?: NONE.arrowCustom
fun value() = values().firstOrNull { it.symbol == it.cwf.singleBg.slopeArrow }?.dynValue ?: NONE.dynValue
}
lateinit var cwf: CustomWatchface
@ -618,6 +638,129 @@ class CustomWatchface : BaseWatchFace() {
SHOW_LOOP_STATUS(CwfMetadataKey.CWF_PREF_WATCH_SHOW_LOOP_STATUS.key, R.string.key_show_external_status),
SHOW_WEEK_NUMBER(CwfMetadataKey.CWF_PREF_WATCH_SHOW_WEEK_NUMBER.key, R.string.key_show_week_number)
}
private enum class ValueMap(val key: String, val min: Double, val max: Double) {
SGV(ViewKeys.SGV.key, 39.0, 400.0),
SGVLEVEL(JsonKeyValues.SGVLEVEL.key, -1.0, 1.0),
DIRECTION(ViewKeys.DIRECTION.key, 1.0, 7.0),
DELTA(ViewKeys.DELTA.key, -25.0, 25.0),
AVG_DELTA(ViewKeys.AVG_DELTA.key, -25.0, 25.0),
UPLOADER_BATTERY(ViewKeys.UPLOADER_BATTERY.key, 0.0, 100.0),
RIG_BATTERY(ViewKeys.RIG_BATTERY.key, 0.0, 100.0),
TIMESTAMP(ViewKeys.TIMESTAMP.key, 0.0, 60.0),
LOOP(ViewKeys.LOOP.key, 0.0, 28.0),
DAY(ViewKeys.DAY.key, 1.0, 31.0),
DAY_NAME(ViewKeys.DAY_NAME.key, 1.0, 7.0),
MONTH(ViewKeys.MONTH.key, 1.0, 12.0),
WEEKNUMBER(ViewKeys.WEEKNUMBER.key, 1.0, 53.0);
fun dynValue(dataValue: Double, dataRange: DataRange, valueRange: DataRange): Int = when {
dataValue < dataRange.minData -> dataRange.minData
dataValue > dataRange.maxData -> dataRange.maxData
else -> dataValue
}.let {
if (dataRange.minData != dataRange.maxData)
(valueRange.minData + (it - dataRange.minData) * (valueRange.maxData - valueRange.minData) / (dataRange.maxData - dataRange.minData)).toInt()
else it.toInt()
}
fun stepValue(dataValue: Double, range: DataRange, step: Int): Int = step(dataValue, range, step)
private fun step(dataValue: Double, dataRange: DataRange, step: Int): Int = when {
dataValue < dataRange.minData -> dataRange.minData
dataValue >= dataRange.maxData -> dataRange.maxData * 0.9999 // to avoid dataValue == maxData and be out of range
else -> dataValue
}.let { if (dataRange.minData != dataRange.maxData) (1 + ((it - dataRange.minData) * step) / (dataRange.maxData - dataRange.minData)).toInt() else 0 }
companion object {
fun fromKey(key: String) = values().firstOrNull { it.key == key }
}
}
private class DynProvider(val cwf: CustomWatchface, val dataJson: JSONObject, val valueMap: ValueMap, val width: Int, val height: Int) {
private val dynDrawable = mutableMapOf<Int, Drawable?>()
private val dynColor = mutableMapOf<Int, Int>()
private var dataRange: DataRange? = null
private var topRange: DataRange? = null
private var leftRange: DataRange? = null
private var rotationRange: DataRange? = null
val stepDraw: Int
get() = dynDrawable.size - 1
val stepColor: Int
get() = dynColor.size - 1
val dataValue: Double?
get() = when (valueMap) {
ValueMap.SGV -> if (cwf.singleBg.sgvString != "---") cwf.singleBg.sgv else null
ValueMap.SGVLEVEL -> if (cwf.singleBg.sgvString != "---") cwf.singleBg.sgvLevel.toDouble() else null
ValueMap.DIRECTION -> TrendArrowMap.value()
ValueMap.DELTA -> cwf.singleBg.deltaMgdl
ValueMap.AVG_DELTA -> cwf.singleBg.avgDeltaMgdl
ValueMap.RIG_BATTERY -> cwf.status.rigBattery.replace("%", "").toDoubleOrNull()
ValueMap.UPLOADER_BATTERY -> cwf.status.battery.replace("%", "").toDoubleOrNull()
ValueMap.LOOP -> if (cwf.status.openApsStatus != -1L) ((System.currentTimeMillis() - cwf.status.openApsStatus) / 1000 / 60).toDouble() else null
ValueMap.TIMESTAMP -> if (cwf.singleBg.timeStamp != 0L) floor(cwf.timeSince() / (1000 * 60)) else null
ValueMap.DAY -> DateTime().dayOfMonth.toDouble()
ValueMap.DAY_NAME -> DateTime().dayOfWeek.toDouble()
ValueMap.MONTH -> DateTime().monthOfYear.toDouble()
ValueMap.WEEKNUMBER -> DateTime().weekOfWeekyear.toDouble()
}
fun getTopOffset(): Int = dataRange?.let { dataRange -> topRange?.let { topRange -> dataValue?.let { valueMap.dynValue(it, dataRange, topRange) } ?: topRange.invalidData } } ?: 0
fun getLeftOffset(): Int = dataRange?.let { dataRange -> leftRange?.let { leftRange -> dataValue?.let { valueMap.dynValue(it, dataRange, leftRange) } ?: leftRange.invalidData } } ?: 0
fun getRotationOffset(): Int = dataRange?.let { dataRange -> rotationRange?.let { rotRange -> dataValue?.let { valueMap.dynValue(it, dataRange, rotRange) } ?: rotRange.invalidData } } ?: 0
fun getDrawable() = dataRange?.let { dataRange -> dataValue?.let { dynDrawable[valueMap.stepValue(it, dataRange, stepDraw)] } ?: dynDrawable[0] }
fun getColor() = if (stepColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynColor[valueMap.stepValue(it, dataRange, stepColor)] } ?: dynColor[0] } else null
private fun load() {
dynDrawable[0] = dataJson.optString(INVALIDIMAGE.key)?.let { cwf.resDataMap[it]?.toDrawable(cwf.resources, width, height) }
var idx = 1
while (dataJson.has("${IMAGE.key}$idx")) {
cwf.resDataMap[dataJson.optString("${IMAGE.key}$idx")]?.toDrawable(cwf.resources, width, height).also { dynDrawable[idx] = it }
idx++
}
dynColor[0] = cwf.getColor(dataJson.optString(INVALIDCOLOR.key))
idx = 1
while (dataJson.has("${COLOR.key}$idx")) {
dynColor[idx] = cwf.getColor(dataJson.optString("${COLOR.key}$idx"))
idx++
}
DataRange(dataJson.optDouble(MINDATA.key, valueMap.min), dataJson.optDouble(MAXDATA.key, valueMap.max)).let { defaultRange ->
dataRange = defaultRange
topRange = parseDataRange(dataJson.optJSONObject(TOPOFFSET.key), defaultRange)
leftRange = parseDataRange(dataJson.optJSONObject(LEFTOFFSET.key), defaultRange)
rotationRange = parseDataRange(dataJson.optJSONObject(ROTATIONOFFSET.key), defaultRange)
}
}
companion object {
val dynData = mutableMapOf<String, DynProvider>()
var dynJson: JSONObject? = null
fun init(dynJson: JSONObject?) {
this.dynJson = dynJson
dynData.clear()
}
fun getDyn(cwf: CustomWatchface, key: String, width: Int, height: Int, defaultViewKey: String): DynProvider? = dynData["${defaultViewKey}_$key"]
?: dynJson?.optJSONObject(key)?.let { dataJson ->
ValueMap.fromKey(dataJson.optString(VALUEKEY.key, defaultViewKey))?.let { valueMap ->
DynProvider(cwf, dataJson, valueMap, width, height).also { it.load() }
}
}?.also { dynData["${defaultViewKey}_$key"] = it }
private fun parseDataRange(json: JSONObject?, defaultData: DataRange) =
json?.let {
DataRange(
minData = it.optDouble(MINVALUE.key, defaultData.minData),
maxData = it.optDouble(MAXVALUE.key, defaultData.maxData),
invalidData = it.optInt(INVALIDVALUE.key, defaultData.invalidData)
)
} ?: defaultData
}
}
private class DataRange (val minData: Double, val maxData: Double, val invalidData: Int = 0)
}