Make OrbitClock somewhat respect orbital dynamics (#276)
* orbitclock adjustments * cleanup * switch to LaunchedEffect * radii-diff golden ratio && readability * equal space between planets && large label on hour * size adjustments * smooth animations * incorporate ms * cleanup and synchronization fix (somewhat?) * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (568 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (568 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Chinese (Traditional)) Currently translated at 64.1% (68 of 106 strings) Translation: Kvæsitso/units * Fix favorites in favorite widget hidden even when clock widget favorites are disabled * Fix crash * Add preference to disable themed icons for themed icon packs * Add tooltip to icon pack themed toggle * Version 1.23.1 * Translated using Weblate (Dutch) Currently translated at 99.4% (565 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (106 of 106 strings) Translation: Kvæsitso/units * Translated using Weblate (Dutch) Currently translated at 100.0% (568 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.2% (564 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Russian) Currently translated at 100.0% (106 of 106 strings) Translation: Kvæsitso/units * Added translation using Weblate (Hungarian) * Translated using Weblate (Turkish) Currently translated at 34.9% (37 of 106 strings) Translation: Kvæsitso/units * Added translation using Weblate (Hungarian) * Added translation using Weblate (Ukrainian) * Translated using Weblate (Hungarian) Currently translated at 4.2% (24 of 568 strings) Translation: Kvæsitso/i18n * Added translation using Weblate (Ukrainian) * Docs: update docusaurus * Docs: Add FAQ page * Update Jetpack Compose and Accompanist * Add TagChip component * Translated using Weblate (Ukrainian) Currently translated at 5.9% (34 of 568 strings) Translation: Kvæsitso/i18n * Translated using Weblate (Ukrainian) Currently translated at 0.9% (1 of 106 strings) Translation: Kvæsitso/units * Extend FAQ * Restore EmptyClock when branch * orbitclock adjustments * cleanup * switch to LaunchedEffect * radii-diff golden ratio && readability * equal space between planets && large label on hour * size adjustments * smooth animations * incorporate ms * cleanup and synchronization fix (somewhat?) * Restore EmptyClock when branch
This commit is contained in:
parent
090914fdbb
commit
4159d7ad50
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given pixel size to a Dp value based on the current density
|
* Converts the given pixel size to a Dp value based on the current density
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.AnalogClock
|
|||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.BinaryClock
|
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.BinaryClock
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock1
|
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock1
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock2
|
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.DigitalClock2
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.EmptyClock
|
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.OrbitClock
|
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.OrbitClock
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider
|
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider
|
||||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||||
@ -155,16 +154,16 @@ fun ClockWidget(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Clock(
|
fun Clock(
|
||||||
style: ClockStyle?,
|
style: ClockStyle?,
|
||||||
layout: ClockWidgetLayout,
|
layout: ClockWidgetLayout
|
||||||
) {
|
) {
|
||||||
val time = LocalTime.current
|
val time = LocalTime.current
|
||||||
when (style) {
|
when (style) {
|
||||||
ClockStyle.DigitalClock1 -> DigitalClock1(time = time, layout = layout)
|
ClockStyle.DigitalClock1 -> DigitalClock1(time, layout)
|
||||||
ClockStyle.DigitalClock2 -> DigitalClock2(time, layout)
|
ClockStyle.DigitalClock2 -> DigitalClock2(time, layout)
|
||||||
ClockStyle.BinaryClock -> BinaryClock(time, layout)
|
ClockStyle.BinaryClock -> BinaryClock(time, layout)
|
||||||
ClockStyle.AnalogClock -> AnalogClock(time, layout)
|
ClockStyle.AnalogClock -> AnalogClock(time, layout)
|
||||||
ClockStyle.OrbitClock -> OrbitClock(time, layout)
|
ClockStyle.OrbitClock -> OrbitClock(time, layout)
|
||||||
ClockStyle.EmptyClock -> EmptyClock(time, layout)
|
ClockStyle.EmptyClock -> {}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package de.mm20.launcher2.ui.launcher.widgets.clock
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.AlarmClock
|
import android.provider.AlarmClock
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -56,7 +55,6 @@ class ClockWidgetVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val layout = dataStore.data.map { it.clockWidget.layout }.asLiveData()
|
val layout = dataStore.data.map { it.clockWidget.layout }.asLiveData()
|
||||||
val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData()
|
val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData()
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import de.mm20.launcher2.preferences.Settings
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EmptyClock(
|
|
||||||
time: Long,
|
|
||||||
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
|
|
||||||
) {
|
|
||||||
// Nothing to see here
|
|
||||||
}
|
|
||||||
@ -1,14 +1,21 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
|
package de.mm20.launcher2.ui.launcher.widgets.clock.clocks
|
||||||
|
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.center
|
import androidx.compose.ui.geometry.center
|
||||||
@ -23,108 +30,224 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||||||
import androidx.compose.ui.unit.center
|
import androidx.compose.ui.unit.center
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.toOffset
|
import androidx.compose.ui.unit.toOffset
|
||||||
|
import de.mm20.launcher2.ktx.TWO_PI
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import java.util.Calendar
|
import java.time.Instant
|
||||||
import kotlin.math.PI
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
private const val PHI_F = 1.618033988749895.toFloat()
|
||||||
|
|
||||||
|
private val currentTime
|
||||||
|
get() = Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault())
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OrbitClock(
|
fun OrbitClock(
|
||||||
time: Long,
|
_time: Long,
|
||||||
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
|
layout: Settings.ClockWidgetSettings.ClockWidgetLayout
|
||||||
) {
|
) {
|
||||||
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
|
val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical
|
||||||
val date = Calendar.getInstance()
|
|
||||||
date.timeInMillis = time
|
|
||||||
val minute = date[Calendar.MINUTE]
|
|
||||||
val hour = if (DateFormat.is24HourFormat(LocalContext.current)) date[Calendar.HOUR_OF_DAY] else date[Calendar.HOUR]
|
|
||||||
|
|
||||||
val mu by animateFloatAsState(minute / 60f * 2f * PI.toFloat())
|
val timeState = remember { mutableStateOf<ZonedDateTime>(currentTime) }
|
||||||
val heta by animateFloatAsState((hour % 12) / 12f * 2f * PI.toFloat() + (minute / 60f) * 1f / 6f * PI.toFloat())
|
|
||||||
|
LaunchedEffect(_time) {
|
||||||
|
timeState.value = currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
val time by timeState
|
||||||
|
|
||||||
|
val second = time.second
|
||||||
|
val minute = time.minute
|
||||||
|
val hour = time.hour
|
||||||
|
val formattedHour = (
|
||||||
|
if (DateFormat.is24HourFormat(LocalContext.current))
|
||||||
|
hour
|
||||||
|
else
|
||||||
|
hour % 12
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
val secsAngleStart = second / 60f * Float.TWO_PI
|
||||||
|
val minsAngleStart = minute / 60f * Float.TWO_PI + secsAngleStart / 60f
|
||||||
|
val hourAngleStart = hour % 12 / 12f * Float.TWO_PI + minsAngleStart / 12f
|
||||||
|
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "timeInfiniteTransition")
|
||||||
|
|
||||||
|
val animatedSecs by infiniteTransition.animateFloat(
|
||||||
|
initialValue = secsAngleStart,
|
||||||
|
targetValue = secsAngleStart + Float.TWO_PI,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 60 * 1000,
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
),
|
||||||
|
label = "secondsAnimation"
|
||||||
|
)
|
||||||
|
val animatedMins by infiniteTransition.animateFloat(
|
||||||
|
initialValue = minsAngleStart,
|
||||||
|
targetValue = minsAngleStart + Float.TWO_PI,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 60 * 60 * 1000,
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
),
|
||||||
|
label = "minutesAnimation"
|
||||||
|
)
|
||||||
|
val animatedHrs by infiniteTransition.animateFloat(
|
||||||
|
initialValue = hourAngleStart,
|
||||||
|
targetValue = hourAngleStart + Float.TWO_PI,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 12 * 60 * 60 * 1000,
|
||||||
|
easing = LinearEasing
|
||||||
|
)
|
||||||
|
),
|
||||||
|
label = "hoursAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
val color = LocalContentColor.current
|
val color = LocalContentColor.current
|
||||||
|
|
||||||
val measurer = rememberTextMeasurer()
|
val textMeasurer = rememberTextMeasurer()
|
||||||
val textStyle = MaterialTheme.typography.labelMedium
|
val minuteStyle = MaterialTheme.typography.labelMedium
|
||||||
|
val hourStyle = MaterialTheme.typography.labelLarge
|
||||||
|
|
||||||
val strokeWidth = if (verticalLayout) 2.dp else 1.dp
|
val strokeWidth = if (verticalLayout) 2.dp else 1.dp
|
||||||
|
|
||||||
Canvas(modifier = Modifier
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
.padding(bottom = if (verticalLayout) 8.dp else 0.dp)
|
.padding(bottom = if (verticalLayout) 8.dp else 0.dp)
|
||||||
.size(if (verticalLayout) 192.dp else 56.dp)
|
.size(if (verticalLayout) 192.dp else 56.dp)
|
||||||
) {
|
) {
|
||||||
val rm = size.width * 0.45f
|
|
||||||
val rh = size.width * 0.25f
|
val rs = size.width * 0.08f
|
||||||
|
val rm = size.width * 0.22f
|
||||||
|
val rh = rm + (rm - rs) * PHI_F
|
||||||
|
|
||||||
|
val sSize = size.width * 0.0175f
|
||||||
|
val mSize = size.width * 0.08f
|
||||||
|
val hSize = rh + sSize + rs - 2f * rm
|
||||||
|
|
||||||
|
if (verticalLayout) {
|
||||||
|
drawCircle(
|
||||||
|
color = color.copy(alpha = 0.5f),
|
||||||
|
radius = rs,
|
||||||
|
style = Stroke(
|
||||||
|
width = strokeWidth.toPx(),
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = color.copy(alpha = 0.5f),
|
color = color.copy(alpha = 0.5f),
|
||||||
radius = rm,
|
radius = rm,
|
||||||
style = Stroke(width = strokeWidth.toPx(),
|
style = Stroke(
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx()))
|
width = strokeWidth.toPx(),
|
||||||
|
pathEffect = PathEffect.dashPathEffect(
|
||||||
|
floatArrayOf(
|
||||||
|
(2 * PHI_F).dp.toPx(),
|
||||||
|
(2 * PHI_F).dp.toPx()
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = color.copy(alpha = 0.5f),
|
color = color.copy(alpha = 0.5f),
|
||||||
radius = rh,
|
radius = rh,
|
||||||
style = Stroke(width = strokeWidth.toPx(),
|
style = Stroke(
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx())),
|
width = strokeWidth.toPx(),
|
||||||
|
pathEffect = PathEffect.dashPathEffect(
|
||||||
|
floatArrayOf(
|
||||||
|
(2 * PHI_F * PHI_F).dp.toPx(),
|
||||||
|
(2 * PHI_F * PHI_F).dp.toPx()
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val mPos = Offset(x = sin(mu) * rm, y = -cos(mu) * rm)
|
val sPos = Offset(x = sin(animatedSecs) * rs, y = -cos(animatedSecs) * rs)
|
||||||
val hPos = Offset(x = sin(heta) * rh, y = -cos(heta) * rh)
|
val mPos = Offset(x = sin(animatedMins) * rm, y = -cos(animatedMins) * rm)
|
||||||
|
val hPos = Offset(x = sin(animatedHrs) * rh, y = -cos(animatedHrs) * rh)
|
||||||
|
|
||||||
|
if (verticalLayout) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
radius = size.width * 0.08f,
|
radius = sSize,
|
||||||
|
center = size.center + sPos,
|
||||||
|
blendMode = BlendMode.DstOut
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawCircle(
|
||||||
|
color = Color.Black,
|
||||||
|
radius = mSize,
|
||||||
center = size.center + mPos,
|
center = size.center + mPos,
|
||||||
blendMode = BlendMode.DstOut
|
blendMode = BlendMode.DstOut
|
||||||
)
|
)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
radius = size.width * 0.08f,
|
radius = hSize,
|
||||||
center = size.center + hPos,
|
center = size.center + hPos,
|
||||||
blendMode = BlendMode.DstOut
|
blendMode = BlendMode.DstOut
|
||||||
)
|
)
|
||||||
|
|
||||||
if (verticalLayout) {
|
if (verticalLayout) {
|
||||||
|
|
||||||
val textMResult = measurer.measure(
|
val textMResult = textMeasurer.measure(
|
||||||
AnnotatedString(minute.toString()),
|
AnnotatedString(minute.toString()),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = textStyle
|
style = minuteStyle
|
||||||
)
|
)
|
||||||
|
|
||||||
val textHResult = measurer.measure(
|
val textHResult = textMeasurer.measure(
|
||||||
AnnotatedString(hour.toString()),
|
AnnotatedString(formattedHour),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = textStyle
|
style = hourStyle
|
||||||
)
|
)
|
||||||
|
|
||||||
drawText(
|
drawText(
|
||||||
textMResult,
|
textMResult,
|
||||||
color = Color.Black,
|
color = color.invert(alpha = 0.65f),
|
||||||
topLeft = size.center - textMResult.size.center.toOffset() + mPos
|
topLeft = size.center - textMResult.size.center.toOffset() + mPos,
|
||||||
|
blendMode = BlendMode.Overlay
|
||||||
)
|
)
|
||||||
drawText(
|
drawText(
|
||||||
textHResult,
|
textHResult,
|
||||||
color = Color.Black,
|
color = color.invert(alpha = 0.65f),
|
||||||
topLeft = size.center - textHResult.size.center.toOffset() + hPos
|
topLeft = size.center - textHResult.size.center.toOffset() + hPos,
|
||||||
|
blendMode = BlendMode.Overlay
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verticalLayout) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = color,
|
color = color,
|
||||||
radius = size.width * 0.08f,
|
radius = sSize,
|
||||||
center = size.center + hPos,
|
center = size.center + sPos,
|
||||||
blendMode = BlendMode.SrcOut
|
blendMode = BlendMode.Overlay
|
||||||
)
|
)
|
||||||
|
}
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = color,
|
color = color,
|
||||||
radius = size.width * 0.08f,
|
radius = mSize,
|
||||||
center = size.center + mPos,
|
center = size.center + mPos,
|
||||||
blendMode = BlendMode.SrcOut
|
blendMode = BlendMode.Overlay
|
||||||
|
)
|
||||||
|
drawCircle(
|
||||||
|
color = color,
|
||||||
|
radius = hSize,
|
||||||
|
center = size.center + hPos,
|
||||||
|
blendMode = BlendMode.Overlay
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Color.invert(alpha: Float? = null): Color =
|
||||||
|
Color(
|
||||||
|
1f - red,
|
||||||
|
1f - green,
|
||||||
|
1f - blue,
|
||||||
|
alpha ?: this.alpha, colorSpace
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
package de.mm20.launcher2.ktx
|
package de.mm20.launcher2.ktx
|
||||||
|
|
||||||
|
import kotlin.math.PI
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
fun Float.ceilToInt(): Int {
|
fun Float.ceilToInt(): Int {
|
||||||
return ceil(this).toInt()
|
return ceil(this).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val TWO_PI_F = (2.0 * PI).toFloat()
|
||||||
|
val Float.Companion.TWO_PI: Float
|
||||||
|
get() = TWO_PI_F
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user