, FractalNavChildScope by fakeChildScope(this) {
62 | @Composable
63 | override fun ChildNode(node: N) {
64 | val thumbnailType = thumbnail(node)
65 | nodeContent(node, onClick = when (thumbnailType) {
66 | is ThumbnailType.Leaf -> null
67 | ThumbnailType.Parent -> {
68 | { zoomToChild(nodeKey(node)) }
69 | }
70 | }) { modifier ->
71 | Crossfade(thumbnailType as ThumbnailType, modifier
72 | .fillMaxSize()
73 | .wrapContentSize()) { thumbnailType ->
74 | when (thumbnailType) {
75 | ThumbnailType.Parent -> {
76 | FractalNavChild(
77 | key = nodeKey(node),
78 | modifier = Modifier
79 | ) {
80 | val childScope = this
81 | childScope.ParentNode(
82 | node,
83 | nodeKey,
84 | childrenContent,
85 | nodeContent,
86 | thumbnail
87 | )
88 | if (isFullyZoomedIn || zoomDirection == ZoomingIn) {
89 | BackHandler {
90 | zoomToParent()
91 | }
92 | }
93 | }
94 | }
95 | is ThumbnailType.Leaf -> {
96 | thumbnailType.content()
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | scope.childrenContent(node)
104 | }
105 |
106 | private fun fakeChildScope(fractalNavScope: FractalNavScope): FractalNavChildScope {
107 | return (fractalNavScope as? FractalNavChildScope)
108 | ?: object : FractalNavChildScope,
109 | FractalNavScope by fractalNavScope {
110 | override val isActive: Boolean
111 | get() = true
112 | override val isFullyZoomedIn: Boolean
113 | get() = true
114 | override val zoomFactor: Float
115 | get() = 1f
116 | override val zoomDirection: ZoomDirection?
117 | get() = null
118 |
119 | override fun zoomToParent() {}
120 | override fun Modifier.fillExpandedWidth(): Modifier = this
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/filebrowser/src/main/java/com/zachklipp/filebrowser/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.filebrowser.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/filebrowser/src/main/java/com/zachklipp/filebrowser/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.filebrowser.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/filebrowser/src/main/java/com/zachklipp/filebrowser/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.filebrowser.ui.theme
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.darkColors
5 | import androidx.compose.runtime.Composable
6 |
7 | private val DarkColorPalette = darkColors(
8 | primary = Purple200,
9 | primaryVariant = Purple700,
10 | secondary = Teal200
11 | )
12 |
13 | @Composable
14 | fun FileBrowserTheme(content: @Composable () -> Unit) {
15 | MaterialTheme(
16 | colors = DarkColorPalette,
17 | typography = Typography,
18 | shapes = Shapes,
19 | content = content
20 | )
21 | }
--------------------------------------------------------------------------------
/filebrowser/src/main/java/com/zachklipp/filebrowser/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.filebrowser.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/filebrowser/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/filebrowser/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/filebrowser/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | fractal-nav
3 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/filebrowser/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/fractalnav/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/fractalnav/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.zachklipp.fractalnav'
8 | compileSdk 32
9 |
10 | defaultConfig {
11 | minSdk 21
12 | targetSdk 32
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | vectorDrawables {
18 | useSupportLibrary true
19 | }
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | kotlinOptions {
33 | jvmTarget = '1.8'
34 | }
35 | buildFeatures {
36 | compose true
37 | }
38 | composeOptions {
39 | kotlinCompilerExtensionVersion '1.1.1'
40 | }
41 | packagingOptions {
42 | resources {
43 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
44 | }
45 | }
46 | }
47 |
48 | dependencies {
49 | implementation "androidx.compose.foundation:foundation:$compose_ui_version"
50 | implementation "androidx.compose.ui:ui:$compose_ui_version"
51 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
52 | testImplementation 'junit:junit:4.13.2'
53 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
55 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
56 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
57 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
58 | }
--------------------------------------------------------------------------------
/fractalnav/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/fractalnav/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/FakeMovableContent.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | // TODO replace with real movableContentOf when fixed. Currently it crashes when zooming out.
6 | //fun movableContentOf(content: @Composable (P) -> Unit): @Composable (P) -> Unit {
7 | // return content
8 | //}
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/FractalChild.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.animation.core.AnimationSpec
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.relocation.BringIntoViewRequester
6 | import androidx.compose.runtime.*
7 | import androidx.compose.runtime.snapshots.Snapshot
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.layout.LayoutCoordinates
10 | import androidx.compose.ui.layout.layout
11 | import androidx.compose.ui.unit.IntOffset
12 | import androidx.compose.ui.unit.constrainHeight
13 | import androidx.compose.ui.unit.constrainWidth
14 |
15 | internal interface FractalParent : FractalNavScope {
16 | val activeChild: FractalChild?
17 | val zoomDirection: ZoomDirection?
18 | val zoomAnimationSpecFactory: () -> AnimationSpec
19 | val viewportWidth: Int
20 |
21 | fun zoomOut()
22 | }
23 |
24 | @OptIn(ExperimentalFoundationApi::class)
25 | internal class FractalChild(
26 | val key: String,
27 | private val parent: FractalParent
28 | ) {
29 | private val childState = FractalNavState() as FractalNavStateImpl
30 | private val childScope = ChildScope(childState)
31 | private var _content: (@Composable FractalNavChildScope.() -> Unit)? by mutableStateOf(null)
32 | var placeholderCoordinates: LayoutCoordinates? by mutableStateOf(null)
33 | val bringIntoViewRequester = BringIntoViewRequester()
34 |
35 | /**
36 | * A [movableContentOf] wrapper that allows all state inside the child's composable to be moved
37 | * from being composed from inside the content of a [FractalNavHost] to being the root of
38 | * the host.
39 | */
40 | private val movableContent = movableContentOf { modifier: Modifier ->
41 | FractalNavHost(
42 | state = childState,
43 | modifier = modifier,
44 | zoomAnimationSpecFactory = { parent.zoomAnimationSpecFactory() }
45 | ) {
46 | _content?.invoke(childScope)
47 | }
48 | }
49 |
50 | fun setContent(content: @Composable FractalNavChildScope.() -> Unit) {
51 | if (Snapshot.withoutReadObservation { _content == null }) {
52 | _content = content
53 | }
54 | }
55 |
56 | @Suppress("NOTHING_TO_INLINE")
57 | @Composable
58 | inline fun MovableContent(modifier: Modifier = Modifier) {
59 | movableContent(modifier)
60 | }
61 |
62 | private inner class ChildScope(private val state: FractalNavStateImpl) :
63 | FractalNavScope by state,
64 | FractalNavChildScope {
65 | override val isActive: Boolean by derivedStateOf {
66 | parent.activeChild === this@FractalChild
67 | }
68 | override val isFullyZoomedIn: Boolean by derivedStateOf {
69 | isActive && parent.childZoomFactor == 1f
70 | }
71 | override val zoomFactor: Float by derivedStateOf {
72 | if (isActive) parent.childZoomFactor else 0f
73 | }
74 | override val zoomDirection: ZoomDirection?
75 | get() = if (isActive) parent.zoomDirection else null
76 |
77 | override fun zoomToChild(key: String) {
78 | // An inactive child should never have zoomed-in children, so if this child isn't
79 | // already zoomed in we need to start zooming.
80 | parent.zoomToChild(this@FractalChild.key)
81 | state.zoomToChild(key)
82 | }
83 |
84 | override fun zoomToParent() {
85 | if (state.hasActiveChild) {
86 | // A zoomed-out child should never have zoomed-in descendents.
87 | state.zoomOut()
88 | }
89 | parent.zoomOut()
90 | }
91 |
92 | override fun Modifier.fillExpandedWidth(): Modifier = layout { measurable, constraints ->
93 | val parentWidth = parent.viewportWidth
94 | val parentWidthConstraints = constraints.copy(
95 | minWidth = parentWidth,
96 | maxWidth = parentWidth
97 | )
98 | val placeable = measurable.measure(parentWidthConstraints)
99 | layout(
100 | width = constraints.constrainWidth(placeable.width),
101 | height = constraints.constrainHeight(placeable.height)
102 | ) {
103 | placeable.place(IntOffset.Zero)
104 | }
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/FractalNavHost.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.animation.core.AnimationSpec
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.runtime.*
7 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.layout.onPlaced
10 |
11 | /**
12 | * A container that can host special composables defined with [FractalNavScope.FractalNavChild] that
13 | * can be [zoomed][FractalNavScope.zoomToChild] into and replace the content of this host.
14 | *
15 | * To preserve navigation state across configuration changes, create your own [FractalNavState] and
16 | * store it in a retained configuration instance (e.g. an AAC `ViewModel`).
17 | */
18 | @NonRestartableComposable
19 | @Composable
20 | fun FractalNavHost(
21 | modifier: Modifier = Modifier,
22 | state: FractalNavState = remember { FractalNavState() },
23 | zoomAnimationSpec: AnimationSpec = FractalNavState.DefaultZoomAnimationSpec,
24 | content: @Composable FractalNavScope.() -> Unit
25 | ) {
26 | FractalNavHost(
27 | state = state,
28 | modifier = modifier,
29 | zoomAnimationSpecFactory = { zoomAnimationSpec },
30 | content = content
31 | )
32 | }
33 |
34 | /**
35 | * Stores the navigation state for a [FractalNavHost].
36 | * Should only be passed to a single [FractalNavHost] at a time.
37 | */
38 | sealed interface FractalNavState {
39 | companion object {
40 | val DefaultZoomAnimationSpec: AnimationSpec = tween(1_000)
41 | }
42 | }
43 |
44 | fun FractalNavState(): FractalNavState = FractalNavStateImpl()
45 |
46 | @Composable
47 | internal fun FractalNavHost(
48 | state: FractalNavState,
49 | modifier: Modifier,
50 | zoomAnimationSpecFactory: () -> AnimationSpec,
51 | content: @Composable FractalNavScope.() -> Unit
52 | ) {
53 | state as FractalNavStateImpl
54 | val coroutineScope = rememberCoroutineScope()
55 | SideEffect {
56 | state.coroutineScope = coroutineScope
57 | state.zoomAnimationSpecFactory = zoomAnimationSpecFactory
58 | }
59 |
60 | val contentStateHolder = rememberSaveableStateHolder()
61 |
62 | // This box serves two purposes:
63 | // 1. It defines what the viewport coordinates are, and gives us a place to put the modifier
64 | // to read them that won't become detached in the middle of zoom animations.
65 | // 2. The layout behavior of Box ensures that the active child will be drawn over the content.
66 | Box(
67 | modifier = modifier
68 | .onPlaced { state.viewportCoordinates = it }
69 | .workaroundBoxOnPlacedBug(),
70 | propagateMinConstraints = true
71 | ) {
72 | if (state.composeContent) {
73 | contentStateHolder.SaveableStateProvider("fractal-nav-host") {
74 | Box(
75 | modifier = Modifier
76 | .then(state.contentZoomModifier)
77 | .onPlaced { state.scaledContentCoordinates = it }
78 | .workaroundBoxOnPlacedBug(),
79 | propagateMinConstraints = true
80 | ) {
81 | content(state)
82 | }
83 | }
84 | }
85 |
86 | state.activeChild?.MovableContent(
87 | modifier = if (state.isFullyZoomedIn) Modifier else state.childZoomModifier
88 | )
89 | }
90 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/FractalNavScope.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.graphics.graphicsLayer
6 |
7 | enum class ZoomDirection {
8 | ZoomingIn,
9 | ZoomingOut
10 | }
11 |
12 | interface FractalNavScope {
13 | /**
14 | * True when a child of this scope is being zoomed in or has fully-zoomed in.
15 | * Always false initially.
16 | */
17 | val hasActiveChild: Boolean
18 |
19 | /**
20 | * When [hasActiveChild] is true, this is the zoom factor of the active child.
21 | */
22 | val childZoomFactor: Float
23 |
24 | /**
25 | * Defines a child composable that can be zoomed into by calling [zoomToChild].
26 | * The [content] of this function is automatically placed inside its own [FractalNavHost], and
27 | * so can define its own children.
28 | */
29 | @Composable
30 | fun FractalNavChild(
31 | key: String,
32 | modifier: Modifier,
33 | content: @Composable FractalNavChildScope.() -> Unit
34 | )
35 |
36 | /**
37 | * Requests that the [FractalNavChild] passed the given [key] be activated and eventually
38 | * entirely replace the content of this [FractalNavHost].
39 | */
40 | fun zoomToChild(key: String)
41 | }
42 |
43 | /**
44 | * A [FractalNavScope] that is also a child of a [FractalNavChild].
45 | */
46 | interface FractalNavChildScope : FractalNavScope {
47 | /**
48 | * True if this [FractalNavChild] is being zoomed in or out of, or is fully zoomed in.
49 | * Only one sibling may be active at a time.
50 | * When this is false, [isFullyZoomedIn] will always be false.
51 | */
52 | val isActive: Boolean
53 |
54 | /**
55 | * True if this [FractalNavChild] is completely replacing its parent content.
56 | * When this is true, [isActive] will always be true.
57 | */
58 | val isFullyZoomedIn: Boolean
59 |
60 | /**
61 | * The amount that this child is zoomed, between 0 ([isActive]=false) and
62 | * 1 ([isFullyZoomedIn]=true).
63 | */
64 | val zoomFactor: Float
65 |
66 | /** Null when not zooming. */
67 | val zoomDirection: ZoomDirection?
68 |
69 | /** Requests the parent of this [FractalNavChild] zoom it out and eventually deactivate it. */
70 | fun zoomToParent()
71 |
72 | fun Modifier.scaleLayoutByZoomFactor(): Modifier = scaleLayout { zoomFactor }
73 | fun Modifier.scaleByZoomFactor(): Modifier = graphicsLayer {
74 | scaleX = zoomFactor
75 | scaleY = zoomFactor
76 | }
77 |
78 | fun Modifier.alphaByZoomFactor(): Modifier = graphicsLayer { alpha = zoomFactor }
79 |
80 | /**
81 | * Measures the modified element with the width constraints it will have when fully zoomed-in.
82 | */
83 | fun Modifier.fillExpandedWidth(): Modifier
84 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/FractalNavStateImpl.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.AnimationSpec
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.relocation.bringIntoViewRequester
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.composed
12 | import androidx.compose.ui.geometry.Offset
13 | import androidx.compose.ui.geometry.lerp
14 | import androidx.compose.ui.graphics.BlurEffect
15 | import androidx.compose.ui.graphics.TileMode
16 | import androidx.compose.ui.graphics.TransformOrigin
17 | import androidx.compose.ui.graphics.graphicsLayer
18 | import androidx.compose.ui.layout.LayoutCoordinates
19 | import androidx.compose.ui.layout.layout
20 | import androidx.compose.ui.layout.onPlaced
21 | import androidx.compose.ui.platform.LocalDensity
22 | import androidx.compose.ui.unit.*
23 | import com.zachklipp.fractalnav.ZoomDirection.ZoomingOut
24 | import kotlinx.coroutines.CoroutineScope
25 | import kotlinx.coroutines.launch
26 | import kotlin.math.roundToInt
27 |
28 | internal class FractalNavStateImpl : FractalNavState, FractalNavScope, FractalParent {
29 | // These do not need to be backed by snapshot state because they're set in a side effect.
30 | lateinit var coroutineScope: CoroutineScope
31 | override lateinit var zoomAnimationSpecFactory: () -> AnimationSpec
32 |
33 | private val children = mutableMapOf()
34 | private val zoomFactorAnimatable = Animatable(0f)
35 | private val zoomFactor: Float by zoomFactorAnimatable.asState()
36 | val isFullyZoomedIn by derivedStateOf { zoomFactor == 1f }
37 | override var zoomDirection: ZoomDirection? by mutableStateOf(null)
38 | private set
39 |
40 | /**
41 | * When true, the nav hosts's content should be composed. This is false when a child is fully
42 | * zoomed-in AND is not imminently starting to zoom out. We compose when starting to zoom out
43 | * even before the [zoomFactor] animation has started to give the content a chance to initialize
44 | * before starting the animation, to avoid jank caused by an extra long first-frame.
45 | */
46 | val composeContent: Boolean get() = !isFullyZoomedIn || zoomDirection == ZoomingOut
47 |
48 | var viewportCoordinates: LayoutCoordinates? = null
49 | var scaledContentCoordinates: LayoutCoordinates? by mutableStateOf(null)
50 |
51 | override val viewportWidth: Int
52 | get() = viewportCoordinates?.size?.width ?: 0
53 |
54 | override val hasActiveChild: Boolean get() = activeChild != null
55 | override val childZoomFactor: Float get() = zoomFactor
56 |
57 | /**
58 | * The first time the content, and thus this modifier, is composed after
59 | * starting a zoom-out, this modifier will run before the placeholder has been
60 | * placed, so we can't calculate the scale. We'll return early, and because
61 | * the above layer will have set alpha to 0, it doesn't matter that the content
62 | * is initially in the wrong place/size. The subsequent frame it will be able to
63 | * calculate correctly.
64 | */
65 | private var isContentBeingScaled by mutableStateOf(false)
66 |
67 | /**
68 | * The child that is currently either zoomed in or out, or fully zoomed in. Null when fully
69 | * zoomed out.
70 | */
71 | override var activeChild: FractalChild? by mutableStateOf(null)
72 | private var childPlaceholderSize: IntSize? by mutableStateOf(null)
73 |
74 | /**
75 | * A pair of values representing the x and y scale factors that the content needs to be scaled
76 | * by to make the placeholder fill the screen.
77 | */
78 | private val activeChildScaleTarget: Offset
79 | get() {
80 | val coords = viewportCoordinates?.takeIf { it.isAttached } ?: return Offset(1f, 1f)
81 | val childSize = childPlaceholderSize ?: return Offset(1f, 1f)
82 | return Offset(
83 | x = coords.size.width / childSize.width.toFloat(),
84 | y = coords.size.height / childSize.height.toFloat()
85 | )
86 | }
87 |
88 | val contentZoomModifier
89 | get() = if (activeChild != null) {
90 | Modifier
91 | .graphicsLayer {
92 | // Somehow, even though this modifier should immediately be removed when
93 | // activeChild is set to null, it's still running this block so we can just exit
94 | // early in that case. Relatedly, it seems that if we *don't* return before
95 | // reading zoomFactor, the snapshot system will sometimes throw a
96 | // ConcurrentModificationException.
97 | if (activeChild == null) return@graphicsLayer
98 |
99 | // The scale needs to happen around the center of the placeholder.
100 | val coords = scaledContentCoordinates?.takeIf { it.isAttached }
101 | val childCoords = activeChild?.placeholderCoordinates?.takeIf { it.isAttached }
102 | val childBounds = childCoords?.let {
103 | coords?.localBoundingBoxOf(childCoords, clipBounds = false)
104 | }
105 |
106 | if (coords == null || childBounds == null) {
107 | // If there's an active child but the content's not being scaled it means
108 | // we're on the first frame of a zoom-out and the scale couldn't be
109 | // calculated yet, so the content is in the wrong place, and we shouldn't
110 | // draw it.
111 | alpha = 0f
112 | isContentBeingScaled = false
113 | return@graphicsLayer
114 | }
115 | isContentBeingScaled = true
116 |
117 | val scaleTarget = activeChildScaleTarget
118 | scaleX = lerp(1f, scaleTarget.x, zoomFactor)
119 | scaleY = lerp(1f, scaleTarget.y, zoomFactor)
120 |
121 | val pivot = TransformOrigin(
122 | pivotFractionX = childBounds.center.x / (coords.size.width),
123 | pivotFractionY = childBounds.center.y / (coords.size.height)
124 | )
125 | transformOrigin = pivot
126 |
127 | // And we need to translate so that the left edge of the placeholder will be
128 | // eventually moved to the left edge of the viewport.
129 | val parentCenter = viewportCoordinates!!.size.center.toOffset()
130 | val distanceToCenter = parentCenter - childBounds.center
131 | translationX = zoomFactor * distanceToCenter.x
132 | translationY = zoomFactor * distanceToCenter.y
133 |
134 | // Fade the content out so that if it's drawing its own background it won't
135 | // blink in and out when the content enters/leaves the composition.
136 | alpha = 1f - zoomFactor
137 |
138 | // Radius of 0f causes a crash.
139 | renderEffect = BlurEffect(
140 | // Swap the x and y scale values for the blur radius so the blur scales
141 | // squarely, and not proportionally with the rest of the layer.
142 | radiusX = 0.000001f + (zoomFactor * scaleTarget.y),
143 | radiusY = 0.000001f + (zoomFactor * scaleTarget.x),
144 | // Since this layer is also being clipped, decal gives a better look for
145 | // the gradient near the edges than the default.
146 | edgeTreatment = TileMode.Decal
147 | )
148 | }
149 | .composed {
150 | // When the scale modifier stops being applied, it's not longer scaling.
151 | DisposableEffect(this@FractalNavStateImpl) {
152 | onDispose {
153 | isContentBeingScaled = false
154 | }
155 | }
156 | Modifier
157 | }
158 | } else Modifier
159 |
160 | /**
161 | * Modifier that is applied to the active child when it's currently being zoomed and has been
162 | * removed from the content composition to be composed by the host instead.
163 | */
164 | val childZoomModifier = Modifier
165 | .layout { measurable, constraints ->
166 | // But scale the layout bounds up instead, so the child will grow to fill the space
167 | // previously filled by the content.
168 | val childSize = childPlaceholderSize!!.toSize()
169 | val parentSize = viewportCoordinates!!.size.toSize()
170 | val scaledSize = lerp(childSize, parentSize, zoomFactor)
171 | val scaledConstraints = Constraints(
172 | minWidth = scaledSize.width.roundToInt(),
173 | minHeight = scaledSize.height.roundToInt(),
174 | maxWidth = scaledSize.width.roundToInt(),
175 | maxHeight = scaledSize.height.roundToInt()
176 | )
177 | val placeable = measurable.measure(scaledConstraints)
178 | layout(
179 | constraints.constrainWidth(placeable.width),
180 | constraints.constrainHeight(placeable.height)
181 | ) {
182 | val coords = viewportCoordinates!!
183 | val childCoords = activeChild?.placeholderCoordinates?.takeIf { it.isAttached }
184 | // If the activeChild is null, that means the animation finished _just_ before this
185 | // placement pass – e.g. the user could have been scrolling the content while the
186 | // animation was still running.
187 | val offset = if (childCoords == null || !isContentBeingScaled) {
188 | IntOffset.Zero
189 | } else {
190 | val placeholderBounds =
191 | coords.localBoundingBoxOf(childCoords, clipBounds = false)
192 | placeholderBounds.topLeft.round()
193 | }
194 | placeable.place(offset)
195 | }
196 | }
197 |
198 | @OptIn(ExperimentalFoundationApi::class)
199 | @Composable
200 | override fun FractalNavChild(
201 | key: String,
202 | modifier: Modifier,
203 | content: @Composable FractalNavChildScope.() -> Unit
204 | ) {
205 | key(this, key) {
206 | // TODO This check fails inside LazyVerticalGrid. It's probably too strict anyway.
207 | // check(composeContent) {
208 | // "FractalNavHost content shouldn't be composed when composeContent is false."
209 | // }
210 |
211 | val child = remember {
212 | if (activeChild?.key == key) {
213 | // If there's an active child on the first composition, that means that we're
214 | // starting a zoom-out and should move the child into the content composition
215 | // here by re-using the child's state.
216 | activeChild!!
217 | } else {
218 | FractalChild(key, parent = this)
219 | }
220 | }
221 |
222 | // Register the child with its key so that zoomToChild can find it.
223 | DisposableEffect(child) {
224 | check(key !in children) {
225 | "FractalNavChild with key \"$key\" has already been composed."
226 | }
227 | children[key] = child
228 | onDispose {
229 | // TODO uncomment this once I figure out why the state is being re-initialized.
230 | //child.placeholderCoordinates = null
231 | children -= key
232 | }
233 | }
234 |
235 | child.setContent(content)
236 |
237 | // When the host is composing the content request the placeholder box to stay at the
238 | // size the content measured before the animation started.
239 | val placeholderSizeModifier = if (child === activeChild) {
240 | childPlaceholderSize?.let { size ->
241 | with(LocalDensity.current) {
242 | Modifier.size(DpSize(size.width.toDp(), size.height.toDp()))
243 | }
244 | } ?: Modifier
245 | } else Modifier
246 |
247 | Box(
248 | modifier = modifier
249 | .onPlaced { child.placeholderCoordinates = it }
250 | .workaroundBoxOnPlacedBug()
251 | .bringIntoViewRequester(child.bringIntoViewRequester)
252 | .then(placeholderSizeModifier),
253 | propagateMinConstraints = true
254 | ) {
255 | // The active child is always composed directly by the host, so when that happens
256 | // remove it from the composition here so the movable content is moved and not
257 | // re-instantiated.
258 | if (child !== activeChild) {
259 | child.MovableContent()
260 | }
261 | }
262 | }
263 | }
264 |
265 | @OptIn(ExperimentalFoundationApi::class)
266 | override fun zoomToChild(key: String) {
267 | // Can't zoom into a child while a different child is currently active.
268 | // However, if we're currently zooming out of a child, and that same child requested to
269 | // be zoomed in again, we can cancel the zoom out and start zooming in immediately.
270 | if (activeChild != null &&
271 | (activeChild?.key != key || zoomDirection != ZoomingOut)
272 | ) {
273 | return
274 | }
275 |
276 | val requestedChild = children.getOrElse(key) {
277 | throw IllegalArgumentException("No child with key \"$key\".")
278 | }
279 | activeChild = requestedChild
280 |
281 | // Capture the size before starting the animation since the child's layout node will be
282 | // removed from wherever it is but we need the placeholder to preserve that space.
283 | // This could probably be removed once speculative layout exists.
284 | childPlaceholderSize = activeChild?.placeholderCoordinates?.takeIf { it.isAttached }?.size
285 | zoomDirection = ZoomDirection.ZoomingIn
286 |
287 | coroutineScope.launch {
288 | try {
289 | // Try to make the child fully visible before starting zoom so the scale animation
290 | // doesn't end up scaling a clipped view.
291 | requestedChild.bringIntoViewRequester.bringIntoView()
292 | zoomFactorAnimatable.animateTo(1f, zoomAnimationSpecFactory())
293 | } finally {
294 | // If we weren't cancelled by an opposing animation, jump to the end state.
295 | if (zoomDirection == ZoomDirection.ZoomingIn) {
296 | zoomDirection = null
297 | // Do this last since it can suspend and we want to make sure the other states are
298 | // updated asap.
299 | zoomFactorAnimatable.snapTo(1f)
300 | }
301 | }
302 | }
303 | }
304 |
305 | override fun zoomOut() {
306 | // If already zooming or zoomed out, do nothing.
307 | if (activeChild == null || zoomDirection == ZoomingOut) return
308 |
309 | zoomDirection = ZoomingOut
310 | coroutineScope.launch {
311 | // Composing the parent for the first time can take some time. Wait a frame before
312 | // animating to give it a chance to settle.
313 | withFrameMillis {}
314 | try {
315 | zoomFactorAnimatable.animateTo(0f, zoomAnimationSpecFactory())
316 | } finally {
317 | // If we weren't cancelled by an opposing animation, jump to the end state.
318 | if (zoomDirection == ZoomingOut) {
319 | activeChild = null
320 | zoomDirection = null
321 | // Do this last since it can suspend and we want to make sure the other states
322 | // are updated asap.
323 | zoomFactorAnimatable.snapTo(0f)
324 | }
325 | }
326 | }
327 | }
328 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/Lerp.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import kotlin.math.roundToInt
4 |
5 | fun lerp(i1: Int, i2: Int, fraction: Float): Int {
6 | return (i1 + (i2 - i1) * fraction).roundToInt()
7 | }
8 |
9 | fun lerp(f1: Float, f2: Float, fraction: Float): Float {
10 | return f1 + (f2 - f1) * fraction
11 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/ScaleConstraintsModifier.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.geometry.Offset
5 | import androidx.compose.ui.graphics.drawscope.scale
6 | import androidx.compose.ui.layout.layout
7 | import androidx.compose.ui.unit.IntOffset
8 | import androidx.compose.ui.unit.round
9 | import kotlin.math.roundToInt
10 |
11 | /**
12 | * Insets and centers a layout by a [factor] of its measured size.
13 | * Does not actually scale the layer, so the modified element may draw outside its bounds.
14 | * Works around the [scale] modifier not being used correctly in calculations.
15 | */
16 | internal fun Modifier.scaleLayout(factor: () -> Float): Modifier = layout { m, c ->
17 | @Suppress("NAME_SHADOWING")
18 | val scale = factor()
19 | if (scale == 0f) {
20 | // Don't measure or place if it won't take any space anyway.
21 | return@layout layout(0, 0) {}
22 | }
23 | val p = m.measure(c)
24 | val width = p.width * scale
25 | val height = p.height * scale
26 | layout(width.roundToInt(), height.roundToInt()) {
27 | val center = Offset(width, height) / 2f
28 | val pCenter = IntOffset(p.width, p.height) / 2f
29 | p.place(center.round() - pCenter)
30 | }
31 | }
--------------------------------------------------------------------------------
/fractalnav/src/main/java/com/zachklipp/fractalnav/WorkaroundBoxOnPlacedBugModifier.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.fractalnav
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.layout.LayoutModifier
5 | import androidx.compose.ui.layout.Measurable
6 | import androidx.compose.ui.layout.MeasureResult
7 | import androidx.compose.ui.layout.MeasureScope
8 | import androidx.compose.ui.unit.Constraints
9 | import androidx.compose.ui.unit.IntOffset
10 |
11 | /**
12 | * Add this modifier after `onPlaced` when applied to a Box. Without it, the onPlaced will never
13 | * get called. Works by simply adding a no-op layout node wrapper between the onPlaced and the
14 | * Box. See b/228128961.
15 | */
16 | internal fun Modifier.workaroundBoxOnPlacedBug(): Modifier =
17 | this.then(WorkaroundBoxOnPlacedBugModifier)
18 |
19 | private object WorkaroundBoxOnPlacedBugModifier : LayoutModifier {
20 | override fun MeasureScope.measure(
21 | measurable: Measurable,
22 | constraints: Constraints
23 | ): MeasureResult = with(measurable.measure(constraints)) {
24 | layout(width, height) {
25 | place(IntOffset.Zero)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/galaxyapp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/galaxyapp/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.zachklipp.galaxyapp'
8 | compileSdk 32
9 |
10 | defaultConfig {
11 | applicationId "com.zachklipp.galaxyapp"
12 | minSdk 21
13 | targetSdk 32
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | }
36 | buildFeatures {
37 | compose true
38 | }
39 | composeOptions {
40 | kotlinCompilerExtensionVersion '1.1.1'
41 | }
42 | packagingOptions {
43 | resources {
44 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
45 | }
46 | }
47 | }
48 |
49 | dependencies {
50 | implementation project(':fractalnav')
51 | implementation 'io.coil-kt:coil-compose:2.0.0-rc02'
52 | implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.0'
53 | implementation 'androidx.core:core-ktx:1.7.0'
54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
55 | implementation 'androidx.activity:activity-compose:1.4.0'
56 | implementation "androidx.compose.ui:ui:$compose_ui_version"
57 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
58 | implementation "androidx.compose.material:material:$compose_ui_version"
59 | testImplementation 'junit:junit:4.13.2'
60 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
62 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
63 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
64 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
65 | }
--------------------------------------------------------------------------------
/galaxyapp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/galaxyapp/src/androidTest/java/com/zachklipp/galaxyapp/BoxOnPlacedRepro.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.layout.LayoutCoordinates
6 | import androidx.compose.ui.layout.layout
7 | import androidx.compose.ui.layout.onPlaced
8 | import androidx.compose.ui.test.junit4.createComposeRule
9 | import androidx.compose.ui.unit.IntOffset
10 | import androidx.test.ext.junit.runners.AndroidJUnit4
11 | import org.junit.Assert.assertNotNull
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 |
16 | /**
17 | * See b/228128961.
18 | */
19 | @RunWith(AndroidJUnit4::class)
20 | class BoxOnPlacedRepro {
21 |
22 | @get:Rule
23 | val rule = createComposeRule()
24 |
25 | @Test
26 | fun onBoxPlaced_failing() {
27 | var coordinates: LayoutCoordinates? = null
28 | rule.setContent {
29 | Box(Modifier.onPlaced { coordinates = it })
30 | }
31 | rule.runOnIdle {
32 | assertNotNull(coordinates)
33 | }
34 | }
35 |
36 | @Test
37 | fun onBoxPlaced_passing() {
38 | var coordinates: LayoutCoordinates? = null
39 | rule.setContent {
40 | Box(Modifier
41 | .onPlaced { coordinates = it }
42 | .layout { m, c ->
43 | with(m.measure(c)) {
44 | layout(width, height) {
45 | place(IntOffset.Zero)
46 | }
47 | }
48 | }
49 | )
50 | }
51 | rule.runOnIdle {
52 | assertNotNull(coordinates)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/App.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.Surface
6 | import androidx.compose.material.darkColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import com.zachklipp.fractalnav.FractalNavHost
12 | import com.zachklipp.fractalnav.FractalNavState
13 |
14 | @Composable
15 | @Preview
16 | fun App(navState: FractalNavState = remember { FractalNavState() }) {
17 | val universeInfo = remember { UniverseInfo() }
18 | MaterialTheme(colors = darkColors()) {
19 | Surface {
20 | FractalNavHost(
21 | state = navState,
22 | modifier = Modifier.fillMaxSize()
23 | ) {
24 | Universe(universeInfo)
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/BackButton.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.material.Icon
5 | import androidx.compose.material.IconButton
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.ArrowBack
8 | import androidx.compose.runtime.Composable
9 | import com.zachklipp.fractalnav.FractalNavChildScope
10 | import com.zachklipp.fractalnav.ZoomDirection
11 |
12 | @Composable
13 | fun FractalNavChildScope.BackButton() {
14 | if (isFullyZoomedIn || zoomDirection == ZoomDirection.ZoomingIn) {
15 | BackHandler {
16 | zoomToParent()
17 | }
18 | }
19 |
20 | IconButton(onClick = { zoomToParent() }) {
21 | Icon(Icons.Default.ArrowBack, contentDescription = "Go back")
22 | }
23 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/Components.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.layout.Arrangement.spacedBy
5 | import androidx.compose.foundation.lazy.grid.GridCells
6 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
7 | import androidx.compose.foundation.lazy.grid.items
8 | import androidx.compose.material.MaterialTheme
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.key
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun ListHeader(text: String) {
17 | Text(
18 | text,
19 | Modifier
20 | .padding(8.dp)
21 | .fillMaxWidth()
22 | .wrapContentWidth()
23 | )
24 | }
25 |
26 | @Composable
27 | fun SpaceList(
28 | items: List,
29 | modifier: Modifier = Modifier,
30 | content: @Composable (T) -> Unit
31 | ) {
32 | Column(
33 | modifier = modifier
34 | .padding(8.dp)
35 | .fillMaxSize(),
36 | verticalArrangement = Arrangement.Absolute.spacedBy(8.dp)
37 | ) {
38 | items.forEach { item ->
39 | key(item) {
40 | content(item)
41 | }
42 | }
43 | }
44 | }
45 |
46 | // Note: Seems to crash when zooming out to items on the left side.
47 | @Composable
48 | fun SpaceGrid(
49 | items: List,
50 | modifier: Modifier = Modifier,
51 | content: @Composable (T) -> Unit
52 | ) {
53 | LazyVerticalGrid(
54 | columns = GridCells.Fixed(2),
55 | verticalArrangement = spacedBy(8.dp),
56 | horizontalArrangement = spacedBy(8.dp),
57 | modifier = modifier
58 | .padding(8.dp)
59 | .fillMaxSize(),
60 | ) {
61 | items(items) { item ->
62 | content(item)
63 | }
64 | }
65 | }
66 |
67 | @Composable
68 | fun InfoText(text: String, modifier: Modifier = Modifier) {
69 | Text(
70 | text.lineSequence().joinToString(separator = " "),
71 | style = MaterialTheme.typography.body2,
72 | modifier = modifier.padding(horizontal = 8.dp)
73 | )
74 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/GalaxyItem.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
10 | import androidx.compose.foundation.relocation.BringIntoViewRequester
11 | import androidx.compose.foundation.relocation.bringIntoViewRequester
12 | import androidx.compose.foundation.rememberScrollState
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.foundation.verticalScroll
15 | import androidx.compose.material.Card
16 | import androidx.compose.material.MaterialTheme
17 | import androidx.compose.material.Text
18 | import androidx.compose.runtime.*
19 | import androidx.compose.ui.Alignment.Companion.Center
20 | import androidx.compose.ui.Alignment.Companion.CenterVertically
21 | import androidx.compose.ui.Alignment.Companion.TopCenter
22 | import androidx.compose.ui.Alignment.Companion.TopStart
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.graphics.BlendMode
25 | import androidx.compose.ui.graphics.Shadow
26 | import androidx.compose.ui.graphics.graphicsLayer
27 | import androidx.compose.ui.text.style.TextAlign
28 | import androidx.compose.ui.unit.dp
29 | import com.zachklipp.fractalnav.FractalNavChildScope
30 | import com.zachklipp.fractalnav.FractalNavScope
31 | import com.zachklipp.fractalnav.ZoomDirection.ZoomingIn
32 | import com.zachklipp.fractalnav.ZoomDirection.ZoomingOut
33 | import com.zachklipp.fractalnav.lerp
34 |
35 | private val Galaxy.fractalKey get() = "galaxy-$name"
36 |
37 | @Composable
38 | fun FractalNavScope.GalaxyItem(
39 | galaxy: Galaxy,
40 | universeInfo: UniverseInfo,
41 | modifier: Modifier = Modifier
42 | ) {
43 | Card(
44 | modifier.clickable { zoomToChild(galaxy.fractalKey) }
45 | ) {
46 | Row(
47 | modifier = Modifier.padding(8.dp),
48 | horizontalArrangement = spacedBy(8.dp),
49 | verticalAlignment = CenterVertically
50 | ) {
51 | FractalNavChild(
52 | galaxy.fractalKey,
53 | Modifier
54 | .size(64.dp)
55 | .wrapContentSize()
56 | ) {
57 | GalaxyChild(galaxy, universeInfo)
58 | }
59 | Text(galaxy.name)
60 | }
61 | }
62 | }
63 |
64 | @OptIn(ExperimentalFoundationApi::class)
65 | @Composable
66 | private fun FractalNavChildScope.GalaxyChild(galaxy: Galaxy, universeInfo: UniverseInfo) {
67 | val scrollState = rememberScrollState()
68 | val bringHeroIntoViewRequester = remember { BringIntoViewRequester() }
69 |
70 | // When zooming out, scroll back to the top, animating in coordination with the zoom.
71 | if (zoomDirection == ZoomingOut) {
72 | LaunchedEffect(scrollState) {
73 | val amountToScroll = scrollState.value
74 | snapshotFlow { zoomFactor }
75 | .collect {
76 | scrollState.scrollTo(lerp(0, amountToScroll, it))
77 | }
78 | }
79 | }
80 |
81 | Column(
82 | verticalArrangement = spacedBy(8.dp),
83 | modifier = if (isActive) Modifier.verticalScroll(scrollState) else Modifier
84 | ) {
85 | GalaxyHero(
86 | galaxy,
87 | showControls = isFullyZoomedIn || zoomDirection == ZoomingIn,
88 | modifier = if (isActive) {
89 | Modifier.bringIntoViewRequester(bringHeroIntoViewRequester)
90 | } else Modifier
91 | )
92 |
93 | if (isActive) {
94 | val stars = remember(galaxy, universeInfo) { universeInfo.getStars(galaxy) }
95 | .collectAsState().value ?: emptyList()
96 |
97 | InfoText(
98 | text = galaxy.description,
99 | modifier = Modifier
100 | .fillExpandedWidth()
101 | .alphaByZoomFactor()
102 | )
103 |
104 | if (stars.isNotEmpty()) {
105 | ListHeader("Stars")
106 | SpaceList(stars) { star ->
107 | StarItem(star, universeInfo, Modifier.fillMaxWidth())
108 | }
109 | }
110 | }
111 | }
112 | }
113 |
114 | @Composable
115 | private fun FractalNavChildScope.GalaxyHero(
116 | galaxy: Galaxy,
117 | showControls: Boolean,
118 | modifier: Modifier = Modifier
119 | ) {
120 | Box(modifier) {
121 | GalaxyImage(
122 | galaxy, Modifier
123 | .fillMaxWidth()
124 | .graphicsLayer {
125 | clip = true
126 | shape = RoundedCornerShape(10.dp * (1f - zoomFactor))
127 | }
128 | )
129 | AnimatedVisibility(showControls, Modifier.align(TopStart)) {
130 | BackButton()
131 | }
132 | AnimatedVisibility(
133 | showControls,
134 | modifier = Modifier.align(Center),
135 | enter = fadeIn(),
136 | exit = fadeOut()
137 | ) {
138 | Text(
139 | "The ${galaxy.name} Galaxy",
140 | style = MaterialTheme.typography.h4
141 | .copy(
142 | shadow = Shadow(blurRadius = 5f),
143 | textAlign = TextAlign.Center,
144 | ),
145 | modifier = Modifier
146 | .alphaByZoomFactor()
147 | .scaleByZoomFactor()
148 | .fillExpandedWidth()
149 | .padding(8.dp)
150 | )
151 | }
152 | }
153 | }
154 |
155 | @Composable
156 | private fun GalaxyImage(galaxy: Galaxy, modifier: Modifier) {
157 | NetworkImage(
158 | url = galaxy.imageUrl,
159 | contentDescription = "Image of ${galaxy.name}",
160 | modifier = modifier,
161 | blendMode = BlendMode.Screen,
162 | alignment = TopCenter,
163 | cacheOriginal = true,
164 | )
165 | }
166 |
167 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/ImageLoading.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.key
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.drawWithContent
9 | import androidx.compose.ui.geometry.Offset
10 | import androidx.compose.ui.geometry.Rect
11 | import androidx.compose.ui.graphics.BlendMode
12 | import androidx.compose.ui.graphics.ColorFilter
13 | import androidx.compose.ui.graphics.Paint
14 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
15 | import androidx.compose.ui.graphics.withSaveLayer
16 | import androidx.compose.ui.layout.ContentScale
17 | import androidx.compose.ui.platform.LocalContext
18 | import coil.compose.AsyncImage
19 | import coil.imageLoader
20 | import coil.request.ImageRequest
21 | import coil.size.Dimension
22 |
23 | /**
24 | * Thin wrapper around external image loader library.
25 | *
26 | * @param key An arbitrary value that will cause the image request to be re-started when it changes
27 | * between compositions. Use to workaround the Coil bug where the image is initially loaded into a
28 | * very small space and then that space grows.
29 | */
30 | @Composable
31 | fun NetworkImage(
32 | url: String,
33 | contentDescription: String?,
34 | modifier: Modifier = Modifier,
35 | contentScale: ContentScale = ContentScale.FillWidth,
36 | alignment: Alignment = Alignment.Center,
37 | blendMode: BlendMode? = null,
38 | colorFilter: ColorFilter? = null,
39 | cacheOriginal: Boolean = false,
40 | ) {
41 | if (cacheOriginal) {
42 | // Warm the cache with the image as large to workaround a Coil bug that doesn't reload the
43 | // image at a higher resolution after loading it for a very small size.
44 | val context = LocalContext.current
45 | LaunchedEffect(context) {
46 | context.imageLoader.execute(
47 | ImageRequest.Builder(context)
48 | .data(url)
49 | .size(Dimension.Original, Dimension.Original)
50 | .build()
51 | )
52 | }
53 | }
54 |
55 | AsyncImage(
56 | model = ImageRequest.Builder(LocalContext.current)
57 | .data(url)
58 | .build(),
59 | contentDescription = contentDescription,
60 | contentScale = contentScale,
61 | colorFilter = colorFilter,
62 | alignment = alignment,
63 | modifier = modifier.then(blendMode?.let(Modifier::blendMode) ?: Modifier),
64 | )
65 | }
66 |
67 | fun Modifier.blendMode(mode: BlendMode): Modifier = drawWithContent {
68 | drawIntoCanvas {
69 | it.withSaveLayer(
70 | bounds = Rect(Offset.Zero, size),
71 | paint = Paint().apply {
72 | blendMode = mode
73 | }
74 | ) {
75 | drawContent()
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import com.zachklipp.fractalnav.FractalNavState
7 | import com.zachklipp.galaxyapp.ui.theme.FractalnavTheme
8 |
9 | class MainActivity : ComponentActivity() {
10 |
11 | private var navState: FractalNavState? = null
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 |
16 | @Suppress("DEPRECATION")
17 | val navState = navState ?: run {
18 | (lastCustomNonConfigurationInstance as? FractalNavState) ?: FractalNavState()
19 | }.also { navState = it }
20 |
21 | setContent {
22 | FractalnavTheme {
23 | // A surface container using the 'background' color from the theme
24 | App(navState)
25 | }
26 | }
27 | }
28 |
29 | @Suppress("OVERRIDE_DEPRECATION")
30 | override fun onRetainCustomNonConfigurationInstance(): Any? = navState
31 | }
32 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/PlanetItem.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.layout.Arrangement.spacedBy
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.drawWithContent
17 | import androidx.compose.ui.graphics.*
18 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.unit.dp
21 | import com.zachklipp.fractalnav.FractalNavChildScope
22 | import com.zachklipp.fractalnav.ZoomDirection
23 |
24 | @Composable
25 | fun FractalNavChildScope.PlanetItem(
26 | planet: Planet,
27 | modifier: Modifier = Modifier
28 | ) {
29 | Column(
30 | modifier = modifier,
31 | verticalArrangement = spacedBy(8.dp)
32 | ) {
33 | PlanetHero(
34 | planet,
35 | showControls = isFullyZoomedIn || zoomDirection == ZoomDirection.ZoomingIn
36 | )
37 |
38 | if (isActive) {
39 | InfoText(
40 | text = planet.description,
41 | modifier = Modifier
42 | .fillExpandedWidth()
43 | .alphaByZoomFactor()
44 | )
45 | }
46 | }
47 | }
48 |
49 | @Composable
50 | private fun FractalNavChildScope.PlanetHero(
51 | planet: Planet,
52 | showControls: Boolean,
53 | modifier: Modifier = Modifier
54 | ) {
55 | Box(
56 | modifier
57 | .fillMaxWidth()
58 | .aspectRatio(1f),
59 | ) {
60 | // Spin the planet when zoomed out, but slow it way down when zoomed in.
61 | val rotationAngle by animateRotation(3_000, scale = { 1f - zoomFactor * 0.9f })
62 | PlanetImage(
63 | planet = planet,
64 | // Darken the image a bit when zoomed in so the white text shows up better.
65 | tint = zoomFactor * 0.5f,
66 | modifier = modifier
67 | .fillMaxSize()
68 | .graphicsLayer { rotationZ = rotationAngle }
69 | // Since the images are drawn with the Screen blendmode, they're
70 | // translucent. We need an opaque background to block out the orbit
71 | // line.
72 | .background(MaterialTheme.colors.background, CircleShape)
73 | .drawWithContent {
74 | drawIntoCanvas { }
75 | drawContent()
76 | }
77 | )
78 | AnimatedVisibility(showControls, Modifier.align(Alignment.TopStart)) {
79 | BackButton()
80 | }
81 | AnimatedVisibility(
82 | showControls,
83 | modifier = Modifier.align(Alignment.Center),
84 | enter = fadeIn(),
85 | exit = fadeOut()
86 | ) {
87 | Text(
88 | planet.name,
89 | style = MaterialTheme.typography.h4
90 | .copy(
91 | shadow = Shadow(blurRadius = 5f),
92 | textAlign = TextAlign.Center,
93 | ),
94 | modifier = Modifier
95 | .alphaByZoomFactor()
96 | .scaleByZoomFactor()
97 | .fillExpandedWidth()
98 | .padding(8.dp)
99 | )
100 | }
101 | }
102 | }
103 |
104 | @Composable
105 | private fun PlanetImage(
106 | planet: Planet,
107 | modifier: Modifier,
108 | tint: Float
109 | ) {
110 | NetworkImage(
111 | url = planet.imageUrl,
112 | contentDescription = "Image of ${planet.name}",
113 | modifier = modifier,
114 | blendMode = BlendMode.Screen,
115 | colorFilter = ColorFilter.tint(Color.Black.copy(alpha = tint), BlendMode.Darken),
116 | // The planets start off being measured only a few pixels, so Coil will cache that scaled-
117 | // down image and never refresh it even when the size grows unless we explicitly cache the
118 | // original size.
119 | cacheOriginal = true
120 | )
121 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/PlanetarySystem.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.animation.core.*
4 | import androidx.compose.foundation.gestures.awaitDragOrCancellation
5 | import androidx.compose.foundation.gestures.awaitFirstDown
6 | import androidx.compose.foundation.gestures.forEachGesture
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.runtime.*
9 | import androidx.compose.runtime.saveable.rememberSaveable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.drawBehind
12 | import androidx.compose.ui.draw.drawWithCache
13 | import androidx.compose.ui.geometry.Offset
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.drawscope.Stroke
16 | import androidx.compose.ui.graphics.drawscope.scale
17 | import androidx.compose.ui.graphics.graphicsLayer
18 | import androidx.compose.ui.input.pointer.PointerInputChange
19 | import androidx.compose.ui.input.pointer.consumePositionChange
20 | import androidx.compose.ui.input.pointer.pointerInput
21 | import androidx.compose.ui.layout.layout
22 | import androidx.compose.ui.unit.Constraints
23 | import androidx.compose.ui.unit.IntOffset
24 | import kotlin.math.roundToInt
25 |
26 | /**
27 | * An animated, scientifically-inaccurate model of a star and its planets.
28 | *
29 | * @param orbitScale A function that returns a value between 0 and 1 that controls how much the
30 | * planet orbits are scaled around the center. 0 means all planets will be pinned to the center.
31 | * @param orbitAnimationScale A function that returns a value between 0 and 1 that controls the
32 | * speed of the orbits.
33 | */
34 | @Composable
35 | fun PlanetarySystem(
36 | star: @Composable () -> Unit,
37 | planets: List
,
38 | modifier: Modifier = Modifier,
39 | orbitScale: () -> Float = { 1f },
40 | orbitAnimationScale: () -> Float = { 1f },
41 | onPlanetTouched: ((Int) -> Unit)? = null,
42 | onPlanetSelected: ((Int) -> Unit)? = null,
43 | planetContent: @Composable (P) -> Unit
44 | ) {
45 | val planetRadii = remember { mutableStateListOf() }
46 | var selectedPlanet by remember { mutableStateOf(-1) }
47 |
48 | // Keep the size of the list of radii in sync with the actual planets list so when the
49 | // onRadiusMeasured callback fires it's always exactly the right size.
50 | SideEffect {
51 | if (planetRadii.size > planets.size) {
52 | planetRadii.removeRange(planets.size, planetRadii.size)
53 | } else if (planets.size > planetRadii.size) {
54 | for (i in planetRadii.size until planets.size) {
55 | planetRadii.add(0f)
56 | }
57 | }
58 | }
59 |
60 | val drawOrbitsModifier = Modifier.drawBehind {
61 | planetRadii.forEach { orbitRadius ->
62 | drawCircle(
63 | Color.Gray,
64 | radius = orbitRadius,
65 | style = Stroke(),
66 | alpha = 0.5f * orbitScale()
67 | )
68 |
69 | if (selectedPlanet in planetRadii.indices) {
70 | drawCircle(
71 | Color.LightGray,
72 | radius = planetRadii[selectedPlanet],
73 | alpha = 0.5f
74 | )
75 | }
76 | }
77 | }
78 |
79 | val inputModifier = if (onPlanetSelected != null && planets.isNotEmpty()) {
80 | val updatedOnPlanetTouched by rememberUpdatedState(onPlanetTouched)
81 | val updatedOnPlanetSelected by rememberUpdatedState(onPlanetSelected)
82 | Modifier.pointerInput(planets) {
83 | forEachGesture {
84 | awaitPointerEventScope {
85 | var change: PointerInputChange? = awaitFirstDown()
86 | while (change != null && change.pressed) {
87 | val totalRadius = size.width / 2f
88 | val radius = (change.position - Offset(
89 | totalRadius,
90 | totalRadius
91 | )).getDistance() / totalRadius
92 | selectedPlanet = if (radius <= 0.3f || radius > 1f) {
93 | -1
94 | } else {
95 | (planets.size * (radius - 0.3f) / .7f / orbitScale()).toInt()
96 | }
97 | updatedOnPlanetTouched?.invoke(selectedPlanet)
98 | change.consumePositionChange()
99 | change = awaitDragOrCancellation(change.id)
100 | }
101 |
102 | // Pointer was either raised or cancelled.
103 | if (change != null && selectedPlanet != -1) {
104 | updatedOnPlanetSelected(selectedPlanet)
105 | }
106 | selectedPlanet = -1
107 | }
108 | }
109 | }
110 | } else Modifier
111 |
112 | RadialLayout(
113 | modifier
114 | .then(drawOrbitsModifier)
115 | .then(inputModifier)
116 | ) {
117 | val transition = rememberInfiniteTransition()
118 |
119 | val starAngle by animateRotation(20_000)
120 | val starTwinkleScale by transition.animateFloat(
121 | initialValue = 0.95f,
122 | targetValue = 1.05f,
123 | animationSpec = infiniteRepeatable(
124 | tween(500),
125 | repeatMode = RepeatMode.Reverse
126 | )
127 | )
128 | Box(
129 | Modifier
130 | // Make the star twice as big as the planets.
131 | .weight { 2f }
132 | .centerOffsetPercent { 0f }
133 | .scaleConstraints { 0.5f + 0.5f * (1f - orbitScale()) }
134 | .graphicsLayer {
135 | rotationZ = -starAngle
136 | }
137 | .drawWithCache {
138 | onDrawWithContent {
139 | drawContent()
140 | // Layer the star so that it looks glowy.
141 | scale(starTwinkleScale) {
142 | this@onDrawWithContent.drawContent()
143 | }
144 | }
145 | },
146 | propagateMinConstraints = true
147 | ) {
148 | star()
149 | }
150 |
151 | planets.forEachIndexed { i, planet ->
152 | val orbitAngle by animateRotation(
153 | duration = 2_000 * (i + 1),
154 | scale = orbitAnimationScale
155 | )
156 | Box(
157 | Modifier
158 | .weight(orbitScale)
159 | .centerOffsetPercent {
160 | 0.3f + 0.7f * (i / planets.size.toFloat() * orbitScale())
161 | }
162 | .onRadiusMeasured {
163 | planetRadii[i] = it
164 | }
165 | .angleDegrees { -orbitAngle }
166 | .scaleConstraints { 0.5f }
167 | ) {
168 | planetContent(planet)
169 | }
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * Insets and centers a layout by a [factor] of its constraints.
176 | * Works around the [scale] modifier not being used correctly in calculations.
177 | */
178 | private fun Modifier.scaleConstraints(factor: () -> Float): Modifier = layout { m, c ->
179 | @Suppress("NAME_SHADOWING")
180 | val scale = factor()
181 | if (scale == 0f) {
182 | // Don't measure or place if it won't take any space anyway.
183 | return@layout layout(0, 0) {}
184 | }
185 |
186 | val constraints = Constraints(
187 | minWidth = (c.minWidth * scale).roundToInt(),
188 | minHeight = (c.minHeight * scale).roundToInt(),
189 | maxWidth = (c.maxWidth * scale).roundToInt(),
190 | maxHeight = (c.maxHeight * scale).roundToInt()
191 | )
192 | val p = m.measure(constraints)
193 | layout(c.maxWidth, c.maxHeight) {
194 | val center = IntOffset(c.maxWidth, c.maxHeight) / 2f
195 | val pCenter = IntOffset(p.width, p.height) / 2f
196 | p.place(center - pCenter)
197 | }
198 | }
199 |
200 | /**
201 | * Returns a value that will animate continuously between 0 and 360 in [duration] millis * [scale].
202 | * The rotation angle is saved in the instance state.
203 | */
204 | @Composable
205 | fun animateRotation(duration: Int, scale: () -> Float = { 1f }): State {
206 | val angle = rememberSaveable { mutableStateOf(0f) }
207 | val running by remember { derivedStateOf { scale() != 0f } }
208 | if (running) {
209 | LaunchedEffect(duration) {
210 | var previousTime: Long = AnimationConstants.UnspecifiedTime
211 | val degreesPerMilli = 360f / duration
212 | while (true) {
213 | withInfiniteAnimationFrameMillis { frame ->
214 | if (previousTime == AnimationConstants.UnspecifiedTime) previousTime = frame
215 | val angleChange =
216 | angle.value + degreesPerMilli * (frame - previousTime) * scale()
217 | angle.value = angleChange.mod(360f)
218 | previousTime = frame
219 | }
220 | }
221 | }
222 | }
223 | return angle
224 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/RadialLayout.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.layout.Layout
7 | import androidx.compose.ui.layout.ParentDataModifier
8 | import androidx.compose.ui.unit.Constraints
9 | import androidx.compose.ui.unit.Density
10 | import androidx.compose.ui.unit.round
11 | import kotlin.math.PI
12 | import kotlin.math.cos
13 | import kotlin.math.roundToInt
14 | import kotlin.math.sin
15 |
16 | private const val HalfPI = PI.toFloat() / 180f
17 |
18 | object RadialLayoutScope {
19 | /**
20 | * Specifies the offset in pixels from the center of a [RadialLayout] to place the center of
21 | * this element.
22 | */
23 | fun Modifier.centerOffsetPercent(radiusPercent: () -> Float): Modifier = radialParentData {
24 | it.centerOffsetPercent = radiusPercent
25 | }
26 |
27 | /**
28 | * Specifies the angle to place this element around the center of a [RadialLayout].
29 | */
30 | fun Modifier.angleDegrees(angle: () -> Float): Modifier = radialParentData {
31 | it.angleDegrees = angle
32 | }
33 |
34 | /**
35 | * Specifies the weight to measure this element relative to the other children of this
36 | * [RadialLayout].
37 | */
38 | fun Modifier.weight(weight: () -> Float): Modifier = radialParentData {
39 | it.weight = weight
40 | }
41 |
42 | /**
43 | * Register a callback to receive the element's distance from the center when measured.
44 | */
45 | fun Modifier.onRadiusMeasured(block: (Float) -> Unit): Modifier = radialParentData { it ->
46 | val previousCallback = it.onRadiusMeasured
47 | it.onRadiusMeasured = { radius ->
48 | previousCallback?.invoke(radius)
49 | block(radius)
50 | }
51 | }
52 |
53 | private fun Modifier.radialParentData(block: (RadialLayoutParentData) -> Unit) =
54 | this.then(object : ParentDataModifier {
55 | override fun Density.modifyParentData(parentData: Any?) =
56 | ((parentData as? RadialLayoutParentData) ?: RadialLayoutParentData()).also {
57 | block(it)
58 | }
59 | })
60 | }
61 |
62 | private data class RadialLayoutParentData(
63 | var centerOffsetPercent: () -> Float = { 0f },
64 | var angleDegrees: () -> Float = { 0f },
65 | var weight: (() -> Float)? = null,
66 | var onRadiusMeasured: ((Float) -> Unit)? = null
67 | )
68 |
69 | /**
70 | * Lays children out in a circle around the center of the layout.
71 | */
72 | @Composable
73 | fun RadialLayout(
74 | modifier: Modifier = Modifier,
75 | content: @Composable RadialLayoutScope.() -> Unit
76 | ) {
77 | Layout(
78 | modifier = modifier,
79 | content = {
80 | RadialLayoutScope.content()
81 | },
82 | ) { measurables, constraints ->
83 | val minDimension = minOf(constraints.maxWidth, constraints.maxHeight)
84 | val maxRadius = minDimension / 2f
85 | val center = Offset(maxRadius, maxRadius)
86 | var totalWeight = 0f
87 |
88 | // Only consider nodes with parent data.
89 | val (weighted, unweighted) = measurables
90 | .mapNotNull {
91 | val layoutData =
92 | (it.parentData as? RadialLayoutParentData) ?: return@mapNotNull null
93 | totalWeight += layoutData.weight?.invoke() ?: 0f
94 | Pair(it, layoutData)
95 | }
96 | .partition { (_, data) -> data.weight.let { it != null && !it.invoke().isNaN() } }
97 |
98 | // TODO measure unweighted placeables.
99 | val weightedPlaceables = weighted.map { (measurable, data) ->
100 | val size: Int = if (totalWeight == 0f) {
101 | minDimension
102 | } else {
103 | (minDimension * (data.weight!!() / totalWeight)).roundToInt()
104 | }
105 | val weightedConstraints = Constraints.fixed(size, size)
106 | Pair(measurable.measure(weightedConstraints), data)
107 | }
108 |
109 | layout(minDimension, minDimension) {
110 | weightedPlaceables.forEachIndexed { i, (placeable, data) ->
111 | val offset = data.centerOffsetPercent()
112 | val centerRadius = maxRadius * offset
113 | data.onRadiusMeasured?.invoke(centerRadius)
114 | val angle = data.angleDegrees() * HalfPI
115 | val centerPosition = center + Offset(
116 | x = centerRadius * cos(angle),
117 | y = centerRadius * sin(angle)
118 | )
119 | val topLeftPosition =
120 | centerPosition - Offset(placeable.width / 2f, placeable.height / 2f)
121 | placeable.place(topLeftPosition.round())
122 | }
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/Sample.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.wrapContentSize
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Surface
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.darkColors
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.graphicsLayer
17 | import androidx.compose.ui.tooling.preview.Preview
18 | import com.zachklipp.fractalnav.FractalNavChildScope
19 | import com.zachklipp.fractalnav.FractalNavHost
20 | import com.zachklipp.fractalnav.FractalNavScope
21 |
22 | @Preview
23 | @Composable
24 | private fun FractalNavSample() {
25 | MaterialTheme(colors = darkColors()) {
26 | Surface {
27 | // The host should wrap the root of your app.
28 | FractalNavHost(Modifier.fillMaxSize()) {
29 | Row(Modifier.wrapContentSize()) {
30 | Text("Click ")
31 | Link("here") {
32 | Text(
33 | "42",
34 | // Scale the text in when clicked.
35 | modifier = Modifier.scaleByZoomFactor(),
36 | style = MaterialTheme.typography.h1,
37 | maxLines = 1
38 | )
39 | }
40 | Text(" to learn more.")
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | @Composable
48 | fun FractalNavScope.Link(
49 | text: String,
50 | content: @Composable FractalNavChildScope.() -> Unit
51 | ) {
52 | // This creates some content that can be zoomed into.
53 | FractalNavChild(
54 | // It's identified by a string key…
55 | key = "link",
56 | modifier = Modifier.clickable {
57 | // …which can be used to expand its content. This will animate
58 | // the content block below to take up the full screen and also
59 | // zoom the parent content, that called this composable, out of
60 | // view.
61 | zoomToChild("link")
62 | }
63 | ) {
64 | Box(contentAlignment = Alignment.Center) {
65 | Text(text, Modifier.graphicsLayer {
66 | // The zoomFactor property is available inside the FractalNavChild
67 | // block. It starts at 0, then when zoomToChild is called it will
68 | // be animated up to 1. In this case, we want this text to start
69 | // at the full size and shrink when zoomed in.
70 | alpha = 1f - zoomFactor
71 | })
72 |
73 | // The isActive flag is also provided inside the FractalNavChild
74 | // content block, and means `zoomFactor > 0` – but is backed by
75 | // a derivedStateOf so it won't invalidate more than once during
76 | // the zoom animation.
77 | if (isActive) {
78 | content()
79 | BackHandler {
80 | // This will animate zoomFactor back down to 0.
81 | zoomToParent()
82 | }
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/StarItem.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.Card
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally
9 | import androidx.compose.ui.Alignment.Companion.CenterVertically
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clipToBounds
12 | import androidx.compose.ui.graphics.BlendMode
13 | import androidx.compose.ui.unit.dp
14 | import com.zachklipp.fractalnav.FractalNavChildScope
15 | import com.zachklipp.fractalnav.FractalNavScope
16 | import kotlin.math.roundToInt
17 |
18 | private val Star.fractalKey get() = "star-$name"
19 |
20 | @Composable
21 | fun FractalNavScope.StarItem(
22 | star: Star,
23 | universeInfo: UniverseInfo,
24 | modifier: Modifier = Modifier
25 | ) {
26 | Card(
27 | modifier.clickable { zoomToChild(star.fractalKey) }
28 | ) {
29 | Row(
30 | modifier = Modifier.padding(8.dp),
31 | horizontalArrangement = Arrangement.Absolute.spacedBy(8.dp),
32 | verticalAlignment = CenterVertically
33 | ) {
34 | FractalNavChild(
35 | star.fractalKey,
36 | Modifier
37 | .size(64.dp)
38 | .wrapContentSize()
39 | .clipToBounds()
40 | ) {
41 | StarChild(star, universeInfo)
42 | }
43 | // Set maxlines to 1 to avoid wrapping when close to fully zoomed-out.
44 | Text(star.name, maxLines = 1)
45 | }
46 | }
47 | }
48 |
49 | @Composable
50 | private fun FractalNavChildScope.StarChild(star: Star, universeInfo: UniverseInfo) {
51 | val planets: List = if (isActive) {
52 | remember(star, universeInfo) { universeInfo.getPlanets(star) }
53 | .collectAsState()
54 | .value ?: emptyList()
55 | } else {
56 | emptyList()
57 | }
58 | var planetUnderFinger by remember { mutableStateOf(-1) }
59 |
60 | Column(
61 | verticalArrangement = Arrangement.SpaceBetween,
62 | horizontalAlignment = CenterHorizontally
63 | ) {
64 | if (isActive) {
65 | Row(
66 | verticalAlignment = CenterVertically,
67 | modifier = Modifier
68 | .scaleLayoutByZoomFactor()
69 | .alphaByZoomFactor()
70 | ) {
71 | BackButton()
72 | Spacer(Modifier.size(8.dp))
73 | Text("The ${star.name} System", Modifier.weight(1f), maxLines = 1)
74 | }
75 | Text(
76 | if (planetUnderFinger in planets.indices) {
77 | "${planets[planetUnderFinger].name}, release to open."
78 | } else {
79 | "Tap or drag on the planets."
80 | },
81 | maxLines = 1,
82 | modifier = Modifier
83 | .scaleLayoutByZoomFactor()
84 | .alphaByZoomFactor()
85 | )
86 | }
87 |
88 | PlanetarySystem(
89 | star = { StarImage(star) },
90 | planets = planets,
91 | // When zooming in, animate the planets out from the center.
92 | orbitScale = { zoomFactor },
93 | // Slow then stop animation when a child is being zoomed in.
94 | orbitAnimationScale = { 1f - childZoomFactor },
95 | onPlanetTouched = {
96 | planetUnderFinger = it
97 | },
98 | onPlanetSelected = {
99 | planetUnderFinger = -1
100 | if (it in planets.indices) {
101 | zoomToChild("planet-${planets[it].name}")
102 | }
103 | }
104 | ) { planet ->
105 | FractalNavChild(
106 | key = "planet-${planet.name}",
107 | modifier = Modifier
108 | ) {
109 | PlanetItem(planet)
110 | }
111 | }
112 |
113 | if (isActive) {
114 | InfoText(
115 | text = star.description,
116 | modifier = Modifier
117 | .fillExpandedWidth()
118 | .alphaByZoomFactor()
119 | )
120 | }
121 |
122 | // Empty spacer just to take up the bottom slot in the column.
123 | Spacer(Modifier)
124 | }
125 | }
126 |
127 | @Composable
128 | private fun StarImage(star: Star, modifier: Modifier = Modifier) {
129 | NetworkImage(
130 | url = star.imageUrl,
131 | contentDescription = "Image of ${star.name}",
132 | modifier = modifier,
133 | blendMode = BlendMode.Screen,
134 | )
135 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/Universe.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material.CircularProgressIndicator
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.collectAsState
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally
11 | import androidx.compose.ui.Modifier
12 | import com.zachklipp.fractalnav.FractalNavScope
13 |
14 | @Composable
15 | fun FractalNavScope.Universe(universeInfo: UniverseInfo) {
16 | val galaxies by universeInfo.galaxies.collectAsState()
17 |
18 | Column(horizontalAlignment = CenterHorizontally) {
19 | ListHeader("Galaxies")
20 |
21 | @Suppress("NAME_SHADOWING")
22 | Crossfade(galaxies) { galaxies ->
23 | if (galaxies == null) {
24 | CircularProgressIndicator()
25 | } else {
26 | SpaceGrid(galaxies) { galaxy ->
27 | GalaxyItem(galaxy, universeInfo, Modifier.fillMaxWidth())
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/UniverseInfo.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.geometry.isSpecified
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.StateFlow
9 |
10 | private val knownGalaxies = listOf(
11 | Galaxy(
12 | name = "Andromeda",
13 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Andromeda_Galaxy_560mm_FL.jpg/600px-Andromeda_Galaxy_560mm_FL.jpg",
14 | description = """
15 | The Andromeda Galaxy (IPA: /ænˈdrɒmɪdə/), also known as Messier 31, M31, or NGC 224 and
16 | originally the Andromeda Nebula (see below), is a barred spiral galaxy with diameter of
17 | about 220,000 ly approximately 2.5 million light-years (770 kiloparsecs) from Earth and
18 | the nearest large galaxy to the Milky Way. The galaxy's name stems from the area of
19 | Earth's sky in which it appears, the constellation of Andromeda, which itself is named
20 | after the Ethiopian (or Phoenician) princess who was the wife of Perseus in Greek
21 | mythology.
22 | (from Wikipedia)
23 | """.trimIndent(),
24 | ),
25 | Galaxy(
26 | name = "Antennae",
27 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Antennae_Galaxies_reloaded.jpg/600px-Antennae_Galaxies_reloaded.jpg",
28 | description = """
29 | The Antennae Galaxies (also known as NGC 4038/NGC 4039 or Caldwell 60/Caldwell 61) are a
30 | pair of interacting galaxies in the constellation Corvus. They are currently going
31 | through a starburst phase, in which the collision of clouds of gas and dust, with
32 | entangled magnetic fields, causes rapid star formation. They were discovered by William
33 | Herschel in 1785.
34 | (from Wikipedia)
35 | """.trimIndent(),
36 | ),
37 | Galaxy(
38 | name = "Backward",
39 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/NGC_4622HSTFull.jpg/600px-NGC_4622HSTFull.jpg",
40 | description = """
41 | NGC 4622 is a face-on unbarred spiral galaxy with a very prominent ring structure
42 | located in the constellation Centaurus. The galaxy is a member of the Centaurus Cluster.
43 | (from Wikipedia)
44 | """.trimIndent(),
45 | ),
46 | Galaxy(
47 | name = "Black Eye",
48 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/NGC_4826_-_HST.png/600px-NGC_4826_-_HST.png",
49 | description = """
50 | The Black Eye Galaxy (also called Sleeping Beauty Galaxy or Evil Eye Galaxy and
51 | designated Messier 64, M64, or NGC 4826) is a relatively isolated[7] spiral galaxy 17
52 | million light-years away in the mildly northern constellation of Coma Berenices. It was
53 | discovered by Edward Pigott in March 1779, and independently by Johann Elert Bode in
54 | April of the same year, as well as by Charles Messier the next year. A dark band of
55 | absorbing dust partially in front of its bright nucleus gave rise to its nicknames of
56 | the "Black Eye", "Evil Eye", or "Sleeping Beauty" galaxy.[10][11] M64 is well known
57 | among amateur astronomers due to its form in small telescopes and visibility across
58 | inhabited latitudes.
59 | (from Wikipedia)
60 | """.trimIndent(),
61 | ),
62 | Galaxy(
63 | name = "Bode's",
64 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Messier_81_HST.jpg/600px-Messier_81_HST.jpg"
65 | ),
66 | Galaxy(
67 | name = "Butterfly",
68 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/NGC_4567_%26_4568.png/600px-NGC_4567_%26_4568.png"
69 | ),
70 | Galaxy(
71 | name = "Cartwheel",
72 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/Cartwheel_Galaxy.jpg/500px-Cartwheel_Galaxy.jpg"
73 | ),
74 | Galaxy(
75 | name = "Cigar",
76 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/M82_HST_ACS_2006-14-a-large_web.jpg/600px-M82_HST_ACS_2006-14-a-large_web.jpg"
77 | ),
78 | Galaxy(
79 | name = "Circinus",
80 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/142_circinus_galaxy.png/600px-142_circinus_galaxy.png"
81 | ),
82 | Galaxy(
83 | name = "Milky Way",
84 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/43/ESO-VLT-Laser-phot-33a-07.jpg/600px-ESO-VLT-Laser-phot-33a-07.jpg",
85 | description = """
86 | The Milky Way[a] is the galaxy that includes our Solar System, with the name describing
87 | the galaxy's appearance from Earth: a hazy band of light seen in the night sky formed
88 | from stars that cannot be individually distinguished by the naked eye. The term Milky
89 | Way is a translation of the Latin via lactea, from the Greek γαλακτικός κύκλος
90 | (galaktikos kýklos), meaning "milky circle."[20][21][22] From Earth, the Milky Way
91 | appears as a band because its disk-shaped structure is viewed from within. Galileo
92 | Galilei first resolved the band of light into individual stars with his telescope in
93 | 1610. Until the early 1920s, most astronomers thought that the Milky Way contained all
94 | the stars in the Universe.[23] Following the 1920 Great Debate between the astronomers
95 | Harlow Shapley and Heber Curtis,[24] observations by Edwin Hubble showed that the Milky
96 | Way is just one of many galaxies.
97 | (from Wikipedia)
98 | """.trimIndent(),
99 | ),
100 | Galaxy(
101 | name = "Needle",
102 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/Needle_Galaxy_4565.jpeg/600px-Needle_Galaxy_4565.jpeg",
103 | description = """
104 | NGC 4565 (also known as the Needle Galaxy or Caldwell 38) is an edge-on spiral galaxy
105 | about 30 to 50 million light-years away in the constellation Coma Berenices.[2] It lies
106 | close to the North Galactic Pole and has a visual magnitude of approximately 10. It is
107 | known as the Needle Galaxy for its narrow profile.[4] First recorded in 1785 by William
108 | Herschel, it is a prominent example of an edge-on spiral galaxy.[5]
109 | (from Wikipedia)
110 | """.trimIndent(),
111 | ),
112 | Galaxy(
113 | name = "Wolf–Lundmark–Melotte",
114 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/The_WLM_galaxy_on_the_edge_of_the_Local_Group.jpg/500px-The_WLM_galaxy_on_the_edge_of_the_Local_Group.jpg",
115 | description = """
116 | The Wolf–Lundmark–Melotte (WLM) is a barred irregular galaxy discovered in 1909 by Max
117 | Wolf, located on the outer edges of the Local Group. The discovery of the nature of the
118 | galaxy was accredited to Knut Lundmark and Philibert Jacques Melotte in 1926. It is in
119 | the constellation Cetus.
120 | (from Wikipedia)
121 | """.trimIndent(),
122 | ),
123 | Galaxy(
124 | name = "Pinwheel",
125 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/M101_hires_STScI-PRC2006-10a.jpg/600px-M101_hires_STScI-PRC2006-10a.jpg",
126 | ),
127 | Galaxy(
128 | name = "Sculptor",
129 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Sculptor_Galaxy_by_VISTA.jpg/560px-Sculptor_Galaxy_by_VISTA.jpg",
130 | ),
131 | Galaxy(
132 | name = "Sombrero",
133 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/M104_ngc4594_sombrero_galaxy_hi-res.jpg/620px-M104_ngc4594_sombrero_galaxy_hi-res.jpg",
134 | ),
135 | Galaxy(
136 | name = "Southern Pinwheel",
137 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Hubble_view_of_barred_spiral_galaxy_Messier_83.jpg/600px-Hubble_view_of_barred_spiral_galaxy_Messier_83.jpg",
138 | ),
139 | Galaxy(
140 | name = "Sunflower",
141 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/M63_%28NGC_5055%29.jpg/600px-M63_%28NGC_5055%29.jpg",
142 | ),
143 | Galaxy(
144 | name = "Tadpole",
145 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/UGC_10214HST.jpg/600px-UGC_10214HST.jpg",
146 | ),
147 | Galaxy(
148 | name = "Triangulum",
149 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/VST_snaps_a_very_detailed_view_of_the_Triangulum_Galaxy.jpg/500px-VST_snaps_a_very_detailed_view_of_the_Triangulum_Galaxy.jpg",
150 | ),
151 | Galaxy(
152 | name = "Whirlpool",
153 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Messier51_sRGB.jpg/600px-Messier51_sRGB.jpg",
154 | description = """
155 | The Whirlpool Galaxy, also known as Messier 51a, M51a, and NGC 5194, is an interacting
156 | grand-design spiral galaxy with a Seyfert 2 active galactic nucleus.[5][6][7] It lies in
157 | the constellation Canes Venatici, and was the first galaxy to be classified as a spiral
158 | galaxy.[8] Its distance is 31 million light-years away from Earth.[9]
159 | The galaxy and its companion, NGC 5195,[10] are easily observed by amateur astronomers,
160 | and the two galaxies may be seen with binoculars.[11] The Whirlpool Galaxy has been
161 | extensively observed by professional astronomers, who study it to understand galaxy
162 | structure (particularly structure associated with the spiral arms) and galaxy
163 | interactions.
164 | (from Wikipedia)
165 | """.trimIndent(),
166 | ),
167 | )
168 |
169 | private val starsByGalaxy = mapOf(
170 | "Milky Way" to listOf(
171 | Star(
172 | name = "Sun",
173 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg/440px-The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg",
174 | description = """
175 | The Sun is the star at the center of the Solar System. It is a nearly perfect ball
176 | of hot plasma,[18][19] heated to incandescence by nuclear fusion reactions in its
177 | core, radiating the energy mainly as visible light, ultraviolet light, and infrared
178 | radiation. It is by far the most important source of energy for life on Earth. Its
179 | diameter is about 1.39 million kilometers (864,000 miles), or 109 times that of
180 | Earth. Its mass is about 330,000 times that of Earth, and it accounts for about
181 | 99.86% of the total mass of the Solar System.[20] Roughly three quarters of the
182 | Sun's mass consists of hydrogen (~73%); the rest is mostly helium (~25%), with much
183 | smaller quantities of heavier elements, including oxygen, carbon, neon and iron.[21]
184 | (from Wikipedia)
185 | """.trimIndent(),
186 | )
187 | )
188 | )
189 |
190 | private val planetsByStar = mapOf(
191 | "Sun" to listOf(
192 | Planet(
193 | name = "Mercury",
194 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Mercury_in_true_color.jpg/440px-Mercury_in_true_color.jpg",
195 | description = """
196 | Mercury is the smallest planet in the Solar System and the closest to the Sun. Its
197 | orbit around the Sun takes 87.97 Earth days, the shortest of all the Sun's planets.
198 | It is named after the Roman god Mercurius (Mercury), god of commerce, messenger of
199 | the gods, and mediator between gods and mortals, corresponding to the Greek god
200 | Hermes (Ἑρμῆς). Like Venus, Mercury orbits the Sun within Earth's orbit as an
201 | inferior planet, and its apparent distance from the Sun as viewed from Earth never
202 | exceeds 28°. This proximity to the Sun means the planet can only be seen near the
203 | western horizon after sunset or the eastern horizon before sunrise, usually in
204 | twilight. At this time, it may appear as a bright star-like object, but is more
205 | difficult to observe than Venus. From Earth, the planet telescopically displays the
206 | complete range of phases, similar to Venus and the Moon, which recurs over its
207 | synodic period of approximately 116 days.
208 | (from Wikipedia)
209 | """.trimIndent()
210 | ),
211 | Planet(
212 | name = "Venus",
213 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Venus_from_Mariner_10.jpg/440px-Venus_from_Mariner_10.jpg",
214 | description = """
215 | Venus is the second planet from the Sun. It is named after the Roman goddess of love
216 | and beauty. As the brightest natural object in Earth's night sky after the Moon,
217 | Venus can cast shadows and can be visible to the naked eye in broad daylight.
218 | Venus's orbit is smaller than that of Earth, but its maximal elongation is 47°;
219 | thus, it can be seen not only near the Sun in the morning or evening, but also a
220 | couple of hours before or after sunrise or sunset, depending on the observer's
221 | latitude and on the positions of Venus and the Sun. Most of the time, it can be seen
222 | either in the morning or in the evening. At some times, it may even be seen a while
223 | in a completely dark sky. Venus orbits the Sun every 224.7 Earth days.[20] It has a
224 | synodic day length of 117 Earth days and a sidereal rotation period of 243 Earth
225 | days. Consequently, it takes longer to rotate about its axis than any other planet
226 | in the Solar System, and does so in the opposite direction to all but Uranus. This
227 | means that the Sun rises from its western horizon and sets in its east.[21] Venus
228 | does not have any moons, a distinction it shares only with Mercury among the planets
229 | in the Solar System.[22]
230 | (from Wikipedia)
231 | """.trimIndent()
232 | ),
233 | Planet(
234 | name = "Earth",
235 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/The_Blue_Marble_%28remastered%29.jpg/440px-The_Blue_Marble_%28remastered%29.jpg",
236 | description = """
237 | Earth is the third planet from the Sun and the only astronomical object known to
238 | harbor life. While large amounts of water can be found throughout the Solar System,
239 | only Earth sustains liquid surface water. About 71% of Earth's surface is made up of
240 | the ocean, dwarfing Earth's polar ice, lakes, and rivers. The remaining 29% of
241 | Earth's surface is land, consisting of continents and islands. Earth's surface layer
242 | is formed of several slowly moving tectonic plates, interacting to produce mountain
243 | ranges, volcanoes, and earthquakes. Earth's liquid outer core generates the magnetic
244 | field that shapes Earth's magnetosphere, deflecting destructive solar winds.
245 | (from Wikipedia)
246 | """.trimIndent()
247 | ),
248 | Planet(
249 | name = "Mars",
250 | imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/OSIRIS_Mars_true_color.jpg/440px-OSIRIS_Mars_true_color.jpg",
251 | description = """
252 | Mars is the fourth planet from the Sun and the second-smallest planet in the Solar
253 | System, being larger than only Mercury. In English, Mars carries the name of the
254 | Roman god of war and is often called the "Red Planet".[17][18] The latter refers to
255 | the effect of the iron oxide prevalent on Mars's surface, which gives it a striking
256 | reddish appearance in the sky.[19] Mars is a terrestrial planet with a thin
257 | atmosphere, with surface features such as impact craters, valleys, dunes, and polar
258 | ice caps.
259 | (from Wikipedia)
260 | """.trimIndent()
261 | ),
262 | )
263 | )
264 |
265 | class UniverseInfo {
266 | val galaxies: StateFlow?> = MutableStateFlow(knownGalaxies)
267 |
268 | fun getStars(galaxy: Galaxy): StateFlow?> {
269 | return MutableStateFlow(starsByGalaxy[galaxy.name])
270 | }
271 |
272 | fun getPlanets(star: Star): StateFlow?> {
273 | return MutableStateFlow(planetsByStar[star.name])
274 | }
275 | }
276 |
277 | data class Galaxy(
278 | val name: String,
279 | val imageUrl: String,
280 | val description: String = "",
281 | )
282 |
283 | data class Star(
284 | val name: String,
285 | val imageUrl: String,
286 | val description: String = "",
287 | )
288 |
289 | data class Planet(
290 | val name: String,
291 | val imageUrl: String,
292 | val description: String = "",
293 | )
294 |
295 | @Stable
296 | fun PolarOffset(angle: Float, radius: Float): PolarOffset = PolarOffset(Offset(angle, radius))
297 |
298 | @JvmInline
299 | @Immutable
300 | value class PolarOffset internal constructor(private val offset: Offset) {
301 | @Stable
302 | val angle: Float
303 | get() = offset.x
304 |
305 | @Stable
306 | val radius: Float
307 | get() = offset.y
308 |
309 | @Stable
310 | val isSpecified: Boolean
311 | get() = offset.isSpecified
312 |
313 | @Stable
314 | override fun toString(): String {
315 | return if (isSpecified) {
316 | "PolarOffset(angle=$angle, radius=$radius)"
317 | } else {
318 | "PolarOffset.Unspecified"
319 | }
320 | }
321 |
322 | companion object {
323 | @Stable
324 | val Zero = PolarOffset(Offset.Zero)
325 |
326 | @Stable
327 | val Unspecified = PolarOffset(Offset.Unspecified)
328 | }
329 | }
330 |
331 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp.ui.theme
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.darkColors
5 | import androidx.compose.runtime.Composable
6 |
7 | private val DarkColorPalette = darkColors(
8 | primary = Purple200,
9 | primaryVariant = Purple700,
10 | secondary = Teal200
11 | )
12 |
13 | @Composable
14 | fun FractalnavTheme(content: @Composable () -> Unit) {
15 | MaterialTheme(
16 | colors = DarkColorPalette,
17 | typography = Typography,
18 | shapes = Shapes,
19 | content = content
20 | )
21 | }
--------------------------------------------------------------------------------
/galaxyapp/src/main/java/com/zachklipp/galaxyapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.zachklipp.galaxyapp.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/galaxyapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | fractal-nav
3 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/galaxyapp/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zach-klippenstein/compose-fractal-nav/bf661446f1dbd224cc7f03a8424d04405bc03fa5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Apr 02 17:21:14 PDT 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "fractal-nav"
16 | include ':filebrowser'
17 | include ':fractalnav'
18 | include ':galaxyapp'
19 |
--------------------------------------------------------------------------------