diff --git a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt index 972a1da..04cc1dd 100644 --- a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt +++ b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt @@ -327,6 +327,15 @@ class ImageCaptureHandler( return rotated } + /** + * Applies tilt-shift effect to a bitmap for real-time preview. + * Runs on [Dispatchers.IO]. The caller owns the returned bitmap. + */ + suspend fun applyTiltShiftPreview(source: Bitmap, params: BlurParameters): Bitmap = + withContext(Dispatchers.IO) { + applyTiltShiftEffect(source, params) + } + /** * Applies tilt-shift blur effect to a bitmap. * Supports both linear and radial modes. diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt index f613107..7c2838e 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt @@ -106,8 +106,8 @@ fun CameraScreen( val showControls by viewModel.showControls.collectAsState() val lastSavedUri by viewModel.lastSavedUri.collectAsState() val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState() - val galleryBitmap by viewModel.galleryBitmap.collectAsState() - val isGalleryPreview = galleryBitmap != null + val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState() + val isGalleryPreview = galleryPreviewBitmap != null val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState() val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState() @@ -179,10 +179,10 @@ fun CameraScreen( ) { // Main view: gallery preview image or camera GL surface if (isGalleryPreview) { - galleryBitmap?.let { bmp -> + galleryPreviewBitmap?.let { bmp -> Image( bitmap = bmp.asImageBitmap(), - contentDescription = "Gallery image preview. Adjust tilt-shift parameters then tap Apply.", + contentDescription = "Gallery image preview with tilt-shift effect. Adjust parameters then tap Apply.", contentScale = ContentScale.Fit, modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt index 89b61b1..cc9eef8 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt @@ -8,10 +8,14 @@ import android.util.Log import android.view.Surface import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import no.naiv.tiltshift.camera.CameraManager import no.naiv.tiltshift.camera.ImageCaptureHandler import no.naiv.tiltshift.effect.BlurParameters @@ -29,6 +33,8 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) companion object { private const val TAG = "CameraViewModel" + /** Max dimension for the preview source bitmap to keep effect computation fast. */ + private const val PREVIEW_MAX_DIMENSION = 1024 } val cameraManager = CameraManager(application) @@ -64,11 +70,19 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) // Gallery preview state private val _galleryBitmap = MutableStateFlow(null) - val galleryBitmap: StateFlow = _galleryBitmap.asStateFlow() private val _galleryImageUri = MutableStateFlow(null) val galleryImageUri: StateFlow = _galleryImageUri.asStateFlow() + /** Downscaled source for fast preview recomputation. */ + private var galleryPreviewSource: Bitmap? = null + + /** Processed preview bitmap shown in the UI. */ + private val _galleryPreviewBitmap = MutableStateFlow(null) + val galleryPreviewBitmap: StateFlow = _galleryPreviewBitmap.asStateFlow() + + private var previewJob: Job? = null + val isGalleryPreview: Boolean get() = _galleryBitmap.value != null // Device state @@ -111,6 +125,24 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) if (bitmap != null) { _galleryBitmap.value = bitmap _galleryImageUri.value = uri + + // Create downscaled source for fast preview recomputation + galleryPreviewSource = withContext(Dispatchers.IO) { + val maxDim = maxOf(bitmap.width, bitmap.height) + if (maxDim > PREVIEW_MAX_DIMENSION) { + val scale = PREVIEW_MAX_DIMENSION.toFloat() / maxDim + Bitmap.createScaledBitmap( + bitmap, + (bitmap.width * scale).toInt(), + (bitmap.height * scale).toInt(), + true + ) + } else { + bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) + } + } + + startPreviewLoop() } else { haptics.error() showError("Failed to load image") @@ -118,11 +150,38 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) } } + /** Reactively recomputes the tilt-shift preview when blur params change. */ + private fun startPreviewLoop() { + previewJob?.cancel() + previewJob = viewModelScope.launch { + _blurParams.collectLatest { params -> + val source = galleryPreviewSource ?: return@collectLatest + try { + val processed = captureHandler.applyTiltShiftPreview(source, params) + val old = _galleryPreviewBitmap.value + _galleryPreviewBitmap.value = processed + old?.recycle() + } catch (e: Exception) { + Log.w(TAG, "Preview computation failed", e) + } + } + } + } + fun cancelGalleryPreview() { - val old = _galleryBitmap.value + previewJob?.cancel() + previewJob = null + + val oldGallery = _galleryBitmap.value + val oldPreview = _galleryPreviewBitmap.value _galleryBitmap.value = null _galleryImageUri.value = null - old?.recycle() + _galleryPreviewBitmap.value = null + + oldGallery?.recycle() + oldPreview?.recycle() + galleryPreviewSource?.recycle() + galleryPreviewSource = null } fun applyGalleryEffect() { @@ -205,5 +264,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) cameraManager.release() _lastThumbnailBitmap.value?.recycle() _galleryBitmap.value?.recycle() + _galleryPreviewBitmap.value?.recycle() + galleryPreviewSource?.recycle() } }