-
Notifications
You must be signed in to change notification settings - Fork 1
인증샷 촬영 기능 구현 #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
인증샷 촬영 기능 구현 #41
Changes from 59 commits
084eb6a
3597ab5
4e001bd
6f50885
846a1ef
7df1e27
8c2818e
b33218e
d3a4a95
0fd6cf3
203fd84
df06bb4
6949feb
4f7972e
215fae3
1d1a933
9268343
c678149
4146605
5fb89fe
5f881aa
41d90d6
7a50827
2b7534e
8c61d1a
f2947c7
19c23fe
9733fde
900c7cc
ac190dd
753f3b8
94ea778
8deb072
7cf1a78
326c781
34442dd
5706c1a
ccfe5e0
d56df8b
b2f436e
c6d7b0d
acaeec3
0f480fd
0e22c51
0450fef
a0255ea
e862959
987840d
1b1895b
c8b73b6
1d628d7
3f55676
4f5b5fc
f7654f4
0658bd3
6d72767
170a413
d908bbf
342d989
6f6c4b5
af74717
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| plugins { | ||
| alias(libs.plugins.twix.feature) | ||
| } | ||
|
|
||
| android { | ||
| namespace = "com.twix.task_certification" | ||
| } | ||
| dependencies { | ||
| implementation(libs.bundles.cameraX) | ||
| implementation(libs.guava) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Add project specific ProGuard rules here. | ||
| # You can control the set of applied configuration files using the | ||
| # proguardFiles setting in build.gradle. | ||
| # | ||
| # For more details, see | ||
| # http://developer.android.com/guide/developing/tools/proguard.html | ||
|
|
||
| # If your project uses WebView with JS, uncomment the following | ||
| # and specify the fully qualified class name to the JavaScript interface | ||
| # class: | ||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
| # public *; | ||
| #} | ||
|
|
||
| # Uncomment this to preserve the line number information for | ||
| # debugging stack traces. | ||
| #-keepattributes SourceFile,LineNumberTable | ||
|
|
||
| # If you keep the line number information, uncomment this to | ||
| # hide the original source file name. | ||
| #-renamesourcefileattribute SourceFile |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <uses-permission android:name="android.permission.CAMERA"/> | ||
| </manifest> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 Camera unbind 메서드가 호출이 안되고 있어서 DisposableEffect나 다른 방식을 써서 화면이 사라질 때 unbind를 호출해야 할 거 같아요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package com.twix.task_certification | ||
|
|
||
| import android.Manifest | ||
| import android.content.pm.PackageManager | ||
| import androidx.activity.compose.rememberLauncherForActivityResult | ||
| import androidx.activity.result.contract.ActivityResultContracts | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.DisposableEffect | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| 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.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.res.stringResource | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.lifecycle.compose.LocalLifecycleOwner | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import androidx.lifecycle.coroutineScope | ||
| import com.twix.designsystem.components.text.AppText | ||
| import com.twix.designsystem.theme.GrayColor | ||
| import com.twix.designsystem.theme.TwixTheme | ||
| import com.twix.domain.model.enums.AppTextStyle | ||
| import com.twix.task_certification.camera.Camera | ||
| import com.twix.task_certification.component.CameraControlBar | ||
| import com.twix.task_certification.component.CameraPreviewBox | ||
| import com.twix.task_certification.component.TaskCertificationTopBar | ||
| import com.twix.task_certification.model.CameraPreview | ||
| import com.twix.task_certification.model.TaskCertificationIntent | ||
| import com.twix.task_certification.model.TaskCertificationUiState | ||
| import kotlinx.coroutines.launch | ||
| import org.koin.androidx.compose.koinViewModel | ||
| import org.koin.compose.koinInject | ||
|
|
||
| @Composable | ||
| fun TaskCertificationRoute( | ||
| camera: Camera = koinInject(), | ||
| viewModel: TaskCertificationViewModel = koinViewModel(), | ||
| navigateToBack: () -> Unit, | ||
| ) { | ||
| val uiState by viewModel.uiState.collectAsStateWithLifecycle() | ||
| val cameraPreview by camera.surfaceRequests.collectAsStateWithLifecycle() | ||
|
|
||
| val lifecycleOwner = LocalLifecycleOwner.current | ||
| val coroutineScope = rememberCoroutineScope() | ||
| val context = androidx.compose.ui.platform.LocalContext.current | ||
|
|
||
| var hasPermission by remember { | ||
| mutableStateOf( | ||
| ContextCompat.checkSelfPermission( | ||
| context, | ||
| Manifest.permission.CAMERA, | ||
| ) == PackageManager.PERMISSION_GRANTED, | ||
| ) | ||
| } | ||
|
|
||
| val permissionLauncher = | ||
| rememberLauncherForActivityResult( | ||
| ActivityResultContracts.RequestPermission(), | ||
| ) { granted -> | ||
| hasPermission = granted | ||
| } | ||
|
|
||
| LaunchedEffect(Unit) { | ||
| if (!hasPermission) { | ||
| permissionLauncher.launch(Manifest.permission.CAMERA) | ||
| } | ||
| } | ||
|
|
||
| DisposableEffect(lifecycleOwner, uiState.lens, hasPermission) { | ||
| if (hasPermission) { | ||
| coroutineScope.launch { | ||
| camera.bind(lifecycleOwner, uiState.lens) | ||
| } | ||
| } | ||
|
|
||
| onDispose { | ||
| coroutineScope.launch { | ||
| camera.unbind() | ||
| } | ||
| } | ||
| } | ||
chanho0908 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| LaunchedEffect(uiState.torch, hasPermission) { | ||
| if (hasPermission) { | ||
| camera.toggleTorch(uiState.torch) | ||
| } | ||
| } | ||
chanho0908 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| TaskCertificationScreen( | ||
| uiState = uiState, | ||
| cameraPreview = cameraPreview, | ||
| onClickClose = navigateToBack, | ||
| onCaptureClick = { | ||
| if (!hasPermission) return@TaskCertificationScreen | ||
|
|
||
| coroutineScope.launch { | ||
| camera | ||
| .takePicture() | ||
| .onSuccess { | ||
| viewModel.dispatch(TaskCertificationIntent.TakePicture(it)) | ||
| } | ||
| } | ||
|
Comment on lines
+100
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 촬영 버튼을 빠르게 연타하면 사진이 계속 찍혀서 중간에 방어 로직 추가하면 좋을 거 같아요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요 문제는 다음 PR에 적용해놨어 ! 이 커밋 참고 부탁해 |
||
| }, | ||
| onToggleCameraClick = { | ||
| viewModel.dispatch(TaskCertificationIntent.ToggleLens) | ||
| }, | ||
| onClickFlash = { | ||
| viewModel.dispatch(TaskCertificationIntent.ToggleTorch) | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| @Composable | ||
| private fun TaskCertificationScreen( | ||
| uiState: TaskCertificationUiState, | ||
| cameraPreview: CameraPreview?, | ||
| onClickClose: () -> Unit, | ||
| onCaptureClick: () -> Unit, | ||
| onToggleCameraClick: () -> Unit, | ||
| onClickFlash: () -> Unit, | ||
| ) { | ||
| Column( | ||
| Modifier | ||
| .fillMaxSize() | ||
| .background(GrayColor.C500), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| ) { | ||
| TaskCertificationTopBar( | ||
| onClickClose = onClickClose, | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(24.26.dp)) | ||
|
|
||
| AppText( | ||
| text = stringResource(R.string.task_certification_title), | ||
| style = AppTextStyle.H2, | ||
| color = GrayColor.C100, | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(40.dp)) | ||
|
|
||
| CameraPreviewBox( | ||
| capture = uiState.capture, | ||
| previewRequest = cameraPreview, | ||
| torch = uiState.torch, | ||
| onClickFlash = { onClickFlash() }, | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(52.dp)) | ||
|
|
||
| CameraControlBar( | ||
| onCaptureClick = onCaptureClick, | ||
| onToggleCameraClick = onToggleCameraClick, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun TaskCertificationScreenPreview() { | ||
| TwixTheme { | ||
| TaskCertificationScreen( | ||
| uiState = TaskCertificationUiState(), | ||
| cameraPreview = null, | ||
| onClickClose = {}, | ||
| onCaptureClick = {}, | ||
| onToggleCameraClick = {}, | ||
| onClickFlash = {}, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.twix.task_certification | ||
|
|
||
| import android.net.Uri | ||
| import androidx.lifecycle.viewModelScope | ||
| import com.twix.task_certification.model.TaskCertificationIntent | ||
| import com.twix.task_certification.model.TaskCertificationSideEffect | ||
| import com.twix.task_certification.model.TaskCertificationUiState | ||
| import com.twix.ui.base.BaseViewModel | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| class TaskCertificationViewModel : | ||
| BaseViewModel<TaskCertificationUiState, TaskCertificationIntent, TaskCertificationSideEffect>( | ||
| TaskCertificationUiState(), | ||
| ) { | ||
| override suspend fun handleIntent(intent: TaskCertificationIntent) { | ||
| when (intent) { | ||
| is TaskCertificationIntent.TakePicture -> { | ||
| reducePicture(intent.uri) | ||
| } | ||
|
|
||
| is TaskCertificationIntent.ToggleLens -> { | ||
| reduceLens() | ||
| } | ||
|
|
||
| is TaskCertificationIntent.ToggleTorch -> { | ||
| reduceTorch() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun reducePicture(uri: Uri?) { | ||
| uri?.let { | ||
| reduce { updatePicture(uri) } | ||
| } ?: run { onFailureCapture() } | ||
| } | ||
|
|
||
| private fun onFailureCapture() { | ||
| viewModelScope.launch { | ||
| emitSideEffect(TaskCertificationSideEffect.ShowImageCaptureFailToast) | ||
| } | ||
| } | ||
|
Comment on lines
+37
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에서 지금 SideEffect를 발생시키기만 하고 처리하는 로직이 없어요! 그리고 네이밍도 토스트를 띄우거나 핸들링하는 로직에 따라서 좀 더 직관적으로 바꾸면 좋을 것 같아요
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 토스트를 띄우는건 다음 PR에 작성해놨어 리뷰 반영 커밋 : f7654f4 |
||
|
|
||
| private fun reduceLens() { | ||
| reduce { toggleLens() } | ||
| } | ||
|
|
||
| private fun reduceTorch() { | ||
| reduce { toggleTorch() } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.twix.task_certification.camera | ||
|
|
||
| import android.net.Uri | ||
| import androidx.camera.core.CameraSelector | ||
| import androidx.lifecycle.LifecycleOwner | ||
| import com.twix.task_certification.model.CameraPreview | ||
| import com.twix.task_certification.model.TorchStatus | ||
| import kotlinx.coroutines.flow.StateFlow | ||
|
|
||
| interface Camera { | ||
| val surfaceRequests: StateFlow<CameraPreview?> | ||
|
|
||
| suspend fun bind( | ||
| lifecycleOwner: LifecycleOwner, | ||
| lens: CameraSelector, | ||
| ) | ||
|
|
||
| suspend fun unbind() | ||
|
|
||
| suspend fun takePicture(): Result<Uri> | ||
|
|
||
| fun toggleTorch(torch: TorchStatus) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.