Add interactive gallery preview before applying tilt-shift effect
Instead of immediately processing gallery images, show a preview where users can adjust blur parameters before committing. Adds Cancel/Apply buttons and hides camera-only controls during gallery preview mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
780a8ab167
commit
5d80dcfcbe
2 changed files with 228 additions and 119 deletions
|
|
@ -189,6 +189,21 @@ class ImageCaptureHandler(
|
|||
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a gallery image and applies EXIF rotation, returning the bitmap for preview.
|
||||
* The caller owns the returned bitmap and is responsible for recycling it.
|
||||
*/
|
||||
suspend fun loadGalleryImage(imageUri: Uri): Bitmap? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val bitmap = loadBitmapFromUri(imageUri)
|
||||
?: return@withContext null
|
||||
applyExifRotation(imageUri, bitmap)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load gallery image for preview", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an existing image from the gallery through the tilt-shift pipeline.
|
||||
* Loads the image, applies EXIF rotation, processes the effect, and saves both versions.
|
||||
|
|
|
|||
|
|
@ -113,40 +113,31 @@ fun CameraScreen(
|
|||
var lastSavedUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var lastThumbnailBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
|
||||
// Gallery preview mode: non-null means we're previewing a gallery image
|
||||
var galleryBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var galleryImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
val isGalleryPreview = galleryBitmap != null
|
||||
|
||||
var currentRotation by remember { mutableStateOf(Surface.ROTATION_0) }
|
||||
var currentLocation by remember { mutableStateOf<Location?>(null) }
|
||||
|
||||
// Gallery picker: process a selected image through the tilt-shift pipeline
|
||||
// Gallery picker: load image for interactive preview before processing
|
||||
val galleryLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia()
|
||||
) { uri ->
|
||||
if (uri != null && !isCapturing) {
|
||||
isCapturing = true
|
||||
if (uri != null && !isCapturing && !isGalleryPreview) {
|
||||
scope.launch {
|
||||
val result = captureHandler.processExistingImage(
|
||||
imageUri = uri,
|
||||
blurParams = blurParams,
|
||||
location = currentLocation
|
||||
)
|
||||
when (result) {
|
||||
is SaveResult.Success -> {
|
||||
haptics.success()
|
||||
lastThumbnailBitmap?.recycle()
|
||||
lastThumbnailBitmap = result.thumbnail
|
||||
lastSavedUri = result.uri
|
||||
showSaveSuccess = true
|
||||
delay(1500)
|
||||
showSaveSuccess = false
|
||||
}
|
||||
is SaveResult.Error -> {
|
||||
val bitmap = captureHandler.loadGalleryImage(uri)
|
||||
if (bitmap != null) {
|
||||
galleryBitmap = bitmap
|
||||
galleryImageUri = uri
|
||||
} else {
|
||||
haptics.error()
|
||||
showSaveError = result.message
|
||||
showSaveError = "Failed to load image"
|
||||
delay(2000)
|
||||
showSaveError = null
|
||||
}
|
||||
}
|
||||
isCapturing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +196,7 @@ fun CameraScreen(
|
|||
cameraManager.release()
|
||||
renderer?.release()
|
||||
lastThumbnailBitmap?.recycle()
|
||||
galleryBitmap?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,6 +205,19 @@ fun CameraScreen(
|
|||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
) {
|
||||
// Main view: gallery preview image or camera GL surface
|
||||
if (isGalleryPreview) {
|
||||
galleryBitmap?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Gallery preview",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// OpenGL Surface for camera preview with effect
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
|
|
@ -232,6 +237,7 @@ fun CameraScreen(
|
|||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
// Tilt-shift overlay (gesture handling + visualization)
|
||||
TiltShiftOverlay(
|
||||
|
|
@ -241,8 +247,10 @@ fun CameraScreen(
|
|||
haptics.tick()
|
||||
},
|
||||
onZoomChange = { zoomDelta ->
|
||||
if (!isGalleryPreview) {
|
||||
val newZoom = (zoomRatio * zoomDelta).coerceIn(minZoom, maxZoom)
|
||||
cameraManager.setZoom(newZoom)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
|
@ -259,10 +267,15 @@ fun CameraScreen(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (!isGalleryPreview) {
|
||||
// Zoom indicator
|
||||
ZoomIndicator(currentZoom = zoomRatio)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(1.dp))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (!isGalleryPreview) {
|
||||
// Camera flip button
|
||||
IconButton(
|
||||
onClick = {
|
||||
|
|
@ -276,6 +289,7 @@ fun CameraScreen(
|
|||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle controls button
|
||||
IconButton(
|
||||
|
|
@ -336,6 +350,85 @@ fun CameraScreen(
|
|||
.systemGestureExclusion(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (isGalleryPreview) {
|
||||
// Gallery preview mode: Cancel | Apply
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(48.dp)
|
||||
) {
|
||||
// Cancel button
|
||||
IconButton(
|
||||
onClick = {
|
||||
galleryBitmap?.recycle()
|
||||
galleryBitmap = null
|
||||
galleryImageUri = null
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0x80000000))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Cancel",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Apply button
|
||||
IconButton(
|
||||
onClick = {
|
||||
val uri = galleryImageUri ?: return@IconButton
|
||||
if (!isCapturing) {
|
||||
isCapturing = true
|
||||
haptics.heavyClick()
|
||||
scope.launch {
|
||||
val result = captureHandler.processExistingImage(
|
||||
imageUri = uri,
|
||||
blurParams = blurParams,
|
||||
location = currentLocation
|
||||
)
|
||||
when (result) {
|
||||
is SaveResult.Success -> {
|
||||
haptics.success()
|
||||
lastThumbnailBitmap?.recycle()
|
||||
lastThumbnailBitmap = result.thumbnail
|
||||
lastSavedUri = result.uri
|
||||
showSaveSuccess = true
|
||||
delay(1500)
|
||||
showSaveSuccess = false
|
||||
}
|
||||
is SaveResult.Error -> {
|
||||
haptics.error()
|
||||
showSaveError = result.message
|
||||
delay(2000)
|
||||
showSaveError = null
|
||||
}
|
||||
}
|
||||
galleryBitmap?.recycle()
|
||||
galleryBitmap = null
|
||||
galleryImageUri = null
|
||||
isCapturing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isCapturing,
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFFFB300))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Apply effect",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Camera mode: Zoom presets + Gallery | Capture | Spacer
|
||||
// Zoom presets (only show for back camera)
|
||||
if (!isFrontCamera) {
|
||||
ZoomControl(
|
||||
|
|
@ -424,9 +517,10 @@ fun CameraScreen(
|
|||
Spacer(modifier = Modifier.size(52.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last captured photo thumbnail
|
||||
LastPhotoThumbnail(
|
||||
// Last captured photo thumbnail (hidden in gallery preview mode)
|
||||
if (!isGalleryPreview) LastPhotoThumbnail(
|
||||
thumbnail = lastThumbnailBitmap,
|
||||
onTap = {
|
||||
lastSavedUri?.let { uri ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue