Initial commit
This commit is contained in:
commit
2d3cc0912f
54 files changed
+2477
No files matched your search
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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.
Vendored
+27
@@ -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.
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="isLightTheme">true</bool>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user