Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions feature/homeImpl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ dependencies {
implementation(projects.feature.featureAApi)

implementation(libs.bundles.exoplayer)
implementation(libs.bundles.camerax)

implementation(libs.glide.compose)
}
7 changes: 7 additions & 0 deletions feature/homeImpl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.featuremodule.homeImpl

import android.graphics.Bitmap
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.featuremodule.core.navigation.HIDE_NAV_BAR
import com.featuremodule.homeApi.HomeDestination
import com.featuremodule.homeImpl.camera.TakePhotoScreen
import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen
import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen
import com.featuremodule.homeImpl.ui.HomeScreen

fun NavGraphBuilder.registerHome() {
Expand All @@ -15,12 +20,36 @@ fun NavGraphBuilder.registerHome() {
composable(InternalRoutes.ExoplayerDestination.ROUTE) {
ExoplayerScreen()
}

composable(InternalRoutes.ImageUploadDestination.ROUTE) { backStack ->
val bitmap by backStack.savedStateHandle
.getStateFlow<Bitmap?>(InternalRoutes.ImageUploadDestination.BITMAP_POP_ARG, null)
.collectAsStateWithLifecycle()
ImageUploadScreen(returnedBitmap = bitmap)
}

composable(InternalRoutes.TakePhotoDestination.ROUTE) {
TakePhotoScreen()
}
}

internal sealed class InternalRoutes {
internal class InternalRoutes {
object ExoplayerDestination {
const val ROUTE = HIDE_NAV_BAR + "exoplayer"

fun constructRoute() = ROUTE
}

object ImageUploadDestination {
const val ROUTE = "image_upload"
const val BITMAP_POP_ARG = "bitmap"

fun constructRoute() = ROUTE
}

object TakePhotoDestination {
const val ROUTE = HIDE_NAV_BAR + "take_photo"

fun constructRoute() = ROUTE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.featuremodule.homeImpl.camera

import android.graphics.Bitmap
import com.featuremodule.core.ui.UiEvent
import com.featuremodule.core.ui.UiState

internal class State : UiState

internal sealed interface Event : UiEvent {
data class CaptureSuccess(val bitmap: Bitmap, val rotation: Int) : Event
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.featuremodule.homeImpl.camera

import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
import androidx.camera.core.ImageProxy
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val previewView = remember {
PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}
}
val cameraController = remember {
LifecycleCameraController(context).apply {
setEnabledUseCases(CameraController.IMAGE_CAPTURE)
bindToLifecycle(lifecycleOwner)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}
}

LaunchedEffect(previewView, cameraController) {
previewView.controller = cameraController
}

Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.windowInsetsPadding(WindowInsets.navigationBars),
) {
AndroidView(
factory = { previewView },
Modifier
.align(Alignment.Center)
.aspectRatio(1f)
.fillMaxSize(),
)

IconButton(
onClick = {
runCatching {
cameraController.takePicture(
ContextCompat.getMainExecutor(context),
object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
viewModel.postEvent(
Event.CaptureSuccess(
image.toBitmap(),
image.imageInfo.rotationDegrees,
),
)
image.close()
}
},
)
}
},
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
.size(50.dp),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.primary, CircleShape),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.featuremodule.homeImpl.camera

import android.graphics.Bitmap
import android.graphics.Matrix
import com.featuremodule.core.navigation.NavCommand
import com.featuremodule.core.navigation.NavManager
import com.featuremodule.core.ui.BaseVM
import com.featuremodule.homeImpl.InternalRoutes.ImageUploadDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
internal class TakePhotoVM @Inject constructor(
private val navManager: NavManager,
) : BaseVM<State, Event>() {
override fun initialState() = State()

override fun handleEvent(event: Event) {
when (event) {
is Event.CaptureSuccess -> launch {
val rotatedBitmap = rotateBitmap(event.bitmap, event.rotation)
navManager.navigate(
NavCommand.PopBackWithArguments(
mapOf(ImageUploadDestination.BITMAP_POP_ARG to rotatedBitmap),
),
)
}
}
}

// Because image is not rotated by default, it only has rotation value in EXIF
private fun rotateBitmap(bitmap: Bitmap, rotation: Int): Bitmap {
val matrix = Matrix().apply {
postRotate(rotation.toFloat())
}
return Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.featuremodule.homeImpl.imageUpload

import android.graphics.Bitmap
import android.net.Uri
import com.featuremodule.core.ui.UiEvent
import com.featuremodule.core.ui.UiState

internal data class State(
val image: Any? = null,
) : UiState

internal sealed interface Event : UiEvent {
data class PhotoTaken(val bitmap: Bitmap) : Event
data class ImagePicked(val uri: Uri) : Event
data object OpenInAppCamera : Event
}
Loading
Loading