- Camerafunktionalität dokumentiert

- Aufräumarbeiten
This commit is contained in:
2026-02-09 22:20:59 +01:00
parent 7d6bf0fdd7
commit 48802606e8
10 changed files with 162 additions and 262 deletions

View File

@@ -1,5 +1,6 @@
package com.example.snapandsolve
import MainScreen
import android.os.Bundle
import androidx.activity.ComponentActivity

View File

@@ -1,119 +0,0 @@
package com.example.snapandsolve.camera
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.content.FileProvider
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.snapandsolve.BuildConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import kotlin.coroutines.CoroutineContext
/**
* This variant inherits from [AndroidViewModel] and has access to the application context
*/
class AlbumAndroidViewModel(private val appContext: Application,
private val coroutineContext: CoroutineContext
): AndroidViewModel(appContext) {
//region View State
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(
AlbumViewState()
)
val viewStateFlow: StateFlow<AlbumViewState>
get() = _albumViewState
//endregion
fun onEvent(intent: Intent) = viewModelScope.launch(coroutineContext) {
when(intent) {
is Intent.OnPermissionGranted -> {
// Create an empty image file in the app's cache directory
val file = File.createTempFile(
"temp_image_file_", /* prefix */
".jpg", /* suffix */
appContext.cacheDir /* cache directory */
)
// Create sandboxed url for this temp file - needed for the camera API
val uri = FileProvider.getUriForFile(appContext,
"${BuildConfig.APPLICATION_ID}.provider",
file
)
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
}
is Intent.OnPermissionDenied -> {
// maybe log the permission denial event
println("User did not grant permission to use the camera")
}
is Intent.OnFinishPickingImages -> {
if (intent.imageUrls.isNotEmpty()) {
// Handle picked images
val newImages = mutableListOf<ImageBitmap>()
for (eachImageUrl in intent.imageUrls) {
val inputStream = appContext.contentResolver.openInputStream(eachImageUrl)
val bytes = inputStream?.readBytes()
inputStream?.close()
if (bytes != null) {
val bitmapOptions = BitmapFactory.Options()
bitmapOptions.inMutable = true
val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
val imageBitmap = bitmap.asImageBitmap()
newImages.add(imageBitmap)
} else {
// error reading the bytes from the image url
println("The image that was picked could not be read from the device at this url: $eachImageUrl")
}
}
val currentViewState = _albumViewState.value
val newCopy = currentViewState.copy(
selectedPictures = (currentViewState.selectedPictures + newImages),
tempFileUrl = null
)
_albumViewState.value = newCopy
} else {
// user did not pick anything
}
}
is Intent.OnImageSaved -> {
val tempImageUrl = _albumViewState.value.tempFileUrl
if (tempImageUrl != null) {
val source: ImageDecoder.Source = ImageDecoder.createSource(appContext.contentResolver, tempImageUrl)
val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null,
selectedPictures = currentPictures)
}
}
is Intent.OnImageSavingCanceled -> {
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
}
is Intent.OnFinishPickingImagesWith -> {
// unnecessary in this viewmodel variant
}
is Intent.OnPermissionGrantedWith -> {
// unnecessary in this viewmodel variant
}
is Intent.OnImageSavedWith -> {
// unnecessary in this viewmodel variant
}
}
}
}

View File

@@ -1,28 +1,39 @@
package com.example.snapandsolve.camera
import android.content.Context
import android.net.Uri
/*
Das sind die Funktionen beim Drücken der Knöpfe. Übersichtlicher wäre es sie direkt mit den
Knöpfen in der ViewModel zu platzieren. AlbumEvents als name ist vielleicht unglücklich gewählt.
*/
/**
* User generated events that can be triggered from the UI.
* Benutzer-Aktionen für das Album/Kamera-System.
*/
sealed class Intent {
data object OnPermissionGranted: Intent()
/**
* Kamera-Permission wurde erteilt.
* @param compositionContext Android Context für Dateizugriff
*/
data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
/**
* Kamera-Permission wurde verweigert.
*/
data object OnPermissionDenied: Intent()
data object OnImageSaved: Intent()
data class OnImageSavedWith (val compositionContext: Context): Intent()
/**
* Kamera-Aufnahme wurde gespeichert.
* @param compositionContext Android Context für Dateizugriff
*/
data class OnImageSavedWith(val compositionContext: Context): Intent()
/**
* Kamera-Aufnahme wurde abgebrochen.
*/
data object OnImageSavingCanceled: Intent()
data class OnFinishPickingImages(val imageUrls: List<Uri>): Intent()
data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List<Uri>): Intent()
}
/**
* Bilder aus Galerie wurden ausgewählt.
* @param compositionContext Android Context für Dateizugriff
* @param imageUrls Liste der ausgewählten Bild-URIs
*/
data class OnFinishPickingImagesWith(
val compositionContext: Context,
val imageUrls: List<Uri>
): Intent()
}

View File

@@ -1,6 +1,6 @@
package com.example.snapandsolve.camera
import Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
@@ -10,27 +10,35 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.snapandsolve.BuildConfig
import com.example.snapandsolve.camera.AlbumViewState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import kotlin.coroutines.CoroutineContext
class AlbumViewModel(private val coroutineContext: CoroutineContext
): ViewModel() {
/**
* ViewModel für Bild-Verwaltung (Kamera + Galerie).
* Verwaltet ausgewählte Bilder und temporäre Kamera-Dateien.
*/
class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel() {
//region View State
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(
AlbumViewState()
)
/**
* Observable State für UI-Komponenten.
*/
val viewStateFlow: StateFlow<AlbumViewState>
get() = _albumViewState
//endregion
// region Intents
/**
* Verarbeitet Benutzer-Aktionen.
* @param intent Die zu verarbeitende Aktion
*/
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
when(intent) {
when (intent) {
is Intent.OnPermissionGrantedWith -> {
println("DEBUG: OnPermissionGrantedWith empfangen")
val tempFile = File.createTempFile(
@@ -40,7 +48,8 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
)
println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}")
val uri = FileProvider.getUriForFile(intent.compositionContext,
val uri = FileProvider.getUriForFile(
intent.compositionContext,
"${BuildConfig.APPLICATION_ID}.provider",
tempFile
)
@@ -50,13 +59,11 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
}
is Intent.OnPermissionDenied -> {
// maybe log the permission denial event
println("User did not grant permission to use the camera")
}
is Intent.OnFinishPickingImagesWith -> {
if (intent.imageUrls.isNotEmpty()) {
// Handle picked images
val newImages = mutableListOf<ImageBitmap>()
for (eachImageUrl in intent.imageUrls) {
val inputStream = intent.compositionContext.contentResolver.openInputStream(eachImageUrl)
@@ -69,7 +76,6 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
newImages.add(bitmap.asImageBitmap())
} else {
// error reading the bytes from the image url
println("The image that was picked could not be read from the device at this url: $eachImageUrl")
}
}
@@ -80,42 +86,36 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
tempFileUrl = null
)
_albumViewState.value = newCopy
} else {
// user did not pick anything
}
}
is Intent.OnImageSavedWith -> {
val tempImageUrl = _albumViewState.value.tempFileUrl
if (tempImageUrl != null) {
val source = ImageDecoder.createSource(intent.compositionContext.contentResolver, tempImageUrl)
val source = ImageDecoder.createSource(
intent.compositionContext.contentResolver,
tempImageUrl
)
val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null,
selectedPictures = currentPictures)
_albumViewState.value = _albumViewState.value.copy(
tempFileUrl = null,
selectedPictures = currentPictures
)
}
}
is Intent.OnImageSavingCanceled -> {
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
}
is Intent.OnPermissionGranted -> {
// unnecessary in this viewmodel variant
}
is Intent.OnFinishPickingImages -> {
// unnecessary in this viewmodel variant
}
is Intent.OnImageSaved -> {
// unnecessary in this viewmodel variant
}
}
}
// endregion
/**
* Löscht alle ausgewählten Bilder aus dem State.
*/
fun clearSelection() {
_albumViewState.value = _albumViewState.value.copy(selectedPictures = emptyList())
}

View File

@@ -1,96 +0,0 @@
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
package com.example.snapandsolve.ui.theme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
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
import androidx.compose.ui.unit.dp
@Composable
fun OverlayShell(
title: String,
footer: @Composable RowScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.25f)),
contentAlignment = Alignment.Center
) {
BoxWithConstraints {
val verticalMargin = 50.dp
val maxCardHeight = maxHeight - verticalMargin * 2
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.heightIn(min = 400.dp, max = maxCardHeight),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = WidgetColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = Color.Black
)
}
Spacer(Modifier.height(12.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = true)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content
)
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
content = footer
)
}
}
}
}
}

View File

@@ -26,7 +26,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.composable.AppButton
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
import com.example.snapandsolve.ui.theme.composable.DialogContainer