Skip to content

Commit

Permalink
feat: Timer 구현 및 QuizScreen 구현 (#446)
Browse files Browse the repository at this point in the history
* chore: util- android 추가

* feat: 시험 결과 화면 WIP

* refactor: MeasurPolicy 재사용 가능하도록 변경

* refactor: PagerState 따로 분리

* feat: Timer 구현

* feat: LinearProgressBar 구현

* feat: 퀴즈 화면 구현

* fix: gradlew build 검출사항
  • Loading branch information
EvergreenTree97 authored Apr 30, 2023
1 parent 62480a6 commit b2a9c5e
Show file tree
Hide file tree
Showing 10 changed files with 582 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ internal fun ExamResultScreen(
}

@Composable
private fun GrayBorderSmallButton(
internal fun GrayBorderSmallButton(
modifier: Modifier = Modifier,
text: String,
onClick: () -> Unit,
Expand All @@ -150,7 +150,7 @@ private fun GrayBorderSmallButton(

// TODO(EvergreenTree97): QuackLoadingIndicator로 통합 필요
@Composable
private fun LoadingIndicator() {
internal fun LoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Designed and developed by Duckie Team, 2022
*
* Licensed under the MIT.
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/

package team.duckie.app.android.feature.ui.exam.result.screen

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import team.duckie.app.android.feature.ui.exam.result.ExamResultActivity
import team.duckie.app.android.feature.ui.exam.result.R
import team.duckie.app.android.feature.ui.exam.result.viewmodel.ExamResultViewModel
import team.duckie.app.android.shared.ui.compose.quack.QuackCrossfade
import team.duckie.app.android.util.compose.activityViewModel
import team.duckie.quackquack.ui.component.QuackImage
import team.duckie.quackquack.ui.component.QuackSmallButton
import team.duckie.quackquack.ui.component.QuackSmallButtonType
import team.duckie.quackquack.ui.component.QuackTopAppBar
import team.duckie.quackquack.ui.icon.QuackIcon

@Composable
internal fun QuizResultScreen(
viewModel: ExamResultViewModel = activityViewModel(),
) {
val state by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val activity = LocalContext.current as ExamResultActivity

LaunchedEffect(Unit) {
viewModel.initState()
}

Scaffold(
modifier = Modifier
.statusBarsPadding()
.navigationBarsPadding(),
topBar = {
QuackTopAppBar(
modifier = Modifier
.padding(vertical = 12.dp)
.padding(horizontal = 16.dp),
leadingIcon = QuackIcon.Close,
onLeadingIconClick = viewModel::exitExam,
)
},
bottomBar = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 20.dp,
vertical = 12.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
GrayBorderSmallButton(
modifier = Modifier
.heightIn(min = 44.dp)
.weight(1f),
text = stringResource(id = R.string.solve_retry),
onClick = { // TODO(EvergreenTree97) 해당 시험 본 적 있는지 플래그 생기면 enabled 설정 해야함
viewModel.clickRetry(activity.getString(R.string.feature_prepare))
},
)
QuackSmallButton(
modifier = Modifier
.heightIn(44.dp)
.weight(1f),
type = QuackSmallButtonType.Fill,
text = stringResource(id = R.string.exit_exam),
enabled = true,
onClick = viewModel::exitExam,
)
}
},
) { padding ->
QuackCrossfade(targetState = state.isReportLoading) {
when (it) {
true -> {
LoadingIndicator()
}

false -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(all = 16.dp),
contentAlignment = Alignment.Center,
) {
QuackImage(
modifier = Modifier.fillMaxSize(),
src = state.reportUrl,
)
}
}
}
}
}
}
1 change: 1 addition & 0 deletions feature-ui-solve-problem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
projects.utilKotlin,
projects.utilCompose,
projects.utilExceptionHandling,
projects.utilAndroid,
projects.sharedUiCompose,
libs.orbit.viewmodel,
libs.orbit.compose,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Designed and developed by Duckie Team, 2022
*
* Licensed under the MIT.
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/

@file:OptIn(ExperimentalFoundationApi::class)

package team.duckie.app.android.feature.ui.solve.problem.screen

import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.feature.ui.solve.problem.R
import team.duckie.app.android.feature.ui.solve.problem.answer.answerSection
import team.duckie.app.android.feature.ui.solve.problem.common.CloseAndPageTopBar
import team.duckie.app.android.feature.ui.solve.problem.common.DoubleButtonBottomBar
import team.duckie.app.android.feature.ui.solve.problem.question.questionSection
import team.duckie.app.android.feature.ui.solve.problem.viewmodel.SolveProblemViewModel
import team.duckie.app.android.feature.ui.solve.problem.viewmodel.SolveProblemViewModel.Companion.TimerCount
import team.duckie.app.android.feature.ui.solve.problem.viewmodel.state.SolveProblemState
import team.duckie.app.android.shared.ui.compose.LinearProgressBar
import team.duckie.app.android.shared.ui.compose.dialog.DuckieDialog
import team.duckie.app.android.util.compose.activityViewModel
import team.duckie.app.android.util.compose.moveNextPage
import team.duckie.app.android.util.compose.movePreviousPage
import team.duckie.app.android.util.kotlin.exception.duckieResponseFieldNpe

private const val QuizTopAppBarLayoutId = "QuizTopAppBar"
private const val QuizContentLayoutId = "QuizContent"
private const val QuizBottomBarLayoutId = "QuizBottomBar"

@Composable
internal fun QuizScreen(
viewModel: SolveProblemViewModel = activityViewModel(),
) {
val state by viewModel.container.stateFlow.collectAsStateWithLifecycle()
val totalPage = remember { state.totalPage }
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
var examExitDialogVisible by remember { mutableStateOf(false) }
var examSubmitDialogVisible by remember { mutableStateOf(false) }

LaunchedEffect(key1 = pagerState.currentPage) {
viewModel.setPage(pagerState.currentPage)
}

// 시험 종료 다이얼로그
DuckieDialog(
title = stringResource(id = R.string.quit_exam),
message = stringResource(id = R.string.not_saved),
leftButtonText = stringResource(id = R.string.cancel),
leftButtonOnClick = { examExitDialogVisible = false },
rightButtonText = stringResource(id = R.string.quit),
rightButtonOnClick = { viewModel.stopExam() },
visible = examExitDialogVisible,
onDismissRequest = { examExitDialogVisible = false },
)

// 답안 제출 다이얼로그
DuckieDialog(
title = stringResource(id = R.string.submit_answer),
message = stringResource(id = R.string.submit_answer_warning),
leftButtonText = stringResource(id = R.string.cancel),
leftButtonOnClick = { examSubmitDialogVisible = false },
rightButtonText = stringResource(id = R.string.submit),
rightButtonOnClick = { viewModel.finishExam() },
visible = examSubmitDialogVisible,
onDismissRequest = { examSubmitDialogVisible = false },
)

Layout(
content = {
CloseAndPageTopBar(
modifier = Modifier
.layoutId(QuizTopAppBarLayoutId)
.padding(vertical = 12.dp)
.padding(end = 16.dp),
onCloseClick = {
examExitDialogVisible = true
},
currentPage = pagerState.currentPage + 1,
totalPage = totalPage,
)
ContentSection(
modifier = Modifier.layoutId(QuizContentLayoutId),
viewModel = viewModel,
pagerState = pagerState,
state = state,
)
DoubleButtonBottomBar(
modifier = Modifier.layoutId(QuizBottomBarLayoutId),
isFirstPage = pagerState.currentPage == 0,
isLastPage = pagerState.currentPage == totalPage - 1,
onLeftButtonClick = {
coroutineScope.launch {
pagerState.movePreviousPage(viewModel::onMovePreviousPage)
}
},
onRightButtonClick = {
coroutineScope.launch {
if (pagerState.currentPage == totalPage - 1) {
examSubmitDialogVisible = true
} else {
pagerState.moveNextPage(viewModel::onMoveNextPage)
}
}
},
)
},
measurePolicy = screenMeasurePolicy(
topLayoutId = QuizTopAppBarLayoutId,
contentLayoutId = QuizContentLayoutId,
bottomLayoutId = QuizBottomBarLayoutId,
),
)
}

@Composable
private fun ContentSection(
modifier: Modifier = Modifier,
viewModel: SolveProblemViewModel,
pagerState: PagerState,
state: SolveProblemState,
) {
val progress by viewModel.timerCount.collectAsStateWithLifecycle()
val timeOver by remember {
derivedStateOf { progress == 0 }
}
LaunchedEffect(timeOver) {
if (timeOver) {
Log.d("timeOver", "timeOver")
// TODO(EvergreenTree97) 추후 덕퀴즈 종료로 이동 viewModel.finishExam()
}
}
DisposableEffect(state.currentPageIndex) {
viewModel.startTimer()
onDispose {
viewModel.stopTimer()
}
}
HorizontalPager(
modifier = modifier,
pageCount = state.totalPage,
state = pagerState,
) { pageIndex ->
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(space = 24.dp),
) {
progressSection(
progress = { progress.toFloat() / TimerCount },
)
questionSection(
page = pageIndex,
question = state.problems[pageIndex].problem.question,
)
val answer = state.problems[pageIndex].problem.answer
answerSection(
page = pageIndex,
answer = when (answer) {
is Answer.Short -> Answer.Short(
state.problems[pageIndex].problem.correctAnswer
?: duckieResponseFieldNpe("null 이 되면 안됩니다."),
)

is Answer.Choice, is Answer.ImageChoice -> answer
else -> duckieResponseFieldNpe("해당 분기로 빠질 수 없는 AnswerType 입니다.")
},
inputAnswers = state.inputAnswers,
onClickAnswer = viewModel::inputAnswer,
)
}
}
}

fun LazyListScope.progressSection(
modifier: Modifier = Modifier,
progress: () -> Float,
) {
item {
LinearProgressBar(
modifier = modifier,
progress = progress(),
)
}
}
Loading

0 comments on commit b2a9c5e

Please sign in to comment.