Skip to content

Commit

Permalink
Merge pull request #54 from Nexters/feature/home-ui
Browse files Browse the repository at this point in the history
[FEAT][#5] 홈 화면 UI
  • Loading branch information
josushell authored Feb 15, 2024
2 parents c0cff6a + a92d58a commit 039a018
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 22 deletions.
Binary file not shown.
Binary file modified core/designsystem/src/main/res/drawable/bg_home_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified core/designsystem/src/main/res/drawable/bg_login_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified core/designsystem/src/main/res/drawable/ic_upload_photo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 15 additions & 13 deletions core/ui/src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nexters.ilab.core.ui.component

import android.os.Build.VERSION.SDK_INT
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
Expand All @@ -21,6 +22,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage
Expand All @@ -36,8 +38,6 @@ fun ExampleImage(
contentDescription: String,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current

if (LocalInspectionMode.current) {
Icon(
imageVector = Icons.Outlined.Person,
Expand All @@ -47,11 +47,8 @@ fun ExampleImage(
.height(139.dp),
)
} else {
AsyncImage(
model = ImageRequest.Builder(context)
.data(resId)
.crossfade(true)
.build(),
Image(
painter = painterResource(id = resId),
contentDescription = contentDescription,
contentScale = ContentScale.Fit,
modifier = modifier,
Expand Down Expand Up @@ -94,15 +91,11 @@ fun BackgroundImage(
contentDescription: String,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current

if (LocalInspectionMode.current) {
Box(modifier = Modifier.fillMaxSize())
} else {
AsyncImage(
model = ImageRequest.Builder(context)
.data(resId)
.build(),
Image(
painter = painterResource(id = resId),
contentDescription = contentDescription,
contentScale = ContentScale.Fit,
modifier = modifier,
Expand Down Expand Up @@ -218,6 +211,15 @@ fun NetworkImagePreview() {
@Composable
fun BackgroundImagePreview() {
BackgroundImage(
resId = 0,
contentDescription = "Loading Image Icon",
)
}

@ComponentPreview
@Composable
fun LoadingImagePreview() {
LoadingImage(
resId = 0,
contentDescription = "Background Image Icon",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private fun CreateImageCompleteScreen(

Box(modifier = Modifier.fillMaxSize()) {
BackgroundImage(
resId = R.drawable.bg_create_image_complete_screen,
resId = R.drawable.bg_my_page_screen,
contentDescription = "Background Image for Create Image Complete Screen",
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,245 @@
package com.nexters.ilab.android.feature.home

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nexters.ilab.android.core.designsystem.R
import com.nexters.ilab.android.core.designsystem.theme.Blue500
import com.nexters.ilab.android.core.designsystem.theme.Blue600
import com.nexters.ilab.android.core.designsystem.theme.Subtitle1
import com.nexters.ilab.android.core.designsystem.theme.Subtitle2
import com.nexters.ilab.android.core.designsystem.theme.Title1
import com.nexters.ilab.android.core.designsystem.theme.Title2
import com.nexters.ilab.core.ui.component.BackgroundImage
import com.nexters.ilab.core.ui.component.ILabButton
import com.nexters.ilab.core.ui.component.ILabTopAppBar
import com.nexters.ilab.core.ui.component.NetworkImage
import com.nexters.ilab.core.ui.component.PagerIndicator
import com.nexters.ilab.core.ui.component.TopAppBarNavigationType

@Suppress("unused")
@Composable
internal fun HomeRoute(
padding: PaddingValues,
onSettingClick: () -> Unit,
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
viewModel: HomeViewModel = hiltViewModel(),
) {
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()

HomeScreen(
uiState = uiState,
padding = padding,
onSettingClick = onSettingClick,
)
}

@Composable
internal fun HomeScreen(
uiState: HomeState,
padding: PaddingValues,
onSettingClick: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
verticalArrangement = Arrangement.Center,
.padding(bottom = padding.calculateBottomPadding()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "HomeScreen")
HomeTopAppBar(onSettingClick)
Box(modifier = Modifier.fillMaxSize()) {
BackgroundImage(
resId = R.drawable.bg_home_screen,
contentDescription = "Background Image for Home Screen",
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
)
HomeContent(
styleImageList = uiState.styleImageList,
profileImageList = uiState.profileImageList,
onGenerateImgBtnClick = {},
)
}
}
}

@Composable
internal fun HomeTopAppBar(onSettingClick: () -> Unit) {
ILabTopAppBar(
titleRes = R.string.normal,
navigationType = TopAppBarNavigationType.Setting,
navigationIconContentDescription = "home title bar",
modifier = Modifier
.statusBarsPadding()
.padding(start = 20.dp, end = 10.dp)
.height(56.dp),
isTextLogo = true,
onNavigationClick = onSettingClick,
)
}

@Composable
internal fun HomeContent(
styleImageList: List<ProfileImage>,
profileImageList: List<ProfileImage>,
onGenerateImgBtnClick: () -> Unit,
) {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(count = 2),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 24.dp),
horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
verticalItemSpacing = 12.dp,
) {
item(span = StaggeredGridItemSpan.FullLine) {
HomeKeywordView(
styleImageList = styleImageList,
onGenerateImgBtnClick = onGenerateImgBtnClick,
)
}

itemsIndexed(profileImageList) { index, item ->
val imageHeight = if (index % 6 == 1 || index % 6 == 3) 336 else 162
KeywordSampleImageItem(
profileImage = item,
imageHeight = imageHeight,
)
}
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun HomeKeywordView(
styleImageList: List<ProfileImage>,
onGenerateImgBtnClick: () -> Unit,
) {
val pageCount = styleImageList.size
val pagerState = rememberPagerState(pageCount = { pageCount })

Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(id = R.string.home_style),
color = Blue500,
style = Subtitle2,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalPager(state = pagerState) { page ->
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "#" + styleImageList[page].profileKeyword,
textAlign = TextAlign.Center,
style = Title1,
)
Spacer(modifier = Modifier.height(20.dp))
NetworkImage(
imageUrl = styleImageList[page].profileImage,
contentDescription = "Style Image Example ${page + 1}",
modifier = Modifier
.clip(RoundedCornerShape(topStart = 999.dp, topEnd = 999.dp))
.size(width = 200.dp, height = 266.dp),
)
}
}
Spacer(modifier = Modifier.height(20.dp))
PagerIndicator(
pageCount = pagerState.pageCount,
currentPage = pagerState.currentPage,
targetPage = pagerState.currentPage,
currentPageOffsetFraction = pagerState.currentPageOffsetFraction,
)
Spacer(modifier = Modifier.height(32.dp))
ILabButton(
onClick = onGenerateImgBtnClick,
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
containerColor = Blue600,
contentColor = Color.White,
text = {
Text(
text = stringResource(id = R.string.home_generate_img_with_this_keyword),
style = Subtitle1,
)
},
)
Spacer(modifier = Modifier.height(40.dp))
Text(
text = stringResource(id = R.string.home_profile),
style = Title2,
color = Color.Black,
)
}
}

@Composable
internal fun KeywordSampleImageItem(
profileImage: ProfileImage,
imageHeight: Int,
) {
Box(
modifier = Modifier
.width(162.dp)
.height(height = imageHeight.dp)
.clip(RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center,
) {
NetworkImage(
imageUrl = profileImage.profileImage,
contentDescription = "Profile Image",
modifier = Modifier.fillMaxSize(),
)
Text(
text = "#" + profileImage.profileKeyword,
style = Subtitle1,
color = Color.White,
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, bottom = 16.dp),
)
}
}

@Preview(showBackground = true)
@Composable
internal fun previewHomeScreen() {
HomeScreen(HomeState(), PaddingValues(0.dp), {})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.nexters.ilab.android.feature.home

interface HomeSideEffect
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nexters.ilab.android.feature.home

data class ProfileImage(
val profileImage: String = "",
val profileKeyword: String = "",
)
data class HomeState(
val styleImageList: List<ProfileImage> = emptyList(),
val profileImageList: List<ProfileImage> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,47 @@ package com.nexters.ilab.android.feature.home

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor() : ViewModel()
class HomeViewModel @Inject constructor() : ViewModel(), ContainerHost<HomeState, HomeSideEffect> {
override val container = container<HomeState, HomeSideEffect>(HomeState())

// for test
val dummyStyleImageList: List<ProfileImage> = listOf(
ProfileImage("https://picsum.photos/200/266", "몽환적인"),
ProfileImage("https://picsum.photos/200/266", "자연적인"),
ProfileImage("https://picsum.photos/200/266", "스케치"),
ProfileImage("https://picsum.photos/200/266", "고독한"),
)

val dummyProfileImageList: List<ProfileImage> = listOf(
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
ProfileImage("https://picsum.photos/162/336", "몽환적인"),
ProfileImage("https://picsum.photos/162/336", "자연적인"),
)

init {
intent {
reduce {
state.copy(styleImageList = dummyStyleImageList)
}
reduce {
state.copy(profileImageList = dummyProfileImageList)
}
}
}
}
Loading

0 comments on commit 039a018

Please sign in to comment.