-
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.
Implement peekaboo-ui module with custom camera view both for Android…
… and iOS (#19)
- Loading branch information
Showing
14 changed files
with
795 additions
and
0 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) |
199 changes: 199 additions & 0 deletions
199
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,199 @@ | ||
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.gestures.detectHorizontalDragGestures | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.material.CircularProgressIndicator | ||
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.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.input.pointer.pointerInput | ||
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 com.google.accompanist.permissions.ExperimentalPermissionsApi | ||
import com.google.accompanist.permissions.PermissionStatus | ||
import com.google.accompanist.permissions.rememberPermissionState | ||
import com.preat.peekaboo.ui.icon.IconPhotoCamera | ||
import java.io.ByteArrayOutputStream | ||
import java.nio.ByteBuffer | ||
import java.util.concurrent.Executors | ||
import kotlin.coroutines.resume | ||
import kotlin.coroutines.suspendCoroutine | ||
import kotlin.math.absoluteValue | ||
|
||
private val executor = Executors.newSingleThreadExecutor() | ||
|
||
@Suppress("FunctionName") | ||
@OptIn(ExperimentalPermissionsApi::class) | ||
@Composable | ||
actual fun PeekabooCamera( | ||
modifier: Modifier, | ||
onCapture: (byteArray: ByteArray?) -> Unit, | ||
) { | ||
val cameraPermissionState = | ||
rememberPermissionState( | ||
android.Manifest.permission.CAMERA, | ||
) | ||
when (cameraPermissionState.status) { | ||
PermissionStatus.Granted -> { | ||
CameraWithGrantedPermission(modifier, onCapture) | ||
} | ||
is PermissionStatus.Denied -> { | ||
LaunchedEffect(Unit) { | ||
cameraPermissionState.launchPermissionRequest() | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
private fun CameraWithGrantedPermission( | ||
modifier: Modifier, | ||
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(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) } | ||
|
||
Box( | ||
modifier = | ||
modifier | ||
.pointerInput(isFrontCamera) { | ||
detectHorizontalDragGestures { change, dragAmount -> | ||
if (dragAmount.absoluteValue > 50.0) { | ||
isFrontCamera = !isFrontCamera | ||
} | ||
} | ||
}, | ||
) { | ||
AndroidView( | ||
factory = { previewView }, | ||
modifier = Modifier.fillMaxSize(), | ||
) | ||
CircularButton( | ||
imageVector = IconPhotoCamera, | ||
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp), | ||
enabled = !capturePhotoStarted, | ||
) { | ||
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 | ||
} | ||
}, | ||
) | ||
} | ||
if (capturePhotoStarted) { | ||
CircularProgressIndicator( | ||
modifier = Modifier.size(80.dp).align(Alignment.Center), | ||
color = Color.White.copy(alpha = 0.7f), | ||
strokeWidth = 8.dp, | ||
) | ||
} | ||
} | ||
} | ||
|
||
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> |
70 changes: 70 additions & 0 deletions
70
peekaboo-ui/src/commonMain/kotlin/com/preat/peekaboo/ui/CircularButton.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,70 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material.Icon | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.vector.ImageVector | ||
import androidx.compose.ui.unit.dp | ||
import com.preat.peekaboo.ui.icon.IconCustomArrowBack | ||
import com.preat.peekaboo.ui.style.PeekabooColors | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
fun CircularButton( | ||
content: @Composable () -> Unit, | ||
modifier: Modifier = Modifier, | ||
enabled: Boolean, | ||
onClick: () -> Unit, | ||
) { | ||
Box( | ||
modifier | ||
.size(60.dp) | ||
.clip(CircleShape) | ||
.background(PeekabooColors.uiLightBlack) | ||
.run { | ||
if (enabled) { | ||
clickable { onClick() } | ||
} else { | ||
this | ||
} | ||
}, | ||
contentAlignment = Alignment.Center, | ||
) { | ||
content() | ||
} | ||
} | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
fun CircularButton( | ||
imageVector: ImageVector, | ||
modifier: Modifier = Modifier, | ||
enabled: Boolean = true, | ||
onClick: () -> Unit, | ||
) { | ||
CircularButton( | ||
modifier = modifier, | ||
content = { | ||
Icon(imageVector, null, Modifier.size(34.dp), Color.White) | ||
}, | ||
enabled = enabled, | ||
onClick = onClick, | ||
) | ||
} | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
fun BackButton(onClick: () -> Unit) { | ||
CircularButton( | ||
imageVector = IconCustomArrowBack, | ||
onClick = onClick, | ||
) | ||
} |
11 changes: 11 additions & 0 deletions
11
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,11 @@ | ||
package com.preat.peekaboo.ui | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
|
||
@Suppress("FunctionName") | ||
@Composable | ||
expect fun PeekabooCamera( | ||
modifier: Modifier, | ||
onCapture: (byteArray: ByteArray?) -> Unit, | ||
) |
Oops, something went wrong.