-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from TEAM-PREAT/feature/add-custom-camera-view
[feature/add-custom-camera-view] Implement Custom Camera View
- Loading branch information
Showing
21 changed files
with
1,139 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
plugins { | ||
alias(libs.plugins.kotlinMultiplatform) | ||
alias(libs.plugins.androidLibrary) | ||
alias(libs.plugins.composeMultiplatform) | ||
id("module.publication") | ||
} | ||
|
||
kotlin { | ||
androidTarget { | ||
publishLibraryVariants("release") | ||
compilations.all { | ||
kotlinOptions { | ||
jvmTarget = "11" | ||
} | ||
} | ||
} | ||
|
||
iosX64() | ||
iosArm64() | ||
iosSimulatorArm64() | ||
|
||
sourceSets { | ||
commonMain.dependencies { | ||
implementation(compose.runtime) | ||
implementation(compose.foundation) | ||
implementation(compose.material) | ||
implementation(libs.components.resources) | ||
} | ||
commonTest.dependencies { | ||
implementation(libs.kotlin.test) | ||
} | ||
androidMain.dependencies { | ||
implementation(libs.androidx.activity.compose) | ||
implementation(libs.accompanist.permissions) | ||
implementation(libs.camera.camera2) | ||
implementation(libs.camera.lifecycle) | ||
implementation(libs.camera.view) | ||
} | ||
} | ||
} | ||
|
||
android { | ||
namespace = "com.preat.peekaboo.ui" | ||
compileSdk = libs.versions.android.compileSdk.get().toInt() | ||
sourceSets["main"].res.srcDirs("src/androidMain/res") | ||
defaultConfig { | ||
minSdk = libs.versions.android.minSdk.get().toInt() | ||
} | ||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_11 | ||
targetCompatibility = JavaVersion.VERSION_11 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
<uses-permission android:name="android.permission.CAMERA" /> | ||
<application> | ||
<provider | ||
android:name="com.preat.peekaboo.ui.ImageViewerFileProvider" | ||
android:authorities="com.preat.peekaboo.fileprovider" | ||
android:exported="false" | ||
android:grantUriPermissions="true" /> | ||
</application> | ||
</manifest> |
5 changes: 5 additions & 0 deletions
5
peekaboo-ui/src/androidMain/kotlin/com/preat/peekaboo/ui/ImageViewerFileProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
import androidx.core.content.FileProvider | ||
|
||
class ImageViewerFileProvider : FileProvider(R.xml.file_paths) |
201 changes: 201 additions & 0 deletions
201
peekaboo-ui/src/androidMain/kotlin/com/preat/peekaboo/ui/PeekabooCamera.android.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
import android.graphics.Bitmap | ||
import android.graphics.BitmapFactory | ||
import android.graphics.Matrix | ||
import androidx.camera.core.CameraSelector | ||
import androidx.camera.core.ImageCapture | ||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback | ||
import androidx.camera.core.ImageProxy | ||
import androidx.camera.core.Preview | ||
import androidx.camera.lifecycle.ProcessCameraProvider | ||
import androidx.camera.view.PreviewView | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
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.saveable.rememberSaveable | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.platform.LocalLifecycleOwner | ||
import androidx.compose.ui.viewinterop.AndroidView | ||
import com.google.accompanist.permissions.ExperimentalPermissionsApi | ||
import com.google.accompanist.permissions.PermissionStatus | ||
import com.google.accompanist.permissions.rememberPermissionState | ||
import java.io.ByteArrayOutputStream | ||
import java.nio.ByteBuffer | ||
import java.util.concurrent.Executors | ||
import kotlin.coroutines.resume | ||
import kotlin.coroutines.suspendCoroutine | ||
|
||
private val executor = Executors.newSingleThreadExecutor() | ||
|
||
@Suppress("FunctionName") | ||
@OptIn(ExperimentalPermissionsApi::class) | ||
@Composable | ||
actual fun PeekabooCamera( | ||
modifier: Modifier, | ||
cameraMode: CameraMode, | ||
captureIcon: @Composable (onClick: () -> Unit) -> Unit, | ||
convertIcon: @Composable (onClick: () -> Unit) -> Unit, | ||
progressIndicator: @Composable () -> Unit, | ||
onCapture: (byteArray: ByteArray?) -> Unit, | ||
) { | ||
val cameraPermissionState = | ||
rememberPermissionState( | ||
android.Manifest.permission.CAMERA, | ||
) | ||
when (cameraPermissionState.status) { | ||
PermissionStatus.Granted -> { | ||
CameraWithGrantedPermission( | ||
modifier = modifier, | ||
cameraMode = cameraMode, | ||
captureIcon = captureIcon, | ||
convertIcon = convertIcon, | ||
progressIndicator = progressIndicator, | ||
onCapture = onCapture, | ||
) | ||
} | ||
is PermissionStatus.Denied -> { | ||
LaunchedEffect(Unit) { | ||
cameraPermissionState.launchPermissionRequest() | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
private fun CameraWithGrantedPermission( | ||
modifier: Modifier, | ||
cameraMode: CameraMode, | ||
captureIcon: @Composable (() -> Unit) -> Unit, | ||
convertIcon: @Composable (onClick: () -> Unit) -> Unit, | ||
progressIndicator: @Composable () -> Unit, | ||
onCapture: (byteArray: ByteArray) -> Unit, | ||
) { | ||
val context = LocalContext.current | ||
val lifecycleOwner = LocalLifecycleOwner.current | ||
var cameraProvider: ProcessCameraProvider? by remember { mutableStateOf(null) } | ||
|
||
val preview = Preview.Builder().build() | ||
val previewView = remember { PreviewView(context) } | ||
val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() } | ||
var isFrontCamera by rememberSaveable { | ||
mutableStateOf( | ||
when (cameraMode) { | ||
CameraMode.Front -> true | ||
CameraMode.Back -> false | ||
}, | ||
) | ||
} | ||
val cameraSelector = | ||
remember(isFrontCamera) { | ||
val lensFacing = | ||
if (isFrontCamera) { | ||
CameraSelector.LENS_FACING_FRONT | ||
} else { | ||
CameraSelector.LENS_FACING_BACK | ||
} | ||
CameraSelector.Builder() | ||
.requireLensFacing(lensFacing) | ||
.build() | ||
} | ||
|
||
DisposableEffect(Unit) { | ||
onDispose { | ||
cameraProvider?.unbindAll() | ||
} | ||
} | ||
|
||
LaunchedEffect(isFrontCamera) { | ||
cameraProvider = | ||
suspendCoroutine<ProcessCameraProvider> { continuation -> | ||
ProcessCameraProvider.getInstance(context).also { cameraProvider -> | ||
cameraProvider.addListener( | ||
{ | ||
continuation.resume(cameraProvider.get()) | ||
}, | ||
executor, | ||
) | ||
} | ||
} | ||
cameraProvider?.unbindAll() | ||
cameraProvider?.bindToLifecycle( | ||
lifecycleOwner, | ||
cameraSelector, | ||
preview, | ||
imageCapture, | ||
) | ||
preview.setSurfaceProvider(previewView.surfaceProvider) | ||
} | ||
|
||
var capturePhotoStarted by remember { mutableStateOf(false) } | ||
|
||
val triggerCapture: () -> Unit = { | ||
capturePhotoStarted = true | ||
imageCapture.takePicture( | ||
executor, | ||
object : OnImageCapturedCallback() { | ||
override fun onCaptureSuccess(image: ImageProxy) { | ||
val rotationDegrees = image.imageInfo.rotationDegrees | ||
val buffer = image.planes[0].buffer | ||
val data = buffer.toByteArray() | ||
|
||
// Rotate the image if necessary | ||
val rotatedData = | ||
if (rotationDegrees != 0) { | ||
rotateImage(data, rotationDegrees) | ||
} else { | ||
data | ||
} | ||
|
||
image.close() | ||
onCapture(rotatedData) | ||
capturePhotoStarted = false | ||
} | ||
}, | ||
) | ||
} | ||
|
||
val toggleCamera: () -> Unit = { | ||
isFrontCamera = !isFrontCamera | ||
} | ||
|
||
Box(modifier = modifier) { | ||
AndroidView( | ||
factory = { previewView }, | ||
modifier = Modifier.fillMaxSize(), | ||
) | ||
// Call the triggerCapture lambda when the capture button is clicked | ||
captureIcon(triggerCapture) | ||
convertIcon(toggleCamera) | ||
if (capturePhotoStarted) { | ||
progressIndicator() | ||
} | ||
} | ||
} | ||
|
||
private fun ByteBuffer.toByteArray(): ByteArray { | ||
rewind() // Rewind the buffer to zero | ||
val data = ByteArray(remaining()) | ||
get(data) // Copy the buffer into a byte array | ||
return data // Return the byte array | ||
} | ||
|
||
private fun rotateImage( | ||
data: ByteArray, | ||
degrees: Int, | ||
): ByteArray { | ||
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size) | ||
val matrix = Matrix().apply { postRotate(degrees.toFloat()) } | ||
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) | ||
val outputStream = ByteArrayOutputStream() | ||
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) | ||
return outputStream.toByteArray() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<paths xmlns:android="http://schemas.android.com/apk/res/android"> | ||
<files-path name="my_images" path="share_images/"/> | ||
</paths> |
7 changes: 7 additions & 0 deletions
7
peekaboo-ui/src/commonMain/kotlin/com/preat/peekaboo/ui/CameraMode.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
sealed class CameraMode { | ||
data object Front : CameraMode() | ||
|
||
data object Back : CameraMode() | ||
} |
26 changes: 26 additions & 0 deletions
26
peekaboo-ui/src/commonMain/kotlin/com/preat/peekaboo/ui/PeekabooCamera.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
|
||
/** | ||
* `PeekabooCamera` is a composable function that provides a customizable camera UI within a Compose Multiplatform application. | ||
* It allows for the display of a camera preview, along with custom capture and convert buttons, and an optional progress indicator during photo capture. | ||
* | ||
* @param modifier The [Modifier] applied to the camera UI component for layout customization. | ||
* @param cameraMode The initial camera mode (front or back). Default is [CameraMode.Back]. | ||
* @param captureIcon A composable lambda for the capture button. It takes an `onClick` lambda that triggers the image capture process. | ||
* @param convertIcon An optional composable lambda for a button to toggle the camera mode. It takes an `onClick` lambda for switching the camera. | ||
* @param progressIndicator An optional composable lambda displayed during photo capture processing. | ||
* @param onCapture A lambda called when a photo is captured, providing the photo as a ByteArray or null if the capture fails. | ||
*/ | ||
@Suppress("FunctionName") | ||
@Composable | ||
expect fun PeekabooCamera( | ||
modifier: Modifier, | ||
cameraMode: CameraMode = CameraMode.Back, | ||
captureIcon: @Composable (onClick: () -> Unit) -> Unit, | ||
convertIcon: @Composable (onClick: () -> Unit) -> Unit = {}, | ||
progressIndicator: @Composable () -> Unit = {}, | ||
onCapture: (byteArray: ByteArray?) -> Unit, | ||
) |
9 changes: 9 additions & 0 deletions
9
peekaboo-ui/src/iosMain/kotlin/com/preat/peekaboo/ui/CameraAccess.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
internal sealed interface CameraAccess { | ||
data object Undefined : CameraAccess | ||
|
||
data object Denied : CameraAccess | ||
|
||
data object Authorized : CameraAccess | ||
} |
Oops, something went wrong.