Skip to content

Commit 390332d

Browse files
committed
feat: Make UI adaptive
1 parent c77188a commit 390332d

File tree

5 files changed

+235
-26
lines changed

5 files changed

+235
-26
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960">
6+
<path
7+
android:fillColor="#e3e3e3"
8+
android:pathData="M160,720q-17,0 -28.5,-11.5T120,680q0,-17 11.5,-28.5T160,640h640q17,0 28.5,11.5T840,680q0,17 -11.5,28.5T800,720L160,720ZM160,520q-17,0 -28.5,-11.5T120,480q0,-17 11.5,-28.5T160,440h640q17,0 28.5,11.5T840,480q0,17 -11.5,28.5T800,520L160,520ZM160,320q-17,0 -28.5,-11.5T120,280q0,-17 11.5,-28.5T160,240h640q17,0 28.5,11.5T840,280q0,17 -11.5,28.5T800,320L160,320Z" />
9+
</vector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960">
6+
<path
7+
android:fillColor="#e3e3e3"
8+
android:pathData="M160,720q-17,0 -28.5,-11.5T120,680q0,-17 11.5,-28.5T160,640h440q17,0 28.5,11.5T640,680q0,17 -11.5,28.5T600,720L160,720ZM756,652L612,508q-12,-12 -12,-28t12,-28l144,-144q11,-11 28,-11t28,11q11,11 11,28t-11,28L696,480l116,116q11,11 11,28t-11,28q-11,11 -28,11t-28,-11ZM160,520q-17,0 -28.5,-11.5T120,480q0,-17 11.5,-28.5T160,440h320q17,0 28.5,11.5T520,480q0,17 -11.5,28.5T480,520L160,520ZM160,320q-17,0 -28.5,-11.5T120,280q0,-17 11.5,-28.5T160,240h440q17,0 28.5,11.5T640,280q0,17 -11.5,28.5T600,320L160,320Z" />
9+
</vector>

composeApp/src/wasmJsMain/kotlin/org/nsh07/nsh07/ui/AppScreen.kt

Lines changed: 159 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,59 @@ package org.nsh07.nsh07.ui
22

33
import androidx.compose.animation.AnimatedContent
44
import androidx.compose.animation.Crossfade
5+
import androidx.compose.animation.core.animateFloatAsState
6+
import androidx.compose.animation.fadeIn
7+
import androidx.compose.animation.fadeOut
8+
import androidx.compose.animation.togetherWith
59
import androidx.compose.foundation.layout.Row
610
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.padding
712
import androidx.compose.material3.Icon
13+
import androidx.compose.material3.IconButton
814
import androidx.compose.material3.MaterialTheme.typography
15+
import androidx.compose.material3.ModalWideNavigationRail
916
import androidx.compose.material3.Text
1017
import androidx.compose.material3.WideNavigationRail
1118
import androidx.compose.material3.WideNavigationRailItem
12-
import androidx.compose.material3.WideNavigationRailState
1319
import androidx.compose.material3.WideNavigationRailValue
1420
import androidx.compose.material3.rememberWideNavigationRailState
21+
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
22+
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
23+
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
1524
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.LaunchedEffect
1626
import androidx.compose.runtime.getValue
1727
import androidx.compose.runtime.mutableStateOf
1828
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.rememberCoroutineScope
1930
import androidx.compose.runtime.setValue
2031
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.draw.rotate
33+
import androidx.compose.ui.semantics.semantics
34+
import androidx.compose.ui.semantics.stateDescription
35+
import androidx.compose.ui.unit.dp
2136
import androidx.compose.ui.util.fastForEach
37+
import kotlinx.coroutines.launch
2238
import nsh07.composeapp.generated.resources.Res
2339
import nsh07.composeapp.generated.resources.filled_home
2440
import nsh07.composeapp.generated.resources.filled_info
41+
import nsh07.composeapp.generated.resources.menu
42+
import nsh07.composeapp.generated.resources.menu_open
2543
import nsh07.composeapp.generated.resources.outline_home
2644
import nsh07.composeapp.generated.resources.outline_info
2745
import org.jetbrains.compose.resources.painterResource
2846
import org.nsh07.nsh07.ui.about.AboutScreen
2947
import org.nsh07.nsh07.ui.home.HomeScreen
3048

49+
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
3150
@Composable
3251
fun AppScreen(modifier: Modifier = Modifier) {
33-
val state: WideNavigationRailState =
34-
rememberWideNavigationRailState(WideNavigationRailValue.Expanded)
35-
3652
var currentPortfolioScreen by remember { mutableStateOf(PortfolioScreen.HOME) }
3753

54+
val windowSizeClass = calculateWindowSizeClass()
55+
val state = rememberWideNavigationRailState(WideNavigationRailValue.Collapsed)
56+
val scope = rememberCoroutineScope()
57+
3858
val railItems = remember {
3959
listOf(
4060
RailItem(
@@ -53,30 +73,147 @@ fun AppScreen(modifier: Modifier = Modifier) {
5373
}
5474

5575
Row(modifier.fillMaxWidth()) {
56-
WideNavigationRail(state = state) {
57-
railItems.fastForEach {
58-
val selected = it.portfolioScreen == currentPortfolioScreen
59-
WideNavigationRailItem(
60-
selected = selected,
61-
onClick = { currentPortfolioScreen = it.portfolioScreen },
62-
icon = {
63-
Crossfade(selected) { targetState ->
64-
when (targetState) {
65-
true -> Icon(painterResource(it.selectedIcon), null)
66-
else -> Icon(painterResource(it.unselectedIcon), null)
76+
when (windowSizeClass.widthSizeClass) {
77+
WindowWidthSizeClass.Expanded -> {
78+
LaunchedEffect(Unit) {
79+
state.expand()
80+
}
81+
WideNavigationRail(
82+
state = state
83+
) {
84+
railItems.fastForEach {
85+
val selected = it.portfolioScreen == currentPortfolioScreen
86+
WideNavigationRailItem(
87+
selected = selected,
88+
onClick = { currentPortfolioScreen = it.portfolioScreen },
89+
icon = {
90+
Crossfade(selected) { targetState ->
91+
when (targetState) {
92+
true -> Icon(painterResource(it.selectedIcon), null)
93+
else -> Icon(painterResource(it.unselectedIcon), null)
94+
}
95+
}
96+
},
97+
label = { Text(it.name, style = typography.labelLarge) },
98+
railExpanded = state.targetValue == WideNavigationRailValue.Expanded
99+
)
100+
}
101+
}
102+
}
103+
104+
WindowWidthSizeClass.Medium -> {
105+
WideNavigationRail(
106+
state = state,
107+
header = {
108+
val rotation by animateFloatAsState(if (state.targetValue == WideNavigationRailValue.Expanded) 0f else 180f)
109+
IconButton(
110+
modifier =
111+
Modifier
112+
.padding(start = 24.dp)
113+
.rotate(rotation)
114+
.semantics {
115+
stateDescription =
116+
if (state.currentValue == WideNavigationRailValue.Expanded) {
117+
"Expanded"
118+
} else {
119+
"Collapsed"
120+
}
121+
},
122+
onClick = {
123+
scope.launch {
124+
if (state.targetValue == WideNavigationRailValue.Expanded)
125+
state.collapse()
126+
else state.expand()
127+
}
128+
},
129+
) {
130+
Crossfade(state.targetValue) {
131+
when (it) {
132+
WideNavigationRailValue.Expanded -> Icon(
133+
painterResource(Res.drawable.menu_open),
134+
"Collapse rail"
135+
)
136+
137+
WideNavigationRailValue.Collapsed -> Icon(
138+
painterResource(Res.drawable.menu),
139+
"Expand rail"
140+
)
141+
}
67142
}
68143
}
69-
},
70-
label = { Text(it.name, style = typography.labelLarge) },
71-
railExpanded = state.targetValue == WideNavigationRailValue.Expanded
72-
)
144+
}
145+
) {
146+
railItems.fastForEach {
147+
val selected = it.portfolioScreen == currentPortfolioScreen
148+
WideNavigationRailItem(
149+
selected = selected,
150+
onClick = {
151+
scope.launch {
152+
currentPortfolioScreen = it.portfolioScreen
153+
state.collapse()
154+
}
155+
},
156+
icon = {
157+
Crossfade(selected) { targetState ->
158+
when (targetState) {
159+
true -> Icon(painterResource(it.selectedIcon), null)
160+
else -> Icon(painterResource(it.unselectedIcon), null)
161+
}
162+
}
163+
},
164+
label = { Text(it.name, style = typography.labelLarge) },
165+
railExpanded = state.targetValue == WideNavigationRailValue.Expanded
166+
)
167+
}
168+
}
169+
}
170+
171+
WindowWidthSizeClass.Compact -> {
172+
ModalWideNavigationRail(
173+
state = state,
174+
hideOnCollapse = true
175+
) {
176+
railItems.fastForEach {
177+
val selected = it.portfolioScreen == currentPortfolioScreen
178+
WideNavigationRailItem(
179+
selected = selected,
180+
onClick = {
181+
scope.launch {
182+
currentPortfolioScreen = it.portfolioScreen
183+
state.collapse()
184+
}
185+
},
186+
icon = {
187+
Crossfade(selected) { targetState ->
188+
when (targetState) {
189+
true -> Icon(painterResource(it.selectedIcon), null)
190+
else -> Icon(painterResource(it.unselectedIcon), null)
191+
}
192+
}
193+
},
194+
label = { Text(it.name, style = typography.labelLarge) },
195+
railExpanded = state.targetValue == WideNavigationRailValue.Expanded
196+
)
197+
}
198+
}
73199
}
74200
}
75201

76-
AnimatedContent(currentPortfolioScreen) {
202+
AnimatedContent(
203+
currentPortfolioScreen,
204+
transitionSpec = { fadeIn().togetherWith(fadeOut()) }
205+
) {
77206
when (it) {
78-
PortfolioScreen.HOME -> HomeScreen()
79-
PortfolioScreen.ABOUT -> AboutScreen()
207+
PortfolioScreen.HOME -> HomeScreen(
208+
expandRail = { scope.launch { state.expand() } },
209+
windowWidthClass = windowSizeClass.widthSizeClass
210+
)
211+
212+
PortfolioScreen.ABOUT -> AboutScreen(
213+
expandRail = { scope.launch { state.expand() } },
214+
windowWidthClass = windowSizeClass.widthSizeClass
215+
)
216+
80217
PortfolioScreen.PROJECTS -> {}
81218
}
82219
}

composeApp/src/wasmJsMain/kotlin/org/nsh07/nsh07/ui/about/AboutScreen.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,46 @@ import androidx.compose.foundation.layout.fillMaxHeight
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.height
1010
import androidx.compose.material3.CircularProgressIndicator
11+
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.Icon
13+
import androidx.compose.material3.IconButton
1114
import androidx.compose.material3.MaterialTheme.colorScheme
1215
import androidx.compose.material3.Scaffold
1316
import androidx.compose.material3.Text
17+
import androidx.compose.material3.TopAppBar
18+
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
1419
import androidx.compose.runtime.Composable
1520
import androidx.compose.ui.Alignment
1621
import androidx.compose.ui.Modifier
1722
import androidx.compose.ui.text.style.TextAlign
1823
import androidx.compose.ui.unit.dp
1924
import androidx.compose.ui.unit.sp
25+
import nsh07.composeapp.generated.resources.Res
26+
import nsh07.composeapp.generated.resources.menu
27+
import org.jetbrains.compose.resources.painterResource
2028

29+
@OptIn(ExperimentalMaterial3Api::class)
2130
@Composable
22-
fun AboutScreen(modifier: Modifier = Modifier) {
23-
Scaffold(modifier.fillMaxSize()) {
31+
fun AboutScreen(
32+
expandRail: () -> Unit,
33+
windowWidthClass: WindowWidthSizeClass,
34+
modifier: Modifier = Modifier
35+
) {
36+
Scaffold(
37+
topBar = {
38+
TopAppBar(
39+
navigationIcon = {
40+
if (windowWidthClass == WindowWidthSizeClass.Compact) {
41+
IconButton(onClick = expandRail) {
42+
Icon(painterResource(Res.drawable.menu), "Open menu")
43+
}
44+
}
45+
},
46+
title = {}
47+
)
48+
},
49+
modifier = modifier.fillMaxSize()
50+
) {
2451
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
2552
Column(
2653
horizontalAlignment = Alignment.CenterHorizontally,

composeApp/src/wasmJsMain/kotlin/org/nsh07/nsh07/ui/home/HomeScreen.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,46 @@ import androidx.compose.foundation.layout.fillMaxHeight
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.height
1010
import androidx.compose.material3.CircularProgressIndicator
11+
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.Icon
13+
import androidx.compose.material3.IconButton
1114
import androidx.compose.material3.MaterialTheme.colorScheme
1215
import androidx.compose.material3.Scaffold
1316
import androidx.compose.material3.Text
17+
import androidx.compose.material3.TopAppBar
18+
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
1419
import androidx.compose.runtime.Composable
1520
import androidx.compose.ui.Alignment
1621
import androidx.compose.ui.Modifier
1722
import androidx.compose.ui.text.style.TextAlign
1823
import androidx.compose.ui.unit.dp
1924
import androidx.compose.ui.unit.sp
25+
import nsh07.composeapp.generated.resources.Res
26+
import nsh07.composeapp.generated.resources.menu
27+
import org.jetbrains.compose.resources.painterResource
2028

29+
@OptIn(ExperimentalMaterial3Api::class)
2130
@Composable
22-
fun HomeScreen(modifier: Modifier = Modifier) {
23-
Scaffold(modifier.fillMaxSize()) {
31+
fun HomeScreen(
32+
expandRail: () -> Unit,
33+
windowWidthClass: WindowWidthSizeClass,
34+
modifier: Modifier = Modifier
35+
) {
36+
Scaffold(
37+
topBar = {
38+
TopAppBar(
39+
navigationIcon = {
40+
if (windowWidthClass == WindowWidthSizeClass.Compact) {
41+
IconButton(onClick = expandRail) {
42+
Icon(painterResource(Res.drawable.menu), "Open menu")
43+
}
44+
}
45+
},
46+
title = {}
47+
)
48+
},
49+
modifier = modifier.fillMaxSize()
50+
) {
2451
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
2552
Column(
2653
horizontalAlignment = Alignment.CenterHorizontally,

0 commit comments

Comments
 (0)