Skip to content

Commit 73f70f3

Browse files
committed
Add webview-based content viewer for KotlinBlog.
1 parent 6096017 commit 73f70f3

File tree

13 files changed

+1159
-108
lines changed

13 files changed

+1159
-108
lines changed

android/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ dependencies {
234234
implementation(project(":kmp:data"))
235235

236236
implementation(project(":feature:common"))
237+
implementation(project(":feature:content-viewer"))
237238
implementation(project(":feature:home"))
238239
implementation(project(":feature:saved-for-later"))
239240

Lines changed: 53 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,34 @@
11
package io.github.reactivecircus.kstreamlined.android
22

3+
import android.content.Context
34
import android.os.Bundle
5+
import android.provider.Settings
46
import androidx.activity.ComponentActivity
7+
import androidx.activity.compose.BackHandler
58
import androidx.activity.compose.setContent
69
import androidx.activity.enableEdgeToEdge
7-
import androidx.compose.animation.core.EaseInOutQuart
8-
import androidx.compose.animation.core.tween
9-
import androidx.compose.foundation.ExperimentalFoundationApi
10+
import androidx.compose.animation.AnimatedContent
1011
import androidx.compose.foundation.background
1112
import androidx.compose.foundation.isSystemInDarkTheme
12-
import androidx.compose.foundation.layout.Box
1313
import androidx.compose.foundation.layout.fillMaxSize
14-
import androidx.compose.foundation.layout.navigationBarsPadding
15-
import androidx.compose.foundation.layout.padding
16-
import androidx.compose.foundation.pager.HorizontalPager
17-
import androidx.compose.foundation.pager.PagerState
18-
import androidx.compose.foundation.pager.rememberPagerState
1914
import androidx.compose.runtime.LaunchedEffect
2015
import androidx.compose.runtime.getValue
2116
import androidx.compose.runtime.mutableStateOf
2217
import androidx.compose.runtime.saveable.rememberSaveable
2318
import androidx.compose.runtime.setValue
2419
import androidx.compose.ui.Alignment
2520
import androidx.compose.ui.Modifier
26-
import androidx.compose.ui.graphics.graphicsLayer
21+
import androidx.compose.ui.graphics.Color
2722
import androidx.compose.ui.graphics.toArgb
28-
import androidx.compose.ui.unit.dp
29-
import androidx.compose.ui.util.lerp
23+
import androidx.compose.ui.platform.LocalContext
3024
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
3125
import dagger.hilt.android.AndroidEntryPoint
32-
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIsland
33-
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandDivider
34-
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandItem
3526
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
36-
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Bookmarks
37-
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
38-
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Kotlin
39-
import io.github.reactivecircus.kstreamlined.android.feature.home.HomeScreen
40-
import io.github.reactivecircus.kstreamlined.android.feature.savedforlater.SavedForLaterScreen
41-
import kotlin.math.absoluteValue
27+
import io.github.reactivecircus.kstreamlined.android.feature.contentviewer.ContentViewerScreen
4228

4329
@AndroidEntryPoint
4430
class KSActivity : ComponentActivity() {
4531

46-
@OptIn(ExperimentalFoundationApi::class)
4732
override fun onCreate(savedInstanceState: Bundle?) {
4833
installSplashScreen()
4934

@@ -54,94 +39,68 @@ class KSActivity : ComponentActivity() {
5439
setContent {
5540
KSTheme {
5641
val darkTheme = isSystemInDarkTheme()
57-
val navigationBarColor = KSTheme.colorScheme.background.toArgb()
58-
LaunchedEffect(darkTheme) {
59-
window.navigationBarColor = navigationBarColor
42+
val context = LocalContext.current
43+
val backgroundColor = KSTheme.colorScheme.background
44+
LaunchedEffect(darkTheme, context) {
45+
val navigationBarColor = when (SystemNavigationMode.of(context)) {
46+
SystemNavigationMode.Gesture -> Color.Transparent
47+
else -> backgroundColor
48+
}
49+
window.navigationBarColor = navigationBarColor.toArgb()
6050
}
6151

62-
Box(
52+
var navDestination by rememberSaveable { mutableStateOf(NavDestination.Main) }
53+
54+
AnimatedContent(
55+
navDestination,
6356
modifier = Modifier
6457
.fillMaxSize()
65-
.background(KSTheme.colorScheme.background)
58+
.background(KSTheme.colorScheme.background),
59+
contentAlignment = Alignment.Center,
60+
label = "NavTransition",
6661
) {
67-
var selectedNavItem by rememberSaveable { mutableStateOf(NavItemKey.Home) }
68-
69-
val pagerState = rememberPagerState(pageCount = { NavItemKey.entries.size })
70-
HorizontalPager(
71-
state = pagerState,
72-
modifier = Modifier.fillMaxSize(),
73-
beyondBoundsPageCount = NavItemKey.entries.size,
74-
userScrollEnabled = false,
75-
) {
76-
when (it) {
77-
NavItemKey.Home.ordinal -> {
78-
HomeScreen(
79-
modifier = Modifier.pagerScaleTransition(it, pagerState)
80-
)
81-
}
82-
83-
NavItemKey.Saved.ordinal -> {
84-
SavedForLaterScreen(
85-
modifier = Modifier.pagerScaleTransition(it, pagerState)
86-
)
87-
}
62+
when (it) {
63+
NavDestination.Main -> {
64+
MainScreen(
65+
onViewContent = {
66+
navDestination = NavDestination.ContentViewer
67+
},
68+
)
8869
}
89-
}
9070

91-
LaunchedEffect(selectedNavItem) {
92-
pagerState.animateScrollToPage(
93-
page = selectedNavItem.ordinal,
94-
animationSpec = tween(
95-
durationMillis = 400,
96-
easing = EaseInOutQuart,
97-
),
98-
)
71+
NavDestination.ContentViewer -> {
72+
ContentViewerScreen(
73+
onNavigateUp = {
74+
navDestination = NavDestination.Main
75+
},
76+
)
77+
}
9978
}
79+
}
10080

101-
NavigationIsland(
102-
modifier = Modifier
103-
.navigationBarsPadding()
104-
.padding(8.dp)
105-
.align(Alignment.BottomCenter),
106-
) {
107-
NavigationIslandItem(
108-
selected = selectedNavItem == NavItemKey.Home,
109-
icon = KSIcons.Kotlin,
110-
contentDescription = "Home",
111-
onClick = {
112-
selectedNavItem = NavItemKey.Home
113-
},
114-
)
115-
NavigationIslandDivider()
116-
NavigationIslandItem(
117-
selected = selectedNavItem == NavItemKey.Saved,
118-
icon = KSIcons.Bookmarks,
119-
contentDescription = "Saved",
120-
onClick = {
121-
selectedNavItem = NavItemKey.Saved
122-
},
123-
)
81+
BackHandler(enabled = navDestination != NavDestination.Main) {
82+
if (navDestination != NavDestination.Main) {
83+
navDestination = NavDestination.Main
12484
}
12585
}
12686
}
12787
}
12888
}
12989
}
13090

131-
@OptIn(ExperimentalFoundationApi::class)
132-
private fun Modifier.pagerScaleTransition(page: Int, pagerState: PagerState) = graphicsLayer {
133-
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
134-
lerp(
135-
start = 0.8f,
136-
stop = 1f,
137-
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f),
138-
).also { scale ->
139-
scaleX = scale
140-
scaleY = scale
141-
}
91+
enum class NavDestination {
92+
Main,
93+
ContentViewer,
14294
}
14395

144-
enum class NavItemKey {
145-
Home,
146-
Saved,
96+
enum class SystemNavigationMode {
97+
ThreeButton,
98+
TwoButton,
99+
Gesture;
100+
101+
companion object {
102+
fun of(context: Context) = entries.getOrNull(
103+
Settings.Secure.getInt(context.contentResolver, "navigation_mode", -1)
104+
)
105+
}
147106
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.github.reactivecircus.kstreamlined.android
2+
3+
import androidx.compose.animation.core.EaseInOutQuart
4+
import androidx.compose.animation.core.tween
5+
import androidx.compose.foundation.ExperimentalFoundationApi
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.navigationBarsPadding
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.pager.HorizontalPager
11+
import androidx.compose.foundation.pager.PagerState
12+
import androidx.compose.foundation.pager.rememberPagerState
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.LaunchedEffect
15+
import androidx.compose.runtime.getValue
16+
import androidx.compose.runtime.mutableStateOf
17+
import androidx.compose.runtime.saveable.rememberSaveable
18+
import androidx.compose.runtime.setValue
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.graphicsLayer
22+
import androidx.compose.ui.unit.dp
23+
import androidx.compose.ui.util.lerp
24+
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIsland
25+
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandDivider
26+
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandItem
27+
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Bookmarks
28+
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
29+
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Kotlin
30+
import io.github.reactivecircus.kstreamlined.android.feature.home.HomeScreen
31+
import io.github.reactivecircus.kstreamlined.android.feature.savedforlater.SavedForLaterScreen
32+
import kotlin.math.absoluteValue
33+
34+
@OptIn(ExperimentalFoundationApi::class)
35+
@Composable
36+
fun MainScreen(
37+
onViewContent: (id: String) -> Unit,
38+
modifier: Modifier = Modifier,
39+
) {
40+
Box(modifier = modifier.fillMaxSize()) {
41+
var selectedNavItem by rememberSaveable { mutableStateOf(NavItemKey.Home) }
42+
43+
val pagerState = rememberPagerState(pageCount = { NavItemKey.entries.size })
44+
HorizontalPager(
45+
state = pagerState,
46+
modifier = Modifier.fillMaxSize(),
47+
beyondBoundsPageCount = NavItemKey.entries.size,
48+
userScrollEnabled = false,
49+
) {
50+
when (it) {
51+
NavItemKey.Home.ordinal -> {
52+
HomeScreen(
53+
onViewContent = onViewContent,
54+
modifier = Modifier.pagerScaleTransition(it, pagerState)
55+
)
56+
}
57+
58+
NavItemKey.Saved.ordinal -> {
59+
SavedForLaterScreen(
60+
onViewContent = onViewContent,
61+
modifier = Modifier.pagerScaleTransition(it, pagerState)
62+
)
63+
}
64+
}
65+
}
66+
67+
LaunchedEffect(selectedNavItem) {
68+
pagerState.animateScrollToPage(
69+
page = selectedNavItem.ordinal,
70+
animationSpec = tween(
71+
durationMillis = 400,
72+
easing = EaseInOutQuart,
73+
),
74+
)
75+
}
76+
77+
NavigationIsland(
78+
modifier = Modifier
79+
.navigationBarsPadding()
80+
.padding(8.dp)
81+
.align(Alignment.BottomCenter),
82+
) {
83+
NavigationIslandItem(
84+
selected = selectedNavItem == NavItemKey.Home,
85+
icon = KSIcons.Kotlin,
86+
contentDescription = "Home",
87+
onClick = {
88+
selectedNavItem = NavItemKey.Home
89+
},
90+
)
91+
NavigationIslandDivider()
92+
NavigationIslandItem(
93+
selected = selectedNavItem == NavItemKey.Saved,
94+
icon = KSIcons.Bookmarks,
95+
contentDescription = "Saved",
96+
onClick = {
97+
selectedNavItem = NavItemKey.Saved
98+
},
99+
)
100+
}
101+
}
102+
}
103+
104+
@OptIn(ExperimentalFoundationApi::class)
105+
private fun Modifier.pagerScaleTransition(page: Int, pagerState: PagerState) = graphicsLayer {
106+
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
107+
lerp(
108+
start = 0.8f,
109+
stop = 1f,
110+
fraction = 1f - pageOffset.absoluteValue.coerceIn(0f, 1f),
111+
).also { scale ->
112+
scaleX = scale
113+
scaleY = scale
114+
}
115+
}
116+
117+
enum class NavItemKey {
118+
Home,
119+
Saved,
120+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.github.reactivecircus.kstreamlined.android.designsystem.component
2+
3+
import androidx.compose.animation.core.animateFloatAsState
4+
import androidx.compose.foundation.layout.padding
5+
import androidx.compose.foundation.layout.width
6+
import androidx.compose.material3.ProgressIndicatorDefaults
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.graphics.Color
11+
import androidx.compose.ui.tooling.preview.PreviewLightDark
12+
import androidx.compose.ui.unit.dp
13+
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
14+
import androidx.compose.material3.LinearProgressIndicator as MaterialLinearProgressIndicator
15+
16+
@Composable
17+
public fun LinearProgressIndicator(
18+
progress: () -> Float,
19+
modifier: Modifier = Modifier,
20+
onProgressAnimationEnd: () -> Unit = {},
21+
color: Color = KSTheme.colorScheme.primary,
22+
trackColor: Color = KSTheme.colorScheme.container,
23+
) {
24+
val animatedProgress by animateFloatAsState(
25+
targetValue = progress(),
26+
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
27+
label = "ProgressAnimation",
28+
finishedListener = { onProgressAnimationEnd() },
29+
)
30+
MaterialLinearProgressIndicator(
31+
progress = { animatedProgress },
32+
modifier = modifier,
33+
color = color,
34+
trackColor = trackColor,
35+
)
36+
}
37+
38+
@Composable
39+
@PreviewLightDark
40+
private fun PreviewLinearProgressIndicator() {
41+
KSTheme {
42+
Surface {
43+
LinearProgressIndicator(
44+
progress = { 0.5f },
45+
modifier = Modifier
46+
.width(200.dp)
47+
.padding(8.dp),
48+
)
49+
}
50+
}
51+
}

android/designsystem/src/main/java/io/github/reactivecircus/kstreamlined/android/designsystem/foundation/icon/KSIcons.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import androidx.compose.material.icons.rounded.Close
55
import androidx.compose.material.icons.rounded.KeyboardArrowDown
66
import androidx.compose.material.icons.rounded.PlayArrow
77
import androidx.compose.material.icons.rounded.Settings
8+
import androidx.compose.material.icons.rounded.Share
89
import androidx.compose.ui.graphics.vector.ImageVector
910

1011
public object KSIcons {
1112
public val Close: ImageVector = Icons.Rounded.Close
1213
public val Settings: ImageVector = Icons.Rounded.Settings
1314
public val ArrowDown: ImageVector = Icons.Rounded.KeyboardArrowDown
1415
public val PlayArrow: ImageVector = Icons.Rounded.PlayArrow
16+
public val Share: ImageVector = Icons.Rounded.Share
1517
}

0 commit comments

Comments
 (0)