Skip to content

Commit

Permalink
Add navigation island.
Browse files Browse the repository at this point in the history
  • Loading branch information
ychescale9 committed Dec 5, 2023
1 parent e248ecb commit bff6f83
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import dagger.hilt.android.AndroidEntryPoint
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIsland
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandDivider
import io.github.reactivecircus.kstreamlined.android.designsystem.component.NavigationIslandItem
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Bookmarks
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Kotlin
import io.github.reactivecircus.kstreamlined.android.feature.home.HomeScreen

@AndroidEntryPoint
class KSActivity : ComponentActivity() {

@OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()

Expand All @@ -32,8 +52,65 @@ class KSActivity : ComponentActivity() {
window.navigationBarColor = navigationBarColor
}

HomeScreen(modifier = Modifier.navigationBarsPadding())
Box(modifier = Modifier.fillMaxSize()) {
var selectedNavItem by rememberSaveable { mutableStateOf(NavItemKey.Home) }

val pagerState = rememberPagerState(pageCount = { NavItemKey.entries.size })
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = NavItemKey.entries.size,
userScrollEnabled = false,
) {
when (it) {
NavItemKey.Home.ordinal -> {
HomeScreen()
}
NavItemKey.Saved.ordinal -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(KSTheme.colorScheme.background)
)
}
}
}

LaunchedEffect(selectedNavItem) {
pagerState.animateScrollToPage(selectedNavItem.ordinal)
}

NavigationIsland(
modifier = Modifier
.navigationBarsPadding()
.padding(8.dp)
.align(Alignment.BottomCenter),
) {
NavigationIslandItem(
selected = selectedNavItem == NavItemKey.Home,
icon = KSIcons.Kotlin,
contentDescription = "Home",
onClick = {
selectedNavItem = NavItemKey.Home
},
)
NavigationIslandDivider()
NavigationIslandItem(
selected = selectedNavItem == NavItemKey.Saved,
icon = KSIcons.Bookmarks,
contentDescription = "Saved",
onClick = {
selectedNavItem = NavItemKey.Saved
},
)
}
}
}
}
}
}

enum class NavItemKey {
Home,
Saved,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.github.reactivecircus.kstreamlined.android.designsystem.component

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.KSTheme
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Bookmarks
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.KSIcons
import io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon.Kotlin

@Composable
public fun NavigationIsland(
modifier: Modifier = Modifier,
items: @Composable RowScope.() -> Unit,
) {
Surface(
modifier = modifier,
shape = CircleShape,
color = KSTheme.colorScheme.containerInverse,
contentColor = KSTheme.colorScheme.onContainerInverse,
elevation = 4.dp,
) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
) {
items()
}
}
}

@Composable
public fun NavigationIslandItem(
selected: Boolean,
icon: ImageVector,
contentDescription: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.minimumInteractiveComponentSize()
.size(72.dp)
.clickable(
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = 40.dp,
),
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = if (selected) {
KSTheme.colorScheme.primary
} else {
KSTheme.colorScheme.onContainerInverse
},
)
}
}

@Composable
public fun NavigationIslandDivider(
modifier: Modifier = Modifier,
) {
VerticalDivider(
modifier = modifier.height(16.dp),
color = KSTheme.colorScheme.onContainerInverse,
)
}

@Composable
@PreviewLightDark
private fun PreviewNavigationIsland() {
KSTheme {
Surface {
NavigationIsland(
modifier = Modifier.padding(8.dp),
) {
NavigationIslandItem(
selected = true,
icon = KSIcons.Kotlin,
contentDescription = "Home",
onClick = {},
)
NavigationIslandDivider()
NavigationIslandItem(
selected = false,
icon = KSIcons.Bookmarks,
contentDescription = "Saved",
onClick = {},
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class KSColorScheme internal constructor(
public val onBackground: Color,
public val onBackgroundVariant: Color,
public val container: Color,
public val containerInverse: Color,
public val onContainerInverse: Color,
public val outline: Color,
public val gradient: List<Color>,
public val isDark: Boolean,
Expand All @@ -42,6 +44,8 @@ internal val LightColorScheme = KSColorScheme(
onBackground = Palette.MysteriousDepths10,
onBackgroundVariant = Palette.MysteriousDepths30,
container = Palette.CandyDreams20,
containerInverse = Palette.MysteriousDepths10,
onContainerInverse = Palette.Lavender20,
outline = Palette.CandyDreams10,
gradient = listOf(
Palette.CandyGrapeFizz50,
Expand All @@ -67,6 +71,8 @@ internal val DarkColorScheme = KSColorScheme(
onTertiary = Palette.MysteriousDepths10,
onTertiaryVariant = Palette.MysteriousDepths30,
container = Palette.MysteriousDepths20,
containerInverse = Palette.Lavender50,
onContainerInverse = Palette.MysteriousDepths40,
outline = Palette.MysteriousDepths30,
gradient = listOf(
Palette.CandyGrapeFizz50,
Expand All @@ -92,10 +98,12 @@ private object Palette {
val Lavender50 = Color(0xFF_E7DBFF)
val Lavender40 = Color(0xFF_DBD0F2)
val Lavender30 = Color(0xFF_C4BAD9)
val Lavender20 = Color(0xFF_ABA2BE)

val CandyDreams20 = Color(0xFF_F1C2F9)
val CandyDreams10 = Color(0xFF_EDB3F7)

val MysteriousDepths40 = Color(0xFF_5A5E77)
val MysteriousDepths30 = Color(0xFF_2C2F4E)
val MysteriousDepths20 = Color(0xFF_18193A)
val MysteriousDepths10 = Color(0xFF_070426)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon

import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.ImageVector

/**
* Copied from `androidx.compose.material.icons.rounded.Bookmarks`.
*/
public val KSIcons.Bookmarks: ImageVector
get() {
if (_bookmarks != null) {
return _bookmarks!!
}
_bookmarks = materialIcon(name = "Rounded.Bookmarks") {
materialPath {
moveTo(19.0f, 18.0f)
lineToRelative(2.0f, 1.0f)
verticalLineTo(3.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
horizontalLineTo(8.99f)
curveTo(7.89f, 1.0f, 7.0f, 1.9f, 7.0f, 3.0f)
horizontalLineToRelative(10.0f)
curveToRelative(1.1f, 0.0f, 2.0f, 0.9f, 2.0f, 2.0f)
verticalLineToRelative(13.0f)
close()
moveTo(15.0f, 5.0f)
horizontalLineTo(5.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
verticalLineToRelative(16.0f)
lineToRelative(7.0f, -3.0f)
lineToRelative(7.0f, 3.0f)
verticalLineTo(7.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
close()
}
}
return _bookmarks!!
}

private var _bookmarks: ImageVector? = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.unit.dp

internal fun ksIcon(
name: String,
pathData: List<PathData>,
) = ImageVector.Builder(
name = name,
defaultWidth = IconSize.dp,
defaultHeight = IconSize.dp,
viewportWidth = IconSize,
viewportHeight = IconSize,
).apply {
pathData.forEach {
addPath(
pathData = addPathNodes(it.path),
pathFillType = it.fillType,
fill = SolidColor(Color.Black),
stroke = null,
strokeLineWidth = 1f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Bevel,
strokeLineMiter = 1f,
)
}
}.build()

internal class PathData(val path: String, val fillType: PathFillType)

private const val IconSize = 24f
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.reactivecircus.kstreamlined.android.designsystem.foundation.icon

import androidx.compose.ui.graphics.vector.DefaultFillType
import androidx.compose.ui.graphics.vector.ImageVector

public val KSIcons.Kotlin: ImageVector
get() {
if (_kotlin != null) {
return _kotlin!!
}
_kotlin = ksIcon(
name = "Kotlin",
pathData = listOf(
PathData(
"M22 22H2V2H22L12 12L22 22Z",
DefaultFillType,
),
)
)
return _kotlin!!
}

private var _kotlin: ImageVector? = null
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,17 @@ public fun HomeScreen(
item {
var item by remember {
mutableStateOf(
FeedItem.KotlinYouTube(
id = "yt:video:zE2LIAUisRI",
title = "Getting Started With KMP: Build Apps for iOS and Android With Shared Logic and Native UIs",
FeedItem.KotlinBlog(
id = "https://blog.jetbrains.com/?post_type=kotlin&p=409489",
title = "Tackle Advent of Code 2023 With Kotlin and Win Prizes!",
publishTime = "2023-11-23T17:00:38Z".toInstant(),
contentUrl = "https://www.youtube.com/watch?v=zE2LIAUisRI",
contentUrl = "https://blog.jetbrains.com/kotlin/2023/11/advent-of-code-2023-with-kotlin/",
savedForLater = false,
thumbnailUrl = "https://i3.ytimg.com/vi/zE2LIAUisRI/hqdefault.jpg",
description = "During this webinar, we will get you up to speed with the basics of Kotlin Multiplatform. The webinar will cover what's involved in configuring your development environment, creating a Multiplatform Mobile project, and progressing to a more elaborate project that shares the data and networking layers.",
featuredImageUrl = "https://blog.jetbrains.com/wp-content/uploads/2023/11/DSGN-18072-Social-media-banners_Blog-Featured-image-1280x720-1.png",
).toDisplayable("Yesterday"),
)
}
KotlinYouTubeCard(
KotlinBlogCard(
item = item,
onItemClick = {},
onSaveButtonClick = {
Expand Down

0 comments on commit bff6f83

Please sign in to comment.