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.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
|
||||
|
||||
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user