-
Notifications
You must be signed in to change notification settings - Fork 227
Add basic Camera sample #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3bcd03f
21b334e
6fa378b
a09c702
0cda32c
97e0c10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
plugins { | ||
alias(libs.plugins.android.application) | ||
alias(libs.plugins.kotlin.android) | ||
alias(libs.plugins.compose.compiler) | ||
} | ||
|
||
android { | ||
namespace = "com.example.media" | ||
compileSdk = 35 | ||
|
||
defaultConfig { | ||
applicationId = "com.example.media" | ||
minSdk = 24 | ||
targetSdk = 34 | ||
versionCode = 1 | ||
versionName = "1.0" | ||
|
||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
|
||
buildTypes { | ||
release { | ||
isMinifyEnabled = false | ||
proguardFiles( | ||
getDefaultProguardFile("proguard-android-optimize.txt"), | ||
"proguard-rules.pro" | ||
) | ||
} | ||
} | ||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_11 | ||
targetCompatibility = JavaVersion.VERSION_11 | ||
} | ||
kotlinOptions { | ||
jvmTarget = "11" | ||
} | ||
buildFeatures { | ||
compose = true | ||
} | ||
} | ||
|
||
dependencies { | ||
|
||
// Add CameraX Compose dependency | ||
implementation(libs.androidx.camera.compose) | ||
implementation("androidx.camera:camera-core:1.3.4") | ||
implementation("androidx.camera:camera-camera2:1.3.4") | ||
|
||
implementation(libs.accompanist.permissions) | ||
implementation(libs.androidx.core.ktx) | ||
implementation(libs.androidx.lifecycle.runtime) | ||
implementation(libs.androidx.activity.compose) | ||
implementation(platform(libs.androidx.compose.bom)) | ||
implementation(libs.androidx.compose.ui) | ||
implementation(libs.androidx.compose.ui.graphics) | ||
implementation(libs.androidx.compose.ui.tooling.preview) | ||
implementation(libs.androidx.compose.material3) | ||
implementation(libs.androidx.camera.lifecycle) | ||
testImplementation(libs.junit) | ||
androidTestImplementation(libs.androidx.junit) | ||
androidTestImplementation(libs.androidx.test.espresso.core) | ||
androidTestImplementation(platform(libs.androidx.compose.bom)) | ||
androidTestImplementation(libs.androidx.compose.ui.test.junit4) | ||
debugImplementation(libs.androidx.compose.ui.tooling) | ||
debugImplementation(libs.androidx.compose.ui.test.manifest) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
JolandaVerhoef marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<uses-feature | ||
android:name="android.hardware.camera" | ||
android:required="true" /> | ||
<uses-feature | ||
android:name="android.hardware.camera.autofocus" | ||
android:required="false" /> | ||
|
||
<uses-permission android:name="android.permission.CAMERA" /> | ||
|
||
<application | ||
android:allowBackup="true" | ||
android:icon="@mipmap/ic_launcher" | ||
android:label="@string/app_name" | ||
android:roundIcon="@mipmap/ic_launcher_round" | ||
android:supportsRtl="true" | ||
android:theme="@style/Theme.Snippets"> | ||
<activity | ||
android:name=".MediaSnippetsActivity" | ||
android:exported="true" | ||
android:label="@string/app_name" | ||
android:theme="@style/Theme.Snippets"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
|
||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
|
||
</manifest> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.example.media | ||
|
||
import android.annotation.SuppressLint | ||
import android.os.Bundle | ||
import androidx.activity.ComponentActivity | ||
import androidx.activity.compose.setContent | ||
import androidx.activity.enableEdgeToEdge | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import com.example.media.camera.CameraPreviewScreen | ||
import com.example.media.camera.CameraPreviewViewModel | ||
import com.example.media.ui.theme.SnippetsTheme | ||
|
||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") | ||
class MediaSnippetsActivity : ComponentActivity() { | ||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
enableEdgeToEdge() | ||
setContent { | ||
SnippetsTheme { | ||
Scaffold(modifier = Modifier.fillMaxSize()) { _ -> | ||
val context = LocalContext.current | ||
val viewModel = remember { CameraPreviewViewModel(context.applicationContext) } | ||
CameraPreviewScreen(viewModel) | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package com.example.media.camera | ||
|
||
import android.content.Context | ||
import androidx.camera.compose.CameraXViewfinder | ||
import androidx.camera.core.CameraSelector | ||
import androidx.camera.core.ImageCapture | ||
import androidx.camera.core.Preview | ||
import androidx.camera.core.SurfaceRequest | ||
import androidx.camera.core.UseCaseGroup | ||
import androidx.camera.core.takePicture | ||
import androidx.camera.lifecycle.ProcessCameraProvider | ||
import androidx.camera.lifecycle.awaitInstance | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Spacer | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.layout.safeContentPadding | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material3.Button | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
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.unit.dp | ||
import androidx.lifecycle.LifecycleOwner | ||
import androidx.lifecycle.ViewModel | ||
import androidx.lifecycle.compose.LifecycleStartEffect | ||
import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
import androidx.lifecycle.viewModelScope | ||
import com.google.accompanist.permissions.ExperimentalPermissionsApi | ||
import com.google.accompanist.permissions.isGranted | ||
import com.google.accompanist.permissions.rememberPermissionState | ||
import com.google.accompanist.permissions.shouldShowRationale | ||
import kotlinx.coroutines.Job | ||
import kotlinx.coroutines.awaitCancellation | ||
import kotlinx.coroutines.cancelAndJoin | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.launch | ||
|
||
// [START android_media_camera_preview_viewmodel] | ||
class CameraPreviewViewModel(private val appContext: Context) : ViewModel() { | ||
private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null) | ||
val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest | ||
|
||
private val previewUseCase = Preview.Builder().build().apply { | ||
setSurfaceProvider { newSurfaceRequest -> | ||
_surfaceRequest.value = newSurfaceRequest | ||
} | ||
} | ||
private val captureUseCase = ImageCapture.Builder().build() | ||
private val useCaseGroup = UseCaseGroup.Builder().apply { | ||
addUseCase(previewUseCase) | ||
addUseCase(captureUseCase) | ||
}.build() | ||
|
||
private var runningCameraJob: Job? = null | ||
|
||
fun startCamera(lifecycleOwner: LifecycleOwner) { | ||
viewModelScope.launch { | ||
runningCameraJob?.cancelAndJoin() | ||
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext) | ||
processCameraProvider.bindToLifecycle( | ||
lifecycleOwner, | ||
CameraSelector.DEFAULT_BACK_CAMERA, | ||
useCaseGroup | ||
) | ||
|
||
try { | ||
awaitCancellation() | ||
} finally { | ||
processCameraProvider.unbindAll() | ||
} | ||
}.also { runningCameraJob = it } | ||
} | ||
|
||
fun stopCamera() { | ||
runningCameraJob?.cancel() | ||
} | ||
|
||
fun takePicture() { | ||
viewModelScope.launch { | ||
val imageProxy = captureUseCase.takePicture() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take picture doesn't compile without parameters passed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't reproduce on my side. Code works well both with camerax-compose alpha02 and alpha06 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an extension function, so make sure you |
||
// Do something with the image | ||
} | ||
} | ||
} | ||
// [END android_media_camera_preview_viewmodel] | ||
|
||
// [START android_media_camera_preview_screen] | ||
@OptIn(ExperimentalPermissionsApi::class) | ||
@Composable | ||
fun CameraPreviewScreen( | ||
viewModel: CameraPreviewViewModel, | ||
modifier: Modifier = Modifier | ||
) { | ||
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA) | ||
if (cameraPermissionState.status.isGranted) { | ||
CameraPreviewContent(viewModel, modifier) | ||
} else { | ||
Column(modifier.safeContentPadding()) { | ||
val textToShow = if (cameraPermissionState.status.shouldShowRationale) { | ||
// If the user has denied the permission but the rationale can be shown, | ||
// then gently explain why the app requires this permission | ||
"The camera is important for this app. Please grant the permission." | ||
} else { | ||
// If it's the first time the user lands on this feature, or the user | ||
// doesn't want to be asked again for this permission, explain that the | ||
// permission is required | ||
"Camera permission required for this feature to be available. " + | ||
"Please grant the permission" | ||
} | ||
Text(textToShow) | ||
Button( cameraPermissionState.launchPermissionRequest() }) { | ||
Text("Request permission") | ||
} | ||
} | ||
} | ||
} | ||
// [END android_media_camera_preview_screen] | ||
|
||
// [START android_media_camera_preview_content] | ||
@Composable | ||
fun CameraPreviewContent( | ||
viewModel: CameraPreviewViewModel, | ||
modifier: Modifier = Modifier | ||
) { | ||
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() | ||
|
||
LifecycleStartEffect(Unit) { | ||
viewModel.startCamera(this) | ||
onStopOrDispose { | ||
viewModel.stopCamera() | ||
} | ||
} | ||
|
||
surfaceRequest?.let { | ||
Box(modifier) { | ||
CameraXViewfinder(it, Modifier.fillMaxSize()) | ||
Spacer( | ||
Modifier | ||
.safeContentPadding() | ||
.padding(bottom = 16.dp) | ||
.size(64.dp) | ||
.clip(CircleShape) | ||
.clickable { viewModel.takePicture() } | ||
.background(Color.White) | ||
.align(Alignment.BottomCenter) | ||
) | ||
} | ||
} | ||
} | ||
// [END android_media_camera_preview_content] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.example.media.ui.theme | ||
|
||
import androidx.compose.ui.graphics.Color | ||
|
||
val Purple80 = Color(0xFFD0BCFF) | ||
val PurpleGrey80 = Color(0xFFCCC2DC) | ||
val Pink80 = Color(0xFFEFB8C8) | ||
|
||
val Purple40 = Color(0xFF6650a4) | ||
val PurpleGrey40 = Color(0xFF625b71) | ||
val Pink40 = Color(0xFF7D5260) |
Uh oh!
There was an error while loading. Please reload this page.