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:
Christoph 2023-04-05 02:40:02 +09:00 committed by GitHub
parent 090914fdbb
commit 4159d7ad50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 177 additions and 63 deletions

View File

@ -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

View File

@ -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 -> {}
} }
} }

View File

@ -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()

View File

@ -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
}

View File

@ -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(
.padding(bottom = if (verticalLayout) 8.dp else 0.dp) modifier = Modifier
.size(if (verticalLayout) 192.dp else 56.dp) .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( 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(
color = Color.Black,
radius = sSize,
center = size.center + sPos,
blendMode = BlendMode.DstOut
)
}
drawCircle( drawCircle(
color = Color.Black, color = Color.Black,
radius = size.width * 0.08f, 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(
color = color,
radius = sSize,
center = size.center + sPos,
blendMode = BlendMode.Overlay
)
}
drawCircle( drawCircle(
color = color, color = color,
radius = size.width * 0.08f, radius = mSize,
center = size.center + hPos,
blendMode = BlendMode.SrcOut
)
drawCircle(
color = color,
radius = size.width * 0.08f,
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
)

View File

@ -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