mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Image Viewer Camera (#2864)
This commit is contained in:
@@ -13,5 +13,5 @@ kotlin.native.useEmbeddableCompilerJar=true
|
||||
kotlin.native.binary.memoryModel=experimental
|
||||
kotlin.version=1.8.0
|
||||
agp.version=7.1.3
|
||||
compose.version=1.4.0-alpha01-dev972
|
||||
compose.version=1.4.0-alpha01-dev975
|
||||
ktor.version=2.2.1
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>This app uses location data to show taken photos on a map</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
@@ -20,6 +22,8 @@
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app uses camera for capturing photos</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
@@ -3,6 +3,7 @@ pluginManagement {
|
||||
gradlePluginPortal()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
google()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
plugins {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
internal actual fun CameraView(modifier: Modifier) {
|
||||
Box(Modifier.fillMaxSize().background(Color.Black)) {
|
||||
Text(
|
||||
text = "Camera is not available on Android for now.",
|
||||
color = Color.White,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -102,9 +102,10 @@ internal fun ImageViewerCommon(
|
||||
}
|
||||
|
||||
is CameraPage -> {
|
||||
CameraScreen(onBack = {
|
||||
navigationStack.back()
|
||||
})
|
||||
CameraScreen(
|
||||
localization = dependencies.localization,
|
||||
onBack = { navigationStack.back() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import example.imageviewer.Localization
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
@Composable
|
||||
internal fun CameraScreen(onBack: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize().background(Color.Black).clickable { onBack() }, contentAlignment = Alignment.Center) {
|
||||
Text("Nothing here yet 📸", textAlign = TextAlign.Center, color = Color.White)
|
||||
internal fun CameraScreen(localization: Localization, onBack: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
CameraView(Modifier.fillMaxSize())
|
||||
TopLayout(
|
||||
alignLeftContent = {
|
||||
Tooltip(localization.back) {
|
||||
CircularButton(
|
||||
painterResource("arrowleft.png"),
|
||||
onClick = { onBack() }
|
||||
)
|
||||
}
|
||||
},
|
||||
alignRightContent = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
internal expect fun CameraView(modifier: Modifier)
|
||||
@@ -15,9 +15,13 @@ import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.style.ImageviewerColors
|
||||
|
||||
@Composable
|
||||
internal fun CircularButton(image: Painter, onClick: () -> Unit) {
|
||||
internal fun CircularButton(
|
||||
image: Painter,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(40.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
|
||||
modifier.size(50.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
|
||||
.clickable { onClick() }, contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
|
||||
@@ -11,10 +11,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -89,7 +86,11 @@ internal fun GalleryScreen(
|
||||
onFullScreen = { onClickPreviewPicture(it) }
|
||||
)
|
||||
}
|
||||
MakeNewMemoryMiniature(onMakeNewMemory)
|
||||
CircularButton(
|
||||
image = painterResource("plus.png"),
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(48.dp),
|
||||
onClick = onMakeNewMemory,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (pictures.isEmpty()) {
|
||||
@@ -123,33 +124,6 @@ private fun SquaresGalleryView(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
@Composable
|
||||
private fun BoxScope.MakeNewMemoryMiniature(onClick: () -> Unit) {
|
||||
Column(modifier = Modifier.align(Alignment.BottomCenter)) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.width(52.dp)
|
||||
.background(ImageviewerColors.uiLightBlack)
|
||||
.aspectRatio(1.0f)
|
||||
.clickable {
|
||||
onClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("plus.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.width(18.dp)
|
||||
.height(18.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
@Composable
|
||||
internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) {
|
||||
|
||||
@@ -155,7 +155,7 @@ internal fun BoxScope.MagicButtonOverlay(onClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 12.dp, bottom = 16.dp)
|
||||
) {
|
||||
CircularButton(painterResource("magic.png"), onClick)
|
||||
CircularButton(painterResource("magic.png"), onClick = onClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ internal fun TopLayout(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.notchPadding()
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(Modifier.align(Alignment.CenterStart)) {
|
||||
alignLeftContent()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
internal actual fun CameraView(modifier: Modifier) {
|
||||
Box(Modifier.fillMaxSize().background(Color.Black)) {
|
||||
Text(
|
||||
text = "Camera is not available on Desktop for now.",
|
||||
color = Color.White,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.interop.UIKitInteropView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.cinterop.CValue
|
||||
import platform.AVFoundation.*
|
||||
import platform.AVFoundation.AVCaptureDeviceDiscoverySession.Companion.discoverySessionWithDeviceTypes
|
||||
import platform.AVFoundation.AVCaptureDeviceInput.Companion.deviceInputWithDevice
|
||||
import platform.CoreGraphics.CGRect
|
||||
import platform.Foundation.NSError
|
||||
import platform.QuartzCore.CATransaction
|
||||
import platform.QuartzCore.kCATransactionDisableActions
|
||||
import platform.UIKit.UIDevice
|
||||
import platform.UIKit.UIDeviceOrientation
|
||||
import platform.UIKit.UIImage
|
||||
import platform.UIKit.UIView
|
||||
import platform.darwin.NSObject
|
||||
|
||||
private sealed interface CameraAccess {
|
||||
object Undefined : CameraAccess
|
||||
object Denied : CameraAccess
|
||||
object Authorized : CameraAccess
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal actual fun CameraView(modifier: Modifier) {
|
||||
var cameraAccess: CameraAccess by remember { mutableStateOf(CameraAccess.Undefined) }
|
||||
LaunchedEffect(Unit) {
|
||||
when (AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)) {
|
||||
AVAuthorizationStatusAuthorized -> {
|
||||
cameraAccess = CameraAccess.Authorized
|
||||
}
|
||||
|
||||
AVAuthorizationStatusDenied, AVAuthorizationStatusRestricted -> {
|
||||
cameraAccess = CameraAccess.Denied
|
||||
}
|
||||
|
||||
AVAuthorizationStatusNotDetermined -> {
|
||||
AVCaptureDevice.requestAccessForMediaType(
|
||||
mediaType = AVMediaTypeVideo
|
||||
) { success ->
|
||||
cameraAccess = if (success) CameraAccess.Authorized else CameraAccess.Denied
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(Color.Black),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (cameraAccess) {
|
||||
CameraAccess.Undefined -> {
|
||||
// Waiting for the user to accept permission
|
||||
}
|
||||
|
||||
CameraAccess.Denied -> {
|
||||
Text("Camera access denied", color = Color.White)
|
||||
}
|
||||
|
||||
CameraAccess.Authorized -> {
|
||||
AuthorizedCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.AuthorizedCamera() {
|
||||
val capturePhotoOutput = remember { AVCapturePhotoOutput() }
|
||||
val photoCaptureDelegate = remember {
|
||||
object : NSObject(), AVCapturePhotoCaptureDelegateProtocol {
|
||||
override fun captureOutput(
|
||||
output: AVCapturePhotoOutput,
|
||||
didFinishProcessingPhoto: AVCapturePhoto,
|
||||
error: NSError?
|
||||
) {
|
||||
val photoData = didFinishProcessingPhoto.fileDataRepresentation()
|
||||
?: error("fileDataRepresentation is null")
|
||||
val uiImage = UIImage(photoData)
|
||||
//todo pass image to gallery page
|
||||
}
|
||||
}
|
||||
}
|
||||
val camera: AVCaptureDevice? = remember {
|
||||
discoverySessionWithDeviceTypes(
|
||||
deviceTypes = listOf(AVCaptureDeviceTypeBuiltInWideAngleCamera),
|
||||
mediaType = AVMediaTypeVideo,
|
||||
position = AVCaptureDevicePositionFront,
|
||||
).devices.firstOrNull() as? AVCaptureDevice
|
||||
}
|
||||
if (camera != null) {
|
||||
val captureSession: AVCaptureSession = remember {
|
||||
AVCaptureSession().also { captureSession ->
|
||||
captureSession.sessionPreset = AVCaptureSessionPresetPhoto
|
||||
val captureDeviceInput: AVCaptureDeviceInput =
|
||||
deviceInputWithDevice(device = camera, error = null)!!
|
||||
captureSession.addInput(captureDeviceInput)
|
||||
captureSession.addOutput(capturePhotoOutput)
|
||||
}
|
||||
}
|
||||
val cameraPreviewLayer = remember {
|
||||
AVCaptureVideoPreviewLayer(session = captureSession)
|
||||
}
|
||||
UIKitInteropView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
background = Color.Black,
|
||||
resize = { view: UIView, rect: CValue<CGRect> ->
|
||||
cameraPreviewLayer.connection?.apply {
|
||||
videoOrientation = when (UIDevice.currentDevice.orientation) {
|
||||
UIDeviceOrientation.UIDeviceOrientationPortrait ->
|
||||
AVCaptureVideoOrientationPortrait
|
||||
|
||||
UIDeviceOrientation.UIDeviceOrientationLandscapeLeft ->
|
||||
AVCaptureVideoOrientationLandscapeRight
|
||||
|
||||
UIDeviceOrientation.UIDeviceOrientationLandscapeRight ->
|
||||
AVCaptureVideoOrientationLandscapeLeft
|
||||
|
||||
UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown ->
|
||||
AVCaptureVideoOrientationPortraitUpsideDown
|
||||
|
||||
else -> videoOrientation
|
||||
}
|
||||
}
|
||||
CATransaction.begin()
|
||||
CATransaction.setValue(true, kCATransactionDisableActions)
|
||||
view.layer.setFrame(rect)
|
||||
cameraPreviewLayer.setFrame(rect)
|
||||
CATransaction.commit()
|
||||
},
|
||||
) {
|
||||
val cameraContainer = UIView()
|
||||
cameraContainer.layer.addSublayer(cameraPreviewLayer)
|
||||
cameraPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
|
||||
captureSession.startRunning()
|
||||
cameraContainer
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(20.dp),
|
||||
onClick = {
|
||||
val photoSettings = AVCapturePhotoSettings.photoSettingsWithFormat(
|
||||
format = mapOf(AVVideoCodecKey to AVVideoCodecTypeJPEG)
|
||||
)
|
||||
photoSettings.setHighResolutionPhotoEnabled(true)
|
||||
capturePhotoOutput.setHighResolutionCaptureEnabled(true)
|
||||
capturePhotoOutput.capturePhotoWithSettings(
|
||||
settings = photoSettings,
|
||||
delegate = photoCaptureDelegate
|
||||
)
|
||||
}) {
|
||||
Text("Compose Button - take a photo 📸")
|
||||
}
|
||||
} else {
|
||||
SimulatorStub()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimulatorStub() {
|
||||
Text(
|
||||
"""
|
||||
Camera is not available on simulator.
|
||||
Please try to run on a real iOS device.
|
||||
""".trimIndent(), color = Color.White
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user