Initial commit

This commit is contained in:
Kyant0 committed 2025-06-18 18:15:41 +08:00
commit 2d3cc0912f
54 files changed
+2477

No files matched your search

+1
View File
@@ -0,0 +1 @@
/build
+95
View File
@@ -0,0 +1,95 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.kyant.glass"
compileSdk = 36
buildToolsVersion = "36.0.0"
defaultConfig {
applicationId = "com.kyant.glass"
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "1.0.0-dev"
ndk.abiFilters += arrayOf("arm64-v8a", "x86_64")
androidResources.localeFilters += arrayOf("en", "zh-rCN")
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfigs {
create("release") {
enableV1Signing = false
enableV2Signing = false
enableV3Signing = true
enableV4Signing = false
}
}
signingConfig = signingConfigs.getByName("release")
vcsInfo.include = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
compilerOptions {
apiVersion = KotlinVersion.KOTLIN_2_3
languageVersion = KotlinVersion.KOTLIN_2_3
jvmTarget = JvmTarget.JVM_21
}
}
buildFeatures {
compose = true
}
packaging {
resources {
excludes += arrayOf(
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"kotlin/**",
"META-INF/*.version",
"META-INF/**/LICENSE.txt"
)
}
dex {
useLegacyPackaging = true
}
jniLibs {
useLegacyPackaging = true
}
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
lint {
checkReleaseBuilds = false
}
}
dependencies {
implementation(files("libs/expressa-core-release.aar"))
implementation(files("libs/expressa-components-release.aar"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.material.ripple)
implementation(libs.androidx.compose.material.icons)
implementation(libs.androidx.graphics.shapes)
}
Binary file not shown.
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void check*(...);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.kyant.glass",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0.0-dev",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 33
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Glass">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,67 @@
package com.kyant.glass
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import com.kyant.expressa.m3.LocalColorScheme
import com.kyant.expressa.m3.color.ColorScheme
import com.kyant.expressa.overscroll.rememberOffsetOverscrollFactory
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ripple.LocalRippleConfiguration
import com.kyant.expressa.ripple.RippleConfiguration
import com.kyant.expressa.ripple.ripple
import com.kyant.expressa.ui.LocalContentColor
import com.kyant.glass.playground.Preview
import com.kyant.glass.playground.PreviewState
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val rippleConfiguration = remember {
RippleConfiguration(
rippleAlpha = RippleAlpha(
hoveredAlpha = 2f * 0.08f,
focusedAlpha = 2f * 0.10f,
pressedAlpha = 2f * 0.10f,
draggedAlpha = 2f * 0.16f
)
)
}
val overscrollFactory = rememberOffsetOverscrollFactory()
CompositionLocalProvider(
LocalColorScheme provides ColorScheme.systemDynamic()
) {
CompositionLocalProvider(
LocalContentColor provides onSurface,
LocalRippleConfiguration provides rippleConfiguration,
LocalIndication provides ripple(),
LocalOverscrollFactory provides overscrollFactory,
) {
val view = LocalView.current
val background = surfaceContainer
LaunchedEffect(view, background) {
view.rootView.background = background.toArgb().toDrawable()
}
val previewState = remember { PreviewState() }
Preview(previewState)
}
}
}
}
}
@@ -0,0 +1,72 @@
package com.kyant.glass.playground
import android.graphics.RenderEffect
import android.graphics.Shader
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
@Composable
fun <S> BlurredAnimatedContent(
targetState: () -> S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
label: String = "AnimatedContent",
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {
val blurRadius = remember { Animatable(0f) }
LaunchedEffect(Unit) {
snapshotFlow { targetState() }
.drop(1)
.collectLatest {
blurRadius.animateTo(24f, spring(stiffness = 2000f))
blurRadius.animateTo(0f, spring(stiffness = 2000f))
}
}
AnimatedContent(
targetState(),
modifier = modifier.graphicsLayer {
val blurRadiusPx = blurRadius.value.dp.toPx()
if (blurRadiusPx > 0f) {
renderEffect =
RenderEffect.createBlurEffect(
blurRadiusPx,
blurRadiusPx,
Shader.TileMode.DECAL
).asComposeRenderEffect()
}
},
transitionSpec = transitionSpec,
contentAlignment = contentAlignment,
label = label,
contentKey = contentKey,
content = content
)
}
@@ -0,0 +1,103 @@
package com.kyant.glass.playground
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
@Composable
fun BottomSheet(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val density = LocalDensity.current
var maxExpandedHeight by remember { mutableFloatStateOf(0f) }
val peekHeight = with(density) { 44.dp.toPx() }
var offset by rememberSaveable { mutableFloatStateOf(with(density) { -256.dp.toPx() }) }
Box(
modifier
.fillMaxSize()
.safeDrawingPadding()
) {
Layout(
{
Box(
Modifier
.draggable(
rememberDraggableState { delta ->
offset = (offset + delta).fastCoerceIn(-maxExpandedHeight, -peekHeight)
},
Orientation.Vertical
)
.padding(vertical = 12.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Box(
Modifier
.clip(CornerShape.full)
.background(primary)
.size(48.dp, 6.dp)
)
}
Box(
Modifier
.shadow(
elevation = 8.dp,
shape = CornerShape.extraLarge,
ambientColor = Color.Black.copy(alpha = 0.3f),
spotColor = Color.Black.copy(alpha = 0.3f)
)
.background(surface)
) {
content()
}
}
) { measurables, constraints ->
val dragHandlePlaceable = measurables[0].measure(constraints)
val dragHandleHeight = dragHandlePlaceable.height
val sheetHeight = (peekHeight - offset).fastRoundToInt()
val sheetPlaceable = measurables[1].measure(
constraints.copy(
minHeight = sheetHeight,
maxHeight = sheetHeight
)
)
maxExpandedHeight = constraints.maxHeight - peekHeight - dragHandleHeight
layout(constraints.maxWidth, constraints.maxHeight) {
dragHandlePlaceable.place(0, constraints.maxHeight - sheetHeight - dragHandleHeight)
sheetPlaceable.place(0, constraints.maxHeight - sheetHeight)
}
}
}
}
@@ -0,0 +1,139 @@
package com.kyant.glass.playground
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kyant.expressa.components.iconbutton.IconButton
import com.kyant.expressa.components.iconbutton.IconButtonColors
import com.kyant.expressa.components.iconbutton.IconButtonSizes
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.glass.R
import kotlinx.coroutines.launch
@Composable
fun ButtonGroup(
state: PreviewState,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
Row(
modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
{ scope.launch { state.reset() } },
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = tertiaryContainer,
contentColor = onTertiaryContainer
)
) {
Icon(
rememberVectorPainter(Icons.Outlined.Restore),
stringResource(R.string.reset),
)
}
Box(Modifier.weight(1f)) {
Row(
modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
{ state.displayControls = !state.displayControls },
sizes = IconButtonSizes.medium,
colors =
if (state.displayControls) IconButtonColors.filled()
else IconButtonColors.tonal()
) {
Icon(
rememberVectorPainter(
if (state.displayControls) Icons.Outlined.Visibility
else Icons.Outlined.VisibilityOff
),
stringResource(
if (state.displayControls) R.string.hide_controls
else R.string.show_controls
)
)
}
IconButton(
{ state.configurationMode = ConfigurationMode.Color },
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal()
) {
Icon(
rememberVectorPainter(Icons.Outlined.Palette),
stringResource(R.string.color)
)
}
IconButton(
{ state.configurationMode = ConfigurationMode.Advanced },
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
containerColor = primaryContainer,
contentColor = onPrimaryContainer
)
) {
Icon(
rememberVectorPainter(Icons.Outlined.Build),
stringResource(R.string.advanced_settings)
)
}
}
}
val context = LocalContext.current
val pickMedia = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
if (uri != null) {
state.loadImage(context, uri)
}
}
IconButton(
{
pickMedia.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
},
sizes = IconButtonSizes.medium
) {
Icon(
rememberVectorPainter(Icons.Outlined.Photo),
stringResource(R.string.pick_an_image)
)
}
}
}
@@ -0,0 +1,118 @@
package com.kyant.glass.playground
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BlurOn
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.ColorLens
import androidx.compose.material.icons.outlined.Opacity
import androidx.compose.material.icons.outlined.Radar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kyant.expressa.components.iconbutton.IconButton
import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.expressa.ui.Text
import com.kyant.glass.R
@Composable
fun ConfigurationBottomSheet(
state: PreviewState,
modifier: Modifier = Modifier
) {
Column(
modifier
.shadow(
elevation = 8.dp,
shape = CornerShape.extraLarge,
ambientColor = Color.Black.copy(alpha = 0.3f),
spotColor = Color.Black.copy(alpha = 0.3f)
)
.background(surface)
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier
.fillMaxWidth()
.padding(start = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
when (state.configurationMode) {
ConfigurationMode.Color -> stringResource(R.string.color)
ConfigurationMode.Advanced -> stringResource(R.string.advanced_settings)
null -> ""
},
titleMedium,
Modifier.weight(1f)
)
IconButton(
{ state.configurationMode = null }
) {
Icon(
rememberVectorPainter(Icons.Outlined.Close),
stringResource(R.string.close)
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
val retainedConfigurationMode = remember {
Snapshot.withoutReadObservation { state.configurationMode }
}
when (retainedConfigurationMode) {
ConfigurationMode.Color -> {
SliderChip(
state.blurRadius,
stringResource(R.string.blur_radius),
{ Icon(rememberVectorPainter(Icons.Outlined.BlurOn)) },
Modifier.fillMaxWidth()
)
SliderChip(
state.opacity,
stringResource(R.string.opacity),
{ Icon(rememberVectorPainter(Icons.Outlined.Opacity)) },
Modifier.fillMaxWidth()
)
SliderChip(
state.chromaMultiplier,
stringResource(R.string.chroma_multiplier),
{ Icon(rememberVectorPainter(Icons.Outlined.ColorLens)) },
Modifier.fillMaxWidth()
)
}
ConfigurationMode.Advanced -> {
SliderChip(
state.eccentricFactor,
stringResource(R.string.eccentric_factor),
{ Icon(rememberVectorPainter(Icons.Outlined.Radar)) },
Modifier.fillMaxWidth()
)
}
null -> {}
}
}
}
}
@@ -0,0 +1,6 @@
package com.kyant.glass.playground
enum class ConfigurationMode {
Color,
Advanced
}
@@ -0,0 +1,363 @@
package com.kyant.glass.playground
import android.graphics.RenderEffect
import android.graphics.RuntimeShader
import android.graphics.Shader
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.coerceIn
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.util.fastCoerceAtMost
import com.kyant.expressa.prelude.*
import com.kyant.glass.R
@Composable
fun Preview(state: PreviewState) {
val graphicsLayer = rememberGraphicsLayer()
val backgroundColor = surfaceContainerLowest
val containerSize = LocalWindowInfo.current.containerSize
var rect by remember { mutableStateOf(Rect.Zero) }
val painter =
state.imageBitmap?.let { BitmapPainter(it) }
?: painterResource(R.drawable.homescreen)
Box(
Modifier.fillMaxSize()
) {
// background
Image(
painter, null,
Modifier
.fillMaxSize()
.drawWithContent {
drawContent()
graphicsLayer.record {
drawRect(backgroundColor)
this@drawWithContent.drawContent()
}
},
contentScale = ContentScale.Crop
)
// glass
Box(
Modifier
.graphicsLayer { // transform and clip
translationX = state.offset.x
translationY = state.offset.y
clip = true
shape = RoundedCornerShape(state.cornerRadius.value)
}
.graphicsLayer { // dispersion effect
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
uniform float cornerRadius;
uniform float dispersionHeight;
float lerp(float a, float b, float t) {
return a + (b - a) * t;
}
float2 normalToTangent(float2 normal) {
return float2(normal.y, -normal.x);
}
float circleMap(float x) {
return 1.0 - sqrt(1.0 - x * x);
}
float sdRectangle(float2 coord, float2 halfSize) {
float2 d = abs(coord) - halfSize;
float outside = length(max(d, 0.0));
float inside = min(max(d.x, d.y), 0.0);
return outside + inside;
}
float sdRoundedRectangle(float2 coord, float2 halfSize, float radius) {
return sdRectangle(coord, halfSize) - radius;
}
const float eps = 0.5;
const float2 epsX = float2(eps, 0.0);
const float2 epsY = float2(0.0, eps);
float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float radius) {
float dx = sdRoundedRectangle(coord + epsX, halfSize, radius)
- sdRoundedRectangle(coord - epsX, halfSize, radius);
float dy = sdRoundedRectangle(coord + epsY, halfSize, radius)
- sdRoundedRectangle(coord - epsY, halfSize, radius);
return normalize(float2(dx, dy));
}
half4 main(float2 coord) {
float2 halfSize = size * 0.5;
float2 centeredCoord = coord - halfSize;
float2 innerHalfSize = halfSize - float2(cornerRadius);
float sd = sdRoundedRectangle(centeredCoord, innerHalfSize, cornerRadius);
if (sd < 0.0 && -sd < dispersionHeight) {
float2 normal = gradSdRoundedRectangle(centeredCoord, innerHalfSize, cornerRadius);
float2 tangent = normalToTangent(normal);
half4 dispersedColor = half4(0.0);
half4 weight = half4(0.0);
float dispersionFraction = 1.0 - -sd / dispersionHeight;
float dispersionWidth = dispersionHeight * 2.0 * pow(circleMap(dispersionFraction), 2.0);
if (dispersionWidth < 2.0) {
half4 color = image.eval(coord);
return color;
}
float maxI = min(dispersionWidth, 100.0);
for (float i = 0.0; i < 100.0; i++) {
float t = i / maxI;
if (t > 1.0) break;
half4 color = image.eval(coord + tangent * float2(t - 0.5) * dispersionWidth);
if (t >= 0.0 && t < 0.5) {
dispersedColor.b += color.b;
weight.b += 1.0;
}
if (t > 0.25 && t < 0.75) {
dispersedColor.g += color.g;
weight.g += 1.0;
}
if (t > 0.5 && t <= 1.0) {
dispersedColor.r += color.r;
weight.r += 1.0;
}
}
dispersedColor /= weight;
half4 color = image.eval(coord);
dispersedColor.a = color.a;
half4 blendedColor = dispersedColor;
return blendedColor;
} else {
half4 color = image.eval(coord);
return color;
}
}"""
).apply {
val cornerRadius =
state.cornerRadius.value.toPx()
.fastCoerceAtMost(size.minDimension / 2f)
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", cornerRadius)
setFloatUniform("dispersionHeight", state.dispersionHeight.value.toPx())
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // chroma boost
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float chromaMultiplier;
const half3 rgbToY = half3(0.299, 0.587, 0.114);
half3 saturateColor(half3 color, float amount) {
half luma = dot(color, rgbToY);
return clamp(mix(half3(luma), color, amount), 0.0, 1.0);
}
half4 main(float2 coord) {
half4 color = image.eval(coord);
color.rgb = saturateColor(color.rgb, chromaMultiplier);
return color;
}
"""
).apply {
setFloatUniform("chromaMultiplier", state.chromaMultiplier.value)
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // glass effect
renderEffect = RenderEffect.createRuntimeShaderEffect(
RuntimeShader(
"""// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
uniform float cornerRadius;
uniform float refractionHeight;
uniform float refractionAmount;
uniform float eccentricFactor;
float circleMap(float x) {
return 1.0 - sqrt(1.0 - x * x);
}
float sdRectangle(float2 coord, float2 halfSize) {
float2 d = abs(coord) - halfSize;
float outside = length(max(d, 0.0));
float inside = min(max(d.x, d.y), 0.0);
return outside + inside;
}
float sdRoundedRectangle(float2 coord, float2 halfSize, float radius) {
return sdRectangle(coord, halfSize) - radius;
}
const float eps = 0.5;
const float2 epsX = float2(eps, 0.0);
const float2 epsY = float2(0.0, eps);
float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float radius) {
float dx = sdRoundedRectangle(coord + epsX, halfSize, radius)
- sdRoundedRectangle(coord - epsX, halfSize, radius);
float dy = sdRoundedRectangle(coord + epsY, halfSize, radius)
- sdRoundedRectangle(coord - epsY, halfSize, radius);
return normalize(float2(dx, dy));
}
half4 main(float2 coord) {
float2 halfSize = size * 0.5;
float2 centeredCoord = coord - halfSize;
float2 innerHalfSize = halfSize - float2(cornerRadius);
float sd = sdRoundedRectangle(centeredCoord, innerHalfSize, cornerRadius);
if (sd < 0.0 && -sd < refractionHeight) {
float2 normal = gradSdRoundedRectangle(centeredCoord, innerHalfSize, cornerRadius);
float refractedDistance = circleMap(1.0 - -sd / refractionHeight) * refractionAmount;
float2 refractedDirection = normalize(normal + eccentricFactor * normalize(centeredCoord));
float2 refractedCoord = coord + refractedDistance * refractedDirection;
half4 refractedColor = image.eval(refractedCoord);
return refractedColor;
} else {
half4 color = image.eval(coord);
return color;
}
}"""
).apply {
val cornerRadius =
state.cornerRadius.value.toPx()
.fastCoerceAtMost(size.minDimension / 2f)
setFloatUniform("size", size.width, size.height)
setFloatUniform("cornerRadius", cornerRadius)
setFloatUniform("refractionHeight", state.refractionHeight.value.toPx())
setFloatUniform("refractionAmount", state.refractionAmount.value.toPx())
setFloatUniform("eccentricFactor", state.eccentricFactor.value)
},
"image"
).asComposeRenderEffect()
}
.graphicsLayer { // blur
val blurRadiusPx = state.blurRadius.value.toPx()
if (blurRadiusPx > 0f) {
renderEffect =
RenderEffect.createBlurEffect(
blurRadiusPx,
blurRadiusPx,
Shader.TileMode.DECAL
).asComposeRenderEffect()
}
}
.drawBehind {
translate(-rect.left, -rect.top) {
drawLayer(graphicsLayer)
}
drawRect(
Color.Black,
alpha = state.opacity.value
)
}
.onGloballyPositioned { layoutCoordinates ->
rect = layoutCoordinates.boundsInParent()
}
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
state.offset += pan
state.size = DpSize(
(state.size.width * zoom).coerceIn(
state.minSize.width,
containerSize.width.toDp()
),
(state.size.height * zoom).coerceIn(
state.minSize.height,
containerSize.height.toDp()
)
)
}
}
.layout { measurable, constraints ->
val size = state.size.toSize().roundToIntSize()
val width = size.width.coerceAtMost(constraints.maxWidth)
val height = size.height.coerceAtMost(constraints.maxHeight)
val placeable = measurable.measure(Constraints.fixed(width, height))
layout(width, height) {
placeable.place(0, 0)
}
}
.align(Alignment.Center)
)
BlurredAnimatedContent({ state.displayControls }) { visible ->
if (visible) {
PreviewControls(
state,
{ rect }
)
}
}
BlurredAnimatedContent(
{ state.isInConfigurationMode },
Modifier
.padding(16.dp)
.safeDrawingPadding()
.align(Alignment.BottomCenter)
) { isInConfigurationMode ->
if (!isInConfigurationMode) {
ButtonGroup(state)
} else {
ConfigurationBottomSheet(state)
}
}
}
}
@@ -0,0 +1,175 @@
package com.kyant.glass.playground
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Height
import androidx.compose.material.icons.outlined.Lens
import androidx.compose.material.icons.outlined.RoundedCorner
import androidx.compose.material.icons.outlined.WaterDrop
import androidx.compose.material.icons.outlined.Window
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.glass.R
import kotlin.math.sqrt
@Composable
fun PreviewControls(
state: PreviewState,
rect: () -> Rect,
modifier: Modifier = Modifier
) {
Box(modifier.fillMaxSize()) {
// width
Icon(
rememberVectorPainter(Icons.Outlined.Height),
stringResource(R.string.width),
onTertiaryContainer,
Modifier
.graphicsLayer {
val rect = rect()
translationX = rect.right + 8.dp.toPx()
translationY = rect.center.y - size.height / 2
}
.border(
2.dp,
tertiary,
CornerShape.full
)
.clip(CornerShape.full)
.background(tertiaryContainer)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
val delta = dragAmount.x * 2
state.size =
state.size.copy(
width =
(state.size.width + delta.toDp())
.coerceAtLeast(state.minSize.width)
)
}
}
.padding(12.dp)
.size(24.dp)
.graphicsLayer {
rotationZ = 90f
}
)
// height
Icon(
rememberVectorPainter(Icons.Outlined.Height),
stringResource(R.string.height),
onTertiaryContainer,
Modifier
.graphicsLayer {
val rect = rect()
translationX = rect.center.x - size.width / 2
translationY = rect.bottom + 8.dp.toPx()
}
.border(
2.dp,
tertiary,
CornerShape.full
)
.clip(CornerShape.full)
.background(tertiaryContainer)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
val delta = dragAmount.y * 2
state.size =
state.size.copy(
height =
(state.size.height + delta.toDp())
.coerceAtLeast(state.minSize.height)
)
}
}
.padding(12.dp)
.size(24.dp)
)
// corner radius
SliderChip(
state.cornerRadius,
null,
{
Icon(
rememberVectorPainter(Icons.Outlined.RoundedCorner),
stringResource(R.string.corner_radius),
Modifier.graphicsLayer {
rotationX = 180f
}
)
},
Modifier
.graphicsLayer {
val rect = rect()
val cornerRadiusOffset =
(-state.cornerRadius.value.toPx() + 8.dp.toPx()) / (2 * sqrt(2f))
translationX = rect.right + cornerRadiusOffset
translationY = rect.bottom + cornerRadiusOffset
}
.border(
2.dp,
primary,
CornerShape.full
),
dragDelta = { dragAmount -> (-dragAmount.x - dragAmount.y) / sqrt(2f) }
)
// refraction
Column(
Modifier
.width(IntrinsicSize.Max)
.graphicsLayer {
val rect = rect()
translationX = rect.center.x - size.width / 2
translationY = rect.top - size.height - 8.dp.toPx()
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
SliderChip(
state.dispersionHeight,
stringResource(R.string.dispersion_height),
{ Icon(rememberVectorPainter(Icons.Outlined.Lens)) },
Modifier.fillMaxWidth()
)
SliderChip(
state.refractionAmount,
stringResource(R.string.refraction_amount),
{ Icon(rememberVectorPainter(Icons.Outlined.WaterDrop)) },
Modifier.fillMaxWidth()
)
SliderChip(
state.refractionHeight,
stringResource(R.string.refraction_height),
{ Icon(rememberVectorPainter(Icons.Outlined.Window)) },
Modifier.fillMaxWidth()
)
}
}
}
@@ -0,0 +1,212 @@
package com.kyant.glass.playground
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateTo
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.lerp
import com.kyant.expressa.m3.motion.MotionScheme
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class PreviewState {
var configurationMode: ConfigurationMode? by mutableStateOf(null)
val isInConfigurationMode: Boolean by derivedStateOf {
configurationMode != null
}
var displayControls: Boolean by mutableStateOf(true)
var imageBitmap: ImageBitmap? by mutableStateOf(null)
val minSize: DpSize = DpSize(24.dp, 24.dp)
var size: DpSize by mutableStateOf(DpSize(200.dp, 200.dp))
var offset: Offset by mutableStateOf(Offset.Zero)
val blurRadius: LiquidGlassParamValue<Dp> =
LiquidGlassParamValue(
initialValue = 0.dp,
valueRange = { 0.dp..24.dp },
typeConverter = Dp.VectorConverter,
valueLabel = { "${it.value.fastRoundToInt()} dp" }
)
val opacity: LiquidGlassParamValue<Float> =
LiquidGlassParamValue(
initialValue = 0f,
valueRange = { 0f..0.2f },
typeConverter = Float.VectorConverter,
valueLabel = { "${(it * 100).fastRoundToInt()}%" }
)
val chromaMultiplier: LiquidGlassParamValue<Float> =
LiquidGlassParamValue(
initialValue = 1f,
valueRange = { 0.5f..2f },
typeConverter = Float.VectorConverter,
valueLabel = { "%.2f".format(it) }
)
val cornerRadius: LiquidGlassParamValue<Dp> =
LiquidGlassParamValue(
initialValue = 32.dp,
valueRange = { 0.dp..min(size.width, size.height) / 2 },
typeConverter = Dp.VectorConverter,
valueLabel = { "${it.value.fastRoundToInt()} dp" }
)
val refractionHeight: LiquidGlassParamValue<Dp> =
LiquidGlassParamValue(
initialValue = 24.dp,
valueRange = { 0.dp..cornerRadius.value },
typeConverter = Dp.VectorConverter,
valueLabel = { "${it.value.fastRoundToInt()} dp" }
)
val refractionAmount: LiquidGlassParamValue<Dp> =
LiquidGlassParamValue(
initialValue = (-32).dp,
valueRange = { (-512).dp..0.dp },
typeConverter = Dp.VectorConverter,
valueLabel = { "${it.value.fastRoundToInt()} dp" }
)
val dispersionHeight: LiquidGlassParamValue<Dp> =
LiquidGlassParamValue(
initialValue = 0.dp,
valueRange = { 0.dp..cornerRadius.value },
typeConverter = Dp.VectorConverter,
valueLabel = { "${it.value.fastRoundToInt()} dp" }
)
val eccentricFactor: LiquidGlassParamValue<Float> =
LiquidGlassParamValue(
initialValue = 1f,
valueRange = { 0f..1f },
typeConverter = Float.VectorConverter,
valueLabel = { "%.2f".format(it) }
)
fun loadImage(context: Context, uri: Uri) {
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
imageBitmap = bitmap.asImageBitmap()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
suspend fun reset() {
imageBitmap = null
coroutineScope {
launch {
AnimationState(
TwoWayConverter(
convertToVector = { AnimationVector2D(it.width.value, it.height.value) },
convertFromVector = { DpSize(it.v1.dp, it.v2.dp) },
), size
)
.animateTo(DpSize(200.dp, 200.dp), MotionScheme.slowSpatial()) {
size = value
}
}
launch {
AnimationState(Offset.VectorConverter, offset)
.animateTo(Offset.Zero, MotionScheme.slowSpatial()) {
offset = value
}
}
launch { blurRadius.reset() }
launch { opacity.reset() }
launch { chromaMultiplier.reset() }
launch { cornerRadius.reset() }
launch { refractionHeight.reset() }
launch { refractionAmount.reset() }
launch { dispersionHeight.reset() }
launch { eccentricFactor.reset() }
}
}
inner class LiquidGlassParamValue<T : Comparable<T>>(
val initialValue: T,
private val valueRange: () -> ClosedRange<T>,
private val typeConverter: TwoWayConverter<T, AnimationVector1D>,
private val valueLabel: (T) -> String
) {
private var state: T by mutableStateOf(initialValue)
val value: T by derivedStateOf {
state.coerceIn(valueRange())
}
val unsafeValue: T
get() = state
val isValid: Boolean by derivedStateOf {
state in valueRange()
}
val progress: Float by derivedStateOf {
val range = valueRange()
if (range.isEmpty()) {
0f
} else {
val current = typeConverter.convertToVector(value).value
val start = typeConverter.convertToVector(range.start).value
val end = typeConverter.convertToVector(range.endInclusive).value
if (start == end) {
0f
} else {
(current - start) / (end - start)
}
}
}
val label: String by derivedStateOf {
valueLabel(value)
}
val unsafeLabel: String by derivedStateOf {
valueLabel(unsafeValue)
}
fun setValue(value: T) {
state = value.coerceIn(valueRange())
}
fun setFloatValue(value: Float) {
setValue(typeConverter.convertFromVector(AnimationVector1D(value)))
}
fun setProgress(progress: Float) {
val range = valueRange()
val start = typeConverter.convertToVector(range.start).value
val end = typeConverter.convertToVector(range.endInclusive).value
setFloatValue(lerp(start, end, progress))
}
suspend fun reset() {
AnimationState(typeConverter, state)
.animateTo(initialValue, MotionScheme.slowSpatial()) {
setValue(value)
}
}
}
}
@@ -0,0 +1,95 @@
package com.kyant.glass.playground
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.kyant.expressa.m3.motion.MotionScheme
import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.LocalTextStyle
import com.kyant.expressa.ui.ProvideTextStyle
import com.kyant.expressa.ui.Text
@Composable
fun <T : Comparable<T>> SliderChip(
value: PreviewState.LiquidGlassParamValue<T>,
label: String?,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
dragDelta: (dragAmount: Offset) -> Float = { it.x }
) {
val containerColor = surfaceContainer
val sliderColor = primaryContainer
val errorColor = errorContainer
Row(
modifier
.clip(CornerShape.full)
.drawBehind {
if (value.isValid) {
drawRect(containerColor)
drawRect(
sliderColor,
size = size.copy(width = size.width * value.progress)
)
} else {
drawRect(errorColor)
}
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
val delta = dragDelta(dragAmount) / size.width
value.setProgress(value.progress + delta)
}
}
.padding(12.dp, 8.dp)
.animateContentSize(MotionScheme.fastSpatial()),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon()
Column {
if (label != null) {
Text(
label,
bodyMedium
)
}
ProvideTextStyle(
if (label != null) labelMedium
else labelLarge
) {
if (value.isValid) {
Text(
value.label,
primary
)
} else {
Row {
Text(
value.unsafeLabel,
LocalTextStyle.current.copy(textDecoration = TextDecoration.LineThrough),
error
)
Text(" -> ")
Text(value.label)
}
}
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#000000">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="201.6"
android:translateY="201.6">
<path
android:fillColor="@android:color/white"
android:pathData="M240,600Q223,600 211.5,588.5Q200,577 200,560Q200,543 211.5,531.5Q223,520 240,520Q257,520 268.5,531.5Q280,543 280,560Q280,577 268.5,588.5Q257,600 240,600ZM240,760Q223,760 211.5,748.5Q200,737 200,720Q200,703 211.5,691.5Q223,680 240,680Q257,680 268.5,691.5Q280,703 280,720Q280,737 268.5,748.5Q257,760 240,760ZM240,440Q223,440 211.5,428.5Q200,417 200,400Q200,383 211.5,371.5Q223,360 240,360Q257,360 268.5,371.5Q280,383 280,400Q280,417 268.5,428.5Q257,440 240,440ZM120,420Q112,420 106,414Q100,408 100,400Q100,392 106,386Q112,380 120,380Q128,380 134,386Q140,392 140,400Q140,408 134,414Q128,420 120,420ZM240,280Q223,280 211.5,268.5Q200,257 200,240Q200,223 211.5,211.5Q223,200 240,200Q257,200 268.5,211.5Q280,223 280,240Q280,257 268.5,268.5Q257,280 240,280ZM120,580Q112,580 106,574Q100,568 100,560Q100,552 106,546Q112,540 120,540Q128,540 134,546Q140,552 140,560Q140,568 134,574Q128,580 120,580ZM400,860Q392,860 386,854Q380,848 380,840Q380,832 386,826Q392,820 400,820Q408,820 414,826Q420,832 420,840Q420,848 414,854Q408,860 400,860ZM400,140Q392,140 386,134Q380,128 380,120Q380,112 386,106Q392,100 400,100Q408,100 414,106Q420,112 420,120Q420,128 414,134Q408,140 400,140ZM400,280Q383,280 371.5,268.5Q360,257 360,240Q360,223 371.5,211.5Q383,200 400,200Q417,200 428.5,211.5Q440,223 440,240Q440,257 428.5,268.5Q417,280 400,280ZM400,620Q375,620 357.5,602.5Q340,585 340,560Q340,535 357.5,517.5Q375,500 400,500Q425,500 442.5,517.5Q460,535 460,560Q460,585 442.5,602.5Q425,620 400,620ZM400,460Q375,460 357.5,442.5Q340,425 340,400Q340,375 357.5,357.5Q375,340 400,340Q425,340 442.5,357.5Q460,375 460,400Q460,425 442.5,442.5Q425,460 400,460ZM400,760Q383,760 371.5,748.5Q360,737 360,720Q360,703 371.5,691.5Q383,680 400,680Q417,680 428.5,691.5Q440,703 440,720Q440,737 428.5,748.5Q417,760 400,760ZM480,840L480,760Q596,760 678,678Q760,596 760,480Q760,364 678,282Q596,200 480,200L480,120Q554,120 619.5,148.5Q685,177 734,226Q783,275 811.5,340.5Q840,406 840,480Q840,554 811.5,619.5Q783,685 734,734Q685,783 619.5,811.5Q554,840 480,840ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</group>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/system_accent1_100" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="isLightTheme">false</bool>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">液态玻璃</string>
<string name="advanced_settings">高级设置</string>
<string name="blur_radius">模糊半径</string>
<string name="chroma_multiplier">彩度增强系数</string>
<string name="close">关闭</string>
<string name="color">颜色</string>
<string name="corner_radius">圆角半径</string>
<string name="dispersion_height">色散高度</string>
<string name="eccentric_factor">离心系数</string>
<string name="height">高度</string>
<string name="hide_controls">隐藏控件</string>
<string name="opacity">不透明度</string>
<string name="pick_an_image">选择图像</string>
<string name="refraction_height">折射高度</string>
<string name="refraction_amount">折射量</string>
<string name="reset">重置</string>
<string name="show_controls">显示控件</string>
<string name="width">宽度</string>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="isLightTheme">true</bool>
</resources>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Liquid Glass</string>
<string name="advanced_settings">Advanced settings</string>
<string name="blur_radius">Blur radius</string>
<string name="chroma_multiplier">Chroma multiplier</string>
<string name="close">Close</string>
<string name="color">Color</string>
<string name="corner_radius">Corner radius</string>
<string name="dispersion_height">Dispersion height</string>
<string name="eccentric_factor">Eccentric factor</string>
<string name="height">Height</string>
<string name="hide_controls">Hide controls</string>
<string name="opacity">Opacity</string>
<string name="pick_an_image">Pick an image</string>
<string name="refraction_height">Refraction height</string>
<string name="refraction_amount">Refraction amount</string>
<string name="reset">Reset</string>
<string name="show_controls">Show controls</string>
<string name="width">Width</string>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Glass" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">@bool/isLightTheme</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightNavigationBar">@bool/isLightTheme</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">always</item>
</style>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>