From 4159d7ad5061f482109a41fcfc8375784b3e4f69 Mon Sep 17 00:00:00 2001 From: Christoph <47949835+Sir-Photch@users.noreply.github.com> Date: Wed, 5 Apr 2023 02:40:02 +0900 Subject: [PATCH] Make `OrbitClock` somewhat respect orbital dynamics (#276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../java/de/mm20/launcher2/ui/ktx/Float.kt | 3 +- .../ui/launcher/widgets/clock/ClockWidget.kt | 7 +- .../launcher/widgets/clock/ClockWidgetVM.kt | 2 - .../widgets/clock/clocks/EmptyClock.kt | 12 - .../widgets/clock/clocks/OrbitClock.kt | 211 ++++++++++++++---- .../main/java/de/mm20/launcher2/ktx/Float.kt | 5 + 6 files changed, 177 insertions(+), 63 deletions(-) delete mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/EmptyClock.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Float.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Float.kt index b246ad07..afd8a649 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Float.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Float.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalDensity 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 @@ -11,4 +12,4 @@ import androidx.compose.ui.unit.dp @Composable fun Float.toDp(): Dp { return (this / LocalDensity.current.density).dp -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt index 2b207e81..495d966c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt @@ -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.DigitalClock1 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.parts.PartProvider import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper @@ -155,16 +154,16 @@ fun ClockWidget( @Composable fun Clock( style: ClockStyle?, - layout: ClockWidgetLayout, + layout: ClockWidgetLayout ) { val time = LocalTime.current when (style) { - ClockStyle.DigitalClock1 -> DigitalClock1(time = time, layout = layout) + ClockStyle.DigitalClock1 -> DigitalClock1(time, layout) ClockStyle.DigitalClock2 -> DigitalClock2(time, layout) ClockStyle.BinaryClock -> BinaryClock(time, layout) ClockStyle.AnalogClock -> AnalogClock(time, layout) ClockStyle.OrbitClock -> OrbitClock(time, layout) - ClockStyle.EmptyClock -> EmptyClock(time, layout) + ClockStyle.EmptyClock -> {} else -> {} } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt index 5b334b5d..5b2babc7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt @@ -3,7 +3,6 @@ package de.mm20.launcher2.ui.launcher.widgets.clock import android.content.Context import android.content.Intent import android.provider.AlarmClock -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -56,7 +55,6 @@ class ClockWidgetVM : ViewModel(), KoinComponent { } } - val layout = dataStore.data.map { it.clockWidget.layout }.asLiveData() val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/EmptyClock.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/EmptyClock.kt deleted file mode 100644 index 88ae0cbb..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/EmptyClock.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/OrbitClock.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/OrbitClock.kt index 95563d84..1725c38e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/OrbitClock.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/clocks/OrbitClock.kt @@ -1,14 +1,21 @@ package de.mm20.launcher2.ui.launcher.widgets.clock.clocks 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.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset 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.dp import androidx.compose.ui.unit.toOffset +import de.mm20.launcher2.ktx.TWO_PI import de.mm20.launcher2.preferences.Settings -import java.util.Calendar -import kotlin.math.PI +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime import kotlin.math.cos import kotlin.math.sin +private const val PHI_F = 1.618033988749895.toFloat() + +private val currentTime + get() = Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault()) + @Composable fun OrbitClock( - time: Long, + _time: Long, layout: Settings.ClockWidgetSettings.ClockWidgetLayout ) { 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 heta by animateFloatAsState((hour % 12) / 12f * 2f * PI.toFloat() + (minute / 60f) * 1f / 6f * PI.toFloat()) + val timeState = remember { mutableStateOf(currentTime) } + + 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 measurer = rememberTextMeasurer() - val textStyle = MaterialTheme.typography.labelMedium + val textMeasurer = rememberTextMeasurer() + val minuteStyle = MaterialTheme.typography.labelMedium + val hourStyle = MaterialTheme.typography.labelLarge val strokeWidth = if (verticalLayout) 2.dp else 1.dp - Canvas(modifier = Modifier - .padding(bottom = if (verticalLayout) 8.dp else 0.dp) - .size(if (verticalLayout) 192.dp else 56.dp) + Canvas( + modifier = Modifier + .padding(bottom = if (verticalLayout) 8.dp else 0.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( color = color.copy(alpha = 0.5f), radius = rm, - style = Stroke(width = strokeWidth.toPx(), - pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx())) + style = Stroke( + width = strokeWidth.toPx(), + pathEffect = PathEffect.dashPathEffect( + floatArrayOf( + (2 * PHI_F).dp.toPx(), + (2 * PHI_F).dp.toPx() + ) + ) ) ) drawCircle( color = color.copy(alpha = 0.5f), radius = rh, - style = Stroke(width = strokeWidth.toPx(), - pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx())), + style = Stroke( + 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 hPos = Offset(x = sin(heta) * rh, y = -cos(heta) * rh) + val sPos = Offset(x = sin(animatedSecs) * rs, y = -cos(animatedSecs) * rs) + 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( + color = Color.Black, + radius = sSize, + center = size.center + sPos, + blendMode = BlendMode.DstOut + ) + } drawCircle( color = Color.Black, - radius = size.width * 0.08f, + radius = mSize, center = size.center + mPos, blendMode = BlendMode.DstOut ) drawCircle( color = Color.Black, - radius = size.width * 0.08f, + radius = hSize, center = size.center + hPos, blendMode = BlendMode.DstOut ) if (verticalLayout) { - val textMResult = measurer.measure( + val textMResult = textMeasurer.measure( AnnotatedString(minute.toString()), maxLines = 1, - style = textStyle + style = minuteStyle ) - val textHResult = measurer.measure( - AnnotatedString(hour.toString()), + val textHResult = textMeasurer.measure( + AnnotatedString(formattedHour), maxLines = 1, - style = textStyle + style = hourStyle ) drawText( textMResult, - color = Color.Black, - topLeft = size.center - textMResult.size.center.toOffset() + mPos + color = color.invert(alpha = 0.65f), + topLeft = size.center - textMResult.size.center.toOffset() + mPos, + blendMode = BlendMode.Overlay ) drawText( textHResult, - color = Color.Black, - topLeft = size.center - textHResult.size.center.toOffset() + hPos + color = color.invert(alpha = 0.65f), + topLeft = size.center - textHResult.size.center.toOffset() + hPos, + blendMode = BlendMode.Overlay ) } + if (verticalLayout) { + drawCircle( + color = color, + radius = sSize, + center = size.center + sPos, + blendMode = BlendMode.Overlay + ) + } drawCircle( color = color, - radius = size.width * 0.08f, - center = size.center + hPos, - blendMode = BlendMode.SrcOut - ) - drawCircle( - color = color, - radius = size.width * 0.08f, + radius = mSize, center = size.center + mPos, - blendMode = BlendMode.SrcOut + blendMode = BlendMode.Overlay + ) + drawCircle( + color = color, + radius = hSize, + center = size.center + hPos, + blendMode = BlendMode.Overlay ) - } -} \ No newline at end of file +} + +private fun Color.invert(alpha: Float? = null): Color = + Color( + 1f - red, + 1f - green, + 1f - blue, + alpha ?: this.alpha, colorSpace + ) + diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt index 2149c1a7..68b569de 100644 --- a/core/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt @@ -1,7 +1,12 @@ package de.mm20.launcher2.ktx +import kotlin.math.PI import kotlin.math.ceil fun Float.ceilToInt(): Int { return ceil(this).toInt() } + +private const val TWO_PI_F = (2.0 * PI).toFloat() +val Float.Companion.TWO_PI: Float + get() = TWO_PI_F