-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
467 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package org.splitties.compose.oclock.sample.elements | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.graphics.BlendMode | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.Shadow | ||
import androidx.compose.ui.graphics.drawscope.DrawStyle | ||
import androidx.compose.ui.graphics.drawscope.Fill | ||
import androidx.compose.ui.graphics.layer.GraphicsLayer | ||
import androidx.compose.ui.graphics.layer.drawLayer | ||
import androidx.compose.ui.platform.LocalDensity | ||
import androidx.compose.ui.text.TextStyle | ||
import androidx.compose.ui.text.drawText | ||
import androidx.compose.ui.text.font.Font | ||
import androidx.compose.ui.text.font.FontFamily | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.text.rememberTextMeasurer | ||
import androidx.compose.ui.unit.Dp | ||
import androidx.compose.ui.unit.TextUnit | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.sp | ||
import com.louiscad.composeoclockplayground.shared.R | ||
import org.splitties.compose.oclock.LocalIsAmbient | ||
import org.splitties.compose.oclock.LocalTime | ||
import org.splitties.compose.oclock.OClockCanvas | ||
import org.splitties.compose.oclock.sample.extensions.SizeDependentState | ||
import org.splitties.compose.oclock.sample.extensions.blurOffset | ||
import org.splitties.compose.oclock.sample.extensions.center | ||
import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState | ||
import org.splitties.compose.oclock.sample.extensions.sizeForLayer | ||
|
||
@Composable | ||
fun FatHourDigits( | ||
interactiveColor: Color, | ||
interactiveShadowColor: Color = interactiveColor, | ||
ambientShadowColor: Color = interactiveShadowColor, | ||
interactiveShadowRepeat: Int = 1, | ||
ambientShadowRepeat: Int = interactiveShadowRepeat.takeIf { it > 1 } ?: 2, | ||
fontSize: TextUnit = 70.sp, | ||
interactiveBlurRadius: Dp = 10.dp, | ||
ambientBlurRadius: Dp = 10.dp | ||
) { | ||
val interactive = fatHourDigitsLayer( | ||
textColor = interactiveColor, | ||
shadowColor = interactiveShadowColor, | ||
fontSize = fontSize, | ||
blurRadius = interactiveBlurRadius | ||
) | ||
val ambient = fatHourDigitsLayer( | ||
textColor = Color.Black, | ||
shadowColor = ambientShadowColor, | ||
fontSize = fontSize, | ||
blurRadius = ambientBlurRadius | ||
) | ||
val isAmbient by LocalIsAmbient.current | ||
OClockCanvas { | ||
val count = if (isAmbient) ambientShadowRepeat else interactiveShadowRepeat | ||
val layer = (if (isAmbient) ambient else interactive).get() | ||
repeat(count) { | ||
drawLayer(layer) | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
fun fatHourDigitsLayer( | ||
textColor: Color = Color.White, | ||
shadowColor: Color = Color.White, | ||
blendMode: BlendMode = BlendMode.SrcOver, | ||
fontSize: TextUnit = 70.sp, | ||
blurRadius: Dp = 10.dp | ||
): SizeDependentState<GraphicsLayer> { | ||
val style = rememberFatHourDigitsTextStyle( | ||
textColor = textColor, | ||
shadowColor = shadowColor, | ||
blendMode = blendMode, | ||
fontSize = fontSize, | ||
blurRadius = blurRadius | ||
) | ||
val hourDigits = rememberHourDigits(style) | ||
return rememberGraphicsLayerAsState { layer -> | ||
layer.blendMode = blendMode | ||
layer.record(size = hourDigits.sizeForLayer()) { | ||
drawText(hourDigits, topLeft = hourDigits.blurOffset()) | ||
} | ||
layer.center() | ||
} | ||
} | ||
|
||
@Composable | ||
fun rememberFatHourDigitsTextStyle( | ||
textColor: Color = Color.White, | ||
shadowColor: Color = Color.White, | ||
blendMode: BlendMode = BlendMode.SrcOver, | ||
fontSize: TextUnit = 70.sp, | ||
drawStyle: DrawStyle = Fill, | ||
blurRadius: Dp = 10.dp | ||
): TextStyle { | ||
val density = LocalDensity.current | ||
val fontFamily = remember { FontFamily(Font(R.font.outfit_extrabold)) } | ||
@Suppress("name_shadowing") | ||
val blurRadius = with(density) { blurRadius.toPx() } | ||
return remember(textColor, shadowColor) { | ||
TextStyle( | ||
color = textColor, | ||
fontSize = fontSize, | ||
// fontWeight = FontWeight.ExtraBold, | ||
drawStyle = drawStyle, | ||
fontFamily = fontFamily, | ||
shadow = if (blurRadius > 0f) Shadow( | ||
color = shadowColor, | ||
blurRadius = blurRadius | ||
) else null | ||
) | ||
} | ||
} | ||
|
||
@Composable | ||
fun HourDigitsUnCached( | ||
textColor: Color = Color.White, | ||
shadowColor: Color = Color.White, | ||
blendMode: BlendMode = BlendMode.SrcOver, | ||
fontSize: TextUnit = 70.sp, | ||
) { | ||
val textMeasurer = rememberTextMeasurer() | ||
val density = LocalDensity.current | ||
val style = remember(textColor, shadowColor) { | ||
val fontFamily = FontFamily( | ||
Font(R.font.outfit_extrabold) | ||
) | ||
TextStyle( | ||
color = textColor, | ||
fontSize = fontSize, | ||
fontWeight = FontWeight.ExtraBold, | ||
fontFamily = fontFamily, | ||
shadow = Shadow( | ||
color = shadowColor, | ||
blurRadius = with(density) { 10.dp.toPx() } | ||
) | ||
) | ||
} | ||
val time = LocalTime.current | ||
val measuredText = remember(time.hours) { textMeasurer.measure("${time.hours}", style) } | ||
OClockCanvas { | ||
drawText( | ||
measuredText, | ||
topLeft = center.run { | ||
copy( | ||
x = x - measuredText.size.width / 2f, | ||
y = y - measuredText.size.height / 2f | ||
) | ||
}, | ||
blendMode = blendMode | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package org.splitties.compose.oclock.sample.elements | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.text.TextLayoutResult | ||
import androidx.compose.ui.text.TextStyle | ||
import androidx.compose.ui.text.rememberTextMeasurer | ||
import org.splitties.compose.oclock.LocalTime | ||
|
||
@Composable | ||
fun rememberHourDigits(textStyle: TextStyle): TextLayoutResult { | ||
val time = LocalTime.current | ||
val textMeasurer = rememberTextMeasurer() | ||
val measuredText = remember(time.hours, textStyle) { | ||
textMeasurer.measure(text = "${time.hours}", style = textStyle) | ||
} | ||
return measuredText | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package org.splitties.compose.oclock.sample.extensions | ||
|
||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.text.TextLayoutResult | ||
import androidx.compose.ui.unit.IntSize | ||
import androidx.compose.ui.unit.toIntSize | ||
import androidx.compose.ui.unit.toSize | ||
|
||
fun TextLayoutResult.sizeForLayer(): IntSize { | ||
val spaceForBlur = (layoutInput.style.shadow?.blurRadius ?: 0f) * 2 | ||
return (size.toSize() + spaceForBlur).toIntSize() | ||
} | ||
|
||
fun TextLayoutResult.blurOffset(): Offset = layoutInput.style.shadow?.blurRadius?.let { | ||
Offset(it, it) | ||
} ?: Offset.Zero |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package org.splitties.compose.oclock.sample.utils | ||
|
||
@PublishedApi | ||
internal fun Int.getBitAt(position: Int, last: Int = 32): Boolean = this shr (last - position) and 1 > 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package org.splitties.compose.oclock.sample.utils | ||
|
||
import androidx.annotation.IntRange | ||
|
||
@JvmInline | ||
value class FiveMinutesLayoutOrder( | ||
private val storage: Int | ||
) { | ||
init { | ||
require((storage and 0b11111_00000_00000_00000).countOneBits() == 1) | ||
require((storage and 0b00000_11111_00000_00000).countOneBits() == 2) | ||
require((storage and 0b00000_00000_11111_00000).countOneBits() == 3) | ||
require((storage and 0b00000_00000_00000_11111).countOneBits() == 4) | ||
} | ||
|
||
companion object { | ||
val linear = FiveMinutesLayoutOrder(0b10000_11000_11100_11110) | ||
val symmetricalSpread = FiveMinutesLayoutOrder(0b00100_01010_10101_11011) | ||
val symmetricalPacked = FiveMinutesLayoutOrder(0b00100_01010_01110_11011) | ||
val symmetricalEdges = FiveMinutesLayoutOrder(0b00100_10001_10101_11011) | ||
} | ||
|
||
fun showFor( | ||
@IntRange(0, 59) minute: Int, | ||
@IntRange(0, 4) minuteMarkIndex: Int, | ||
): Boolean { | ||
require(minute in 0..59) | ||
require(minuteMarkIndex in 0..4) | ||
return storage.getBitAt( | ||
position = ((minute - 1) % 5) * 5 + minuteMarkIndex, | ||
last = 19 | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package org.splitties.compose.oclock.sample.utils | ||
|
||
import androidx.annotation.IntRange | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.geometry.Rect | ||
import androidx.compose.ui.graphics.Path | ||
import androidx.compose.ui.graphics.drawscope.DrawScope | ||
import androidx.compose.ui.graphics.drawscope.rotate | ||
import androidx.compose.ui.graphics.drawscope.scale | ||
import androidx.compose.ui.graphics.layer.CompositingStrategy | ||
import androidx.compose.ui.graphics.layer.drawLayer | ||
import org.splitties.compose.oclock.LocalTime | ||
import org.splitties.compose.oclock.OClockCanvas | ||
import org.splitties.compose.oclock.sample.extensions.SizeDependentState | ||
import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState | ||
import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize | ||
import org.splitties.compose.oclock.sample.extensions.toFlooredIntSize | ||
|
||
@Composable | ||
fun <T> FiveMinutesSlicePattern( | ||
layoutOrder: FiveMinutesLayoutOrder = FiveMinutesLayoutOrder.symmetricalSpread, | ||
mirrored: Boolean = true, | ||
createSliceData: SizeDependentState.Scope.(slicePath: Path, bounds: Rect) -> T, | ||
drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit | ||
) { | ||
val slicePathAndBounds = rememberStateWithSize { | ||
val path = Path().also { | ||
it.setToPieSlice(size, 30f, centered = false) | ||
} | ||
path to path.getBounds() | ||
} | ||
val sliceData = rememberStateWithSize { | ||
val (path, bounds) = slicePathAndBounds.get() | ||
createSliceData(path, bounds) | ||
} | ||
val minuteLayers = List(5) { i -> | ||
minuteSliceLayer( | ||
slicePath = slicePathAndBounds, | ||
sliceData = sliceData, | ||
fiveMinutesIndex = i, | ||
drawSliceContent = drawSliceContent | ||
) | ||
} | ||
val time = LocalTime.current | ||
OClockCanvas { | ||
val max = 12 | ||
val minutes = time.minutes | ||
for (i in 0..<max) { | ||
val isLast = i == (minutes / 5) | ||
if (isLast && minutes % 5 == 0) break | ||
val normalRotation = mirrored.not() || i % 2 == 0 | ||
val angle = 30f * if (normalRotation) i else (i + 1) | ||
rotate(angle) { | ||
scale( | ||
scaleX = if (normalRotation) 1f else -1f, | ||
scaleY = 1f | ||
) { | ||
for (j in 0..4) { | ||
val index = if (normalRotation) j else 4 - j | ||
if (isLast) { | ||
val shouldShowIt = layoutOrder.showFor( | ||
minute = minutes, | ||
minuteMarkIndex = j | ||
) | ||
if (shouldShowIt.not()) continue | ||
} | ||
val layer = minuteLayers[index].get() | ||
drawLayer(layer) | ||
} | ||
} | ||
} | ||
if (isLast) break | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun <T> minuteSliceLayer( | ||
slicePath: SizeDependentState<Pair<Path, Rect>>, | ||
sliceData: SizeDependentState<T>, | ||
@IntRange(0, 4) fiveMinutesIndex: Int, | ||
drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit | ||
) = rememberGraphicsLayerAsState { layer -> | ||
require(fiveMinutesIndex in 0..4) | ||
val (path, bounds) = slicePath.get() | ||
layer.compositingStrategy = CompositingStrategy.Offscreen | ||
layer.setPathOutline(path) | ||
layer.clip = true | ||
layer.translationX = center.x | ||
@Suppress("name_shadowing") val sliceData = sliceData.get() | ||
layer.record(size = bounds.size.toFlooredIntSize()) { | ||
drawSliceContent(fiveMinutesIndex, sliceData) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package org.splitties.compose.oclock.sample.utils | ||
|
||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.geometry.Size | ||
import androidx.compose.ui.geometry.center | ||
import androidx.compose.ui.geometry.toRect | ||
import androidx.compose.ui.graphics.Path | ||
import org.splitties.compose.oclock.sample.extensions.lineTo | ||
import org.splitties.compose.oclock.sample.extensions.moveTo | ||
|
||
fun Path.setToPieSlice(size: Size, degrees: Float, centered: Boolean = true) { | ||
reset() | ||
moveTo(size.center) | ||
lineTo(size.center.copy(y = 0f)) | ||
arcTo(size.toRect(), startAngleDegrees = -90f, sweepAngleDegrees = degrees, forceMoveTo = false) | ||
lineTo(size.center) | ||
close() | ||
if (centered.not()) translate(Offset(x = -size.center.x, y = 0f)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.