Skip to content
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

Add basic Camera sample #382

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ androidx-test = "1.6.1"
androidx-test-espresso = "3.6.1"
androidx-window = "1.3.0"
androidxHiltNavigationCompose = "1.2.0"
cameraCompose = "1.5.0-alpha02"
coil = "2.7.0"
# @keep
compileSdk = "34"
Expand Down Expand Up @@ -50,6 +51,8 @@ targetSdk = "34"
version-catalog-update = "0.8.4"
wearComposeFoundation = "1.4.0"
wearComposeMaterial = "1.4.0"
junitVersion = "1.2.1"
cameraLifecycle = "1.3.4"

[libraries]
accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" }
Expand All @@ -59,6 +62,7 @@ accompanist-theme-adapter-material = { module = "com.google.accompanist:accompan
accompanist-theme-adapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "cameraCompose" }
androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose-latest" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" }
Expand Down Expand Up @@ -127,6 +131,8 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Expand Down
1 change: 1 addition & 0 deletions media/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
66 changes: 66 additions & 0 deletions media/build.gradle.kts
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)
JolandaVerhoef marked this conversation as resolved.
Show resolved Hide resolved
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)
}
21 changes: 21 additions & 0 deletions media/proguard-rules.pro
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
34 changes: 34 additions & 0 deletions media/src/main/AndroidManifest.xml
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>
32 changes: 32 additions & 0 deletions media/src/main/java/com/example/media/MediaSnippetsActivity.kt
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)
}
}
}
}
}
160 changes: 160 additions & 0 deletions media/src/main/java/com/example/media/camera/CameraSnippets.kt
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()
// 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(onClick = { 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]
11 changes: 11 additions & 0 deletions media/src/main/java/com/example/media/ui/theme/Color.kt
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)
Loading