Kamera funktionalität
-AlbumAndroidViewModel -AlbumEvents -AlbumViewModel -AlbumViewState
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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.
|
||||||
|
*/
|
||||||
|
sealed class Intent {
|
||||||
|
data object OnPermissionGranted: Intent()
|
||||||
|
|
||||||
|
data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
|
||||||
|
|
||||||
|
data object OnPermissionDenied: Intent()
|
||||||
|
|
||||||
|
data object OnImageSaved: Intent()
|
||||||
|
|
||||||
|
data class OnImageSavedWith (val compositionContext: Context): Intent()
|
||||||
|
|
||||||
|
data object OnImageSavingCanceled: Intent()
|
||||||
|
|
||||||
|
data class OnFinishPickingImages(val imageUrls: List<Uri>): Intent()
|
||||||
|
|
||||||
|
data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List<Uri>): Intent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.example.snapandsolve.camera
|
||||||
|
|
||||||
|
|
||||||
|
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.ViewModel
|
||||||
|
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
|
||||||
|
|
||||||
|
class AlbumViewModel(private val coroutineContext: CoroutineContext
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
//region View State
|
||||||
|
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(
|
||||||
|
AlbumViewState()
|
||||||
|
)
|
||||||
|
val viewStateFlow: StateFlow<AlbumViewState>
|
||||||
|
get() = _albumViewState
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
// region Intents
|
||||||
|
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
|
||||||
|
when(intent) {
|
||||||
|
is Intent.OnPermissionGrantedWith -> {
|
||||||
|
// Create an empty image file in the app's cache directory
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
"temp_image_file_", /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
intent.compositionContext.cacheDir /* cache directory */
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create sandboxed url for this temp file - needed for the camera API
|
||||||
|
val uri = FileProvider.getUriForFile(intent.compositionContext,
|
||||||
|
"${BuildConfig.APPLICATION_ID}.provider", /* needs to match the provider information in the manifest */
|
||||||
|
tempFile
|
||||||
|
)
|
||||||
|
_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.OnFinishPickingImagesWith -> {
|
||||||
|
if (intent.imageUrls.isNotEmpty()) {
|
||||||
|
// Handle picked images
|
||||||
|
val newImages = mutableListOf<ImageBitmap>()
|
||||||
|
for (eachImageUrl in intent.imageUrls) {
|
||||||
|
val inputStream = intent.compositionContext.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)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.OnImageSavedWith -> {
|
||||||
|
val tempImageUrl = _albumViewState.value.tempFileUrl
|
||||||
|
if (tempImageUrl != null) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.example.snapandsolve.camera
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds state data for the MainScreen composable.
|
||||||
|
*/
|
||||||
|
data class AlbumViewState(
|
||||||
|
/**
|
||||||
|
* holds the URL of the temporary file which stores the image taken by the camera.
|
||||||
|
*/
|
||||||
|
val tempFileUrl: Uri? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* holds the list of images taken by camera or selected pictures from the gallery.
|
||||||
|
*/
|
||||||
|
val selectedPictures: List<ImageBitmap> = emptyList(),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user