Browse Source

Image Viewer Camera (#2864)

pull/2884/head
Vladimir Mazunin 2 years ago committed by GitHub
parent
commit
b94ae4178e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      experimental/examples/imageviewer/gradle.properties
  2. 4
      experimental/examples/imageviewer/iosApp/iosApp/Info.plist
  3. 1
      experimental/examples/imageviewer/settings.gradle.kts
  4. 21
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt
  5. 7
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
  6. 30
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt
  7. 7
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraView.common.kt
  8. 8
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CircularButton.kt
  9. 38
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt
  10. 2
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt
  11. 2
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/TopLayout.kt
  12. 21
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/CameraView.desktop.kt
  13. 176
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/CameraView.ios.kt

2
experimental/examples/imageviewer/gradle.properties

@ -13,5 +13,5 @@ kotlin.native.useEmbeddableCompilerJar=true
kotlin.native.binary.memoryModel=experimental kotlin.native.binary.memoryModel=experimental
kotlin.version=1.8.0 kotlin.version=1.8.0
agp.version=7.1.3 agp.version=7.1.3
compose.version=1.4.0-alpha01-dev972 compose.version=1.4.0-alpha01-dev975
ktor.version=2.2.1 ktor.version=2.2.1

4
experimental/examples/imageviewer/iosApp/iosApp/Info.plist

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses location data to show taken photos on a map</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@ -20,6 +22,8 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>This app uses camera for capturing photos</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

1
experimental/examples/imageviewer/settings.gradle.kts

@ -3,6 +3,7 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google() google()
mavenLocal()
} }
plugins { plugins {

21
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt

@ -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)
)
}
}

7
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt

@ -102,9 +102,10 @@ internal fun ImageViewerCommon(
} }
is CameraPage -> { is CameraPage -> {
CameraScreen(onBack = { CameraScreen(
navigationStack.back() localization = dependencies.localization,
}) onBack = { navigationStack.back() }
)
} }
} }
} }

30
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt

@ -1,19 +1,27 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.foundation.background import androidx.compose.foundation.layout.*
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import example.imageviewer.Localization
import androidx.compose.ui.text.style.TextAlign import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun CameraScreen(onBack: () -> Unit) { internal fun CameraScreen(localization: Localization, onBack: () -> Unit) {
Box(Modifier.fillMaxSize().background(Color.Black).clickable { onBack() }, contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize()) {
Text("Nothing here yet 📸", textAlign = TextAlign.Center, color = Color.White) CameraView(Modifier.fillMaxSize())
TopLayout(
alignLeftContent = {
Tooltip(localization.back) {
CircularButton(
painterResource("arrowleft.png"),
onClick = { onBack() }
)
}
},
alignRightContent = {},
)
} }
} }

7
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraView.common.kt

@ -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)

8
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CircularButton.kt

@ -15,9 +15,13 @@ import androidx.compose.ui.unit.dp
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
@Composable @Composable
internal fun CircularButton(image: Painter, onClick: () -> Unit) { internal fun CircularButton(
image: Painter,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Box( Box(
Modifier.size(40.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack) modifier.size(50.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
.clickable { onClick() }, contentAlignment = Alignment.Center .clickable { onClick() }, contentAlignment = Alignment.Center
) { ) {
Image( Image(

38
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt

@ -11,10 +11,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -89,7 +86,11 @@ internal fun GalleryScreen(
onFullScreen = { onClickPreviewPicture(it) } onFullScreen = { onClickPreviewPicture(it) }
) )
} }
MakeNewMemoryMiniature(onMakeNewMemory) CircularButton(
image = painterResource("plus.png"),
modifier = Modifier.align(Alignment.BottomCenter).padding(48.dp),
onClick = onMakeNewMemory,
)
} }
} }
if (pictures.isEmpty()) { 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) @OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) { internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) {

2
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt

@ -155,7 +155,7 @@ internal fun BoxScope.MagicButtonOverlay(onClick: () -> Unit) {
Column( Column(
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 12.dp, bottom = 16.dp) modifier = Modifier.align(Alignment.BottomEnd).padding(end = 12.dp, bottom = 16.dp)
) { ) {
CircularButton(painterResource("magic.png"), onClick) CircularButton(painterResource("magic.png"), onClick = onClick)
} }
} }

2
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/TopLayout.kt

@ -19,7 +19,7 @@ internal fun TopLayout(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.notchPadding() .notchPadding()
.padding(horizontal = 12.dp) .padding(12.dp)
) { ) {
Row(Modifier.align(Alignment.CenterStart)) { Row(Modifier.align(Alignment.CenterStart)) {
alignLeftContent() alignLeftContent()

21
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/CameraView.desktop.kt

@ -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)
)
}
}

176
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/CameraView.ios.kt

@ -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
)
}
Loading…
Cancel
Save