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

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

View File

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

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
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<ZonedDateTime>(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
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 = size.width * 0.08f,
radius = sSize,
center = size.center + sPos,
blendMode = BlendMode.DstOut
)
}
drawCircle(
color = Color.Black,
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 = size.width * 0.08f,
center = size.center + hPos,
blendMode = BlendMode.SrcOut
radius = sSize,
center = size.center + sPos,
blendMode = BlendMode.Overlay
)
}
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
)
}
}
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
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