Skip to content

Commit 502b67f

Browse files
authored
Add camera capabilities (#6)
1 parent ff80ef8 commit 502b67f

File tree

13 files changed

+497
-15
lines changed

13 files changed

+497
-15
lines changed

feature/homeImpl/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ dependencies {
1111
implementation(projects.feature.featureAApi)
1212

1313
implementation(libs.bundles.exoplayer)
14+
implementation(libs.bundles.camerax)
15+
16+
implementation(libs.glide.compose)
1417
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
3+
<uses-feature
4+
android:name="android.hardware.camera"
5+
android:required="false" />
6+
<uses-permission android:name="android.permission.CAMERA" />
7+
</manifest>

feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.featuremodule.homeImpl
22

3+
import android.graphics.Bitmap
4+
import androidx.compose.runtime.getValue
5+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
36
import androidx.navigation.NavGraphBuilder
47
import androidx.navigation.compose.composable
58
import com.featuremodule.core.navigation.HIDE_NAV_BAR
69
import com.featuremodule.homeApi.HomeDestination
10+
import com.featuremodule.homeImpl.camera.TakePhotoScreen
711
import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen
12+
import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen
813
import com.featuremodule.homeImpl.ui.HomeScreen
914

1015
fun NavGraphBuilder.registerHome() {
@@ -15,12 +20,36 @@ fun NavGraphBuilder.registerHome() {
1520
composable(InternalRoutes.ExoplayerDestination.ROUTE) {
1621
ExoplayerScreen()
1722
}
23+
24+
composable(InternalRoutes.ImageUploadDestination.ROUTE) { backStack ->
25+
val bitmap by backStack.savedStateHandle
26+
.getStateFlow<Bitmap?>(InternalRoutes.ImageUploadDestination.BITMAP_POP_ARG, null)
27+
.collectAsStateWithLifecycle()
28+
ImageUploadScreen(returnedBitmap = bitmap)
29+
}
30+
31+
composable(InternalRoutes.TakePhotoDestination.ROUTE) {
32+
TakePhotoScreen()
33+
}
1834
}
1935

20-
internal sealed class InternalRoutes {
36+
internal class InternalRoutes {
2137
object ExoplayerDestination {
2238
const val ROUTE = HIDE_NAV_BAR + "exoplayer"
2339

2440
fun constructRoute() = ROUTE
2541
}
42+
43+
object ImageUploadDestination {
44+
const val ROUTE = "image_upload"
45+
const val BITMAP_POP_ARG = "bitmap"
46+
47+
fun constructRoute() = ROUTE
48+
}
49+
50+
object TakePhotoDestination {
51+
const val ROUTE = HIDE_NAV_BAR + "take_photo"
52+
53+
fun constructRoute() = ROUTE
54+
}
2655
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.featuremodule.homeImpl.camera
2+
3+
import android.graphics.Bitmap
4+
import com.featuremodule.core.ui.UiEvent
5+
import com.featuremodule.core.ui.UiState
6+
7+
internal class State : UiState
8+
9+
internal sealed interface Event : UiEvent {
10+
data class CaptureSuccess(val bitmap: Bitmap, val rotation: Int) : Event
11+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.featuremodule.homeImpl.camera
2+
3+
import androidx.camera.core.CameraSelector
4+
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
5+
import androidx.camera.core.ImageProxy
6+
import androidx.camera.view.CameraController
7+
import androidx.camera.view.LifecycleCameraController
8+
import androidx.camera.view.PreviewView
9+
import androidx.compose.foundation.background
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.WindowInsets
12+
import androidx.compose.foundation.layout.aspectRatio
13+
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.navigationBars
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.windowInsetsPadding
18+
import androidx.compose.foundation.shape.CircleShape
19+
import androidx.compose.material3.IconButton
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.LaunchedEffect
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.ui.Alignment
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.graphics.Color
27+
import androidx.compose.ui.platform.LocalContext
28+
import androidx.compose.ui.platform.LocalLifecycleOwner
29+
import androidx.compose.ui.unit.dp
30+
import androidx.compose.ui.viewinterop.AndroidView
31+
import androidx.core.content.ContextCompat
32+
import androidx.hilt.navigation.compose.hiltViewModel
33+
34+
@Composable
35+
internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) {
36+
val context = LocalContext.current
37+
val lifecycleOwner = LocalLifecycleOwner.current
38+
val previewView = remember {
39+
PreviewView(context).apply {
40+
scaleType = PreviewView.ScaleType.FILL_CENTER
41+
}
42+
}
43+
val cameraController = remember {
44+
LifecycleCameraController(context).apply {
45+
setEnabledUseCases(CameraController.IMAGE_CAPTURE)
46+
bindToLifecycle(lifecycleOwner)
47+
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
48+
}
49+
}
50+
51+
LaunchedEffect(previewView, cameraController) {
52+
previewView.controller = cameraController
53+
}
54+
55+
Box(
56+
modifier = Modifier
57+
.fillMaxSize()
58+
.background(Color.Black)
59+
.windowInsetsPadding(WindowInsets.navigationBars),
60+
) {
61+
AndroidView(
62+
factory = { previewView },
63+
Modifier
64+
.align(Alignment.Center)
65+
.aspectRatio(1f)
66+
.fillMaxSize(),
67+
)
68+
69+
IconButton(
70+
onClick = {
71+
runCatching {
72+
cameraController.takePicture(
73+
ContextCompat.getMainExecutor(context),
74+
object : OnImageCapturedCallback() {
75+
override fun onCaptureSuccess(image: ImageProxy) {
76+
viewModel.postEvent(
77+
Event.CaptureSuccess(
78+
image.toBitmap(),
79+
image.imageInfo.rotationDegrees,
80+
),
81+
)
82+
image.close()
83+
}
84+
},
85+
)
86+
}
87+
},
88+
modifier = Modifier
89+
.align(Alignment.BottomCenter)
90+
.padding(bottom = 16.dp)
91+
.size(50.dp),
92+
) {
93+
Box(
94+
modifier = Modifier
95+
.fillMaxSize()
96+
.background(MaterialTheme.colorScheme.primary, CircleShape),
97+
)
98+
}
99+
}
100+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.featuremodule.homeImpl.camera
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.Matrix
5+
import com.featuremodule.core.navigation.NavCommand
6+
import com.featuremodule.core.navigation.NavManager
7+
import com.featuremodule.core.ui.BaseVM
8+
import com.featuremodule.homeImpl.InternalRoutes.ImageUploadDestination
9+
import dagger.hilt.android.lifecycle.HiltViewModel
10+
import javax.inject.Inject
11+
12+
@HiltViewModel
13+
internal class TakePhotoVM @Inject constructor(
14+
private val navManager: NavManager,
15+
) : BaseVM<State, Event>() {
16+
override fun initialState() = State()
17+
18+
override fun handleEvent(event: Event) {
19+
when (event) {
20+
is Event.CaptureSuccess -> launch {
21+
val rotatedBitmap = rotateBitmap(event.bitmap, event.rotation)
22+
navManager.navigate(
23+
NavCommand.PopBackWithArguments(
24+
mapOf(ImageUploadDestination.BITMAP_POP_ARG to rotatedBitmap),
25+
),
26+
)
27+
}
28+
}
29+
}
30+
31+
// Because image is not rotated by default, it only has rotation value in EXIF
32+
private fun rotateBitmap(bitmap: Bitmap, rotation: Int): Bitmap {
33+
val matrix = Matrix().apply {
34+
postRotate(rotation.toFloat())
35+
}
36+
return Bitmap.createBitmap(
37+
bitmap,
38+
0,
39+
0,
40+
bitmap.width,
41+
bitmap.height,
42+
matrix,
43+
true,
44+
)
45+
}
46+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.featuremodule.homeImpl.imageUpload
2+
3+
import android.graphics.Bitmap
4+
import android.net.Uri
5+
import com.featuremodule.core.ui.UiEvent
6+
import com.featuremodule.core.ui.UiState
7+
8+
internal data class State(
9+
val image: Any? = null,
10+
) : UiState
11+
12+
internal sealed interface Event : UiEvent {
13+
data class PhotoTaken(val bitmap: Bitmap) : Event
14+
data class ImagePicked(val uri: Uri) : Event
15+
data object OpenInAppCamera : Event
16+
}

0 commit comments

Comments
 (0)