Extract StackBlur and split CameraScreen into smaller files
Move the ~220-line stack blur algorithm from ImageCaptureHandler into its own util/StackBlur.kt object, making it independently testable and reducing ImageCaptureHandler from 759 to 531 lines. Split CameraScreen.kt (873 lines) by extracting: - ControlPanel.kt: ModeToggle, ControlPanel, SliderControl - CaptureControls.kt: CaptureButton, LastPhotoThumbnail CameraScreen.kt is now 609 lines focused on layout and state wiring. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5b9aedd109
commit
88d04515e2
5 changed files with 579 additions and 501 deletions
|
|
@ -1,19 +1,16 @@
|
|||
package no.naiv.tiltshift.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.systemGestureExclusion
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -29,24 +26,21 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.systemGestureExclusion
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FlipCameraAndroid
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material.icons.filled.LocationOff
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
|
|
@ -65,10 +59,9 @@ import androidx.compose.ui.graphics.asImageBitmap
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.LiveRegionMode
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.liveRegion
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -79,7 +72,6 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import no.naiv.tiltshift.effect.BlurMode
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
import no.naiv.tiltshift.effect.TiltShiftRenderer
|
||||
import no.naiv.tiltshift.ui.theme.AppColors
|
||||
|
|
@ -608,265 +600,3 @@ fun CameraScreen(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode toggle for Linear / Radial blur modes.
|
||||
*/
|
||||
@Composable
|
||||
private fun ModeToggle(
|
||||
currentMode: BlurMode,
|
||||
onModeChange: (BlurMode) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(AppColors.OverlayDark)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ModeButton(
|
||||
text = "Linear",
|
||||
isSelected = currentMode == BlurMode.LINEAR,
|
||||
onClick = { onModeChange(BlurMode.LINEAR) }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
ModeButton(
|
||||
text = "Radial",
|
||||
isSelected = currentMode == BlurMode.RADIAL,
|
||||
onClick = { onModeChange(BlurMode.RADIAL) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModeButton(
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(if (isSelected) AppColors.Accent else Color.Transparent)
|
||||
.clickable(role = Role.Button, onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.semantics {
|
||||
stateDescription = if (isSelected) "Selected" else "Not selected"
|
||||
contentDescription = "$text blur mode"
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = if (isSelected) Color.Black else Color.White,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control panel with sliders for blur parameters.
|
||||
* Includes position/size/angle sliders as gesture alternatives for accessibility.
|
||||
*/
|
||||
@Composable
|
||||
private fun ControlPanel(
|
||||
params: BlurParameters,
|
||||
onParamsChange: (BlurParameters) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val currentParams by rememberUpdatedState(params)
|
||||
val currentOnParamsChange by rememberUpdatedState(onParamsChange)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(200.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(AppColors.OverlayDarker)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Reset button
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onReset,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
contentDescription = "Reset all parameters to defaults",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Blur intensity slider
|
||||
SliderControl(
|
||||
label = "Blur",
|
||||
value = params.blurAmount,
|
||||
valueRange = BlurParameters.MIN_BLUR..BlurParameters.MAX_BLUR,
|
||||
formatValue = { "${(it * 100).toInt()}%" },
|
||||
onValueChange = { currentOnParamsChange(currentParams.copy(blurAmount = it)) }
|
||||
)
|
||||
|
||||
// Falloff slider
|
||||
SliderControl(
|
||||
label = "Falloff",
|
||||
value = params.falloff,
|
||||
valueRange = BlurParameters.MIN_FALLOFF..BlurParameters.MAX_FALLOFF,
|
||||
formatValue = { "${(it * 100).toInt()}%" },
|
||||
onValueChange = { currentOnParamsChange(currentParams.copy(falloff = it)) }
|
||||
)
|
||||
|
||||
// Size slider (gesture alternative for pinch-to-resize)
|
||||
SliderControl(
|
||||
label = "Size",
|
||||
value = params.size,
|
||||
valueRange = BlurParameters.MIN_SIZE..BlurParameters.MAX_SIZE,
|
||||
formatValue = { "${(it * 100).toInt()}%" },
|
||||
onValueChange = { currentOnParamsChange(currentParams.copy(size = it)) }
|
||||
)
|
||||
|
||||
// Aspect ratio slider (radial mode only)
|
||||
if (params.mode == BlurMode.RADIAL) {
|
||||
SliderControl(
|
||||
label = "Shape",
|
||||
value = params.aspectRatio,
|
||||
valueRange = BlurParameters.MIN_ASPECT..BlurParameters.MAX_ASPECT,
|
||||
formatValue = { "%.1f:1".format(it) },
|
||||
onValueChange = { currentOnParamsChange(currentParams.copy(aspectRatio = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
// Angle slider (gesture alternative for two-finger rotate)
|
||||
SliderControl(
|
||||
label = "Angle",
|
||||
value = params.angle,
|
||||
valueRange = (-Math.PI.toFloat())..Math.PI.toFloat(),
|
||||
formatValue = { "${(it * 180f / Math.PI.toFloat()).toInt()}°" },
|
||||
onValueChange = { currentOnParamsChange(currentParams.copy(angle = it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SliderControl(
|
||||
label: String,
|
||||
value: Float,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
formatValue: (Float) -> String = { "${(it * 100).toInt()}%" },
|
||||
onValueChange: (Float) -> Unit
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.White,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Text(
|
||||
text = formatValue(value),
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
valueRange = valueRange,
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = AppColors.Accent,
|
||||
activeTrackColor = AppColors.Accent,
|
||||
inactiveTrackColor = Color.White.copy(alpha = 0.3f)
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.semantics { contentDescription = "$label: ${formatValue(value)}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture button with processing indicator.
|
||||
*/
|
||||
@Composable
|
||||
private fun CaptureButton(
|
||||
isCapturing: Boolean,
|
||||
isProcessing: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val outerSize = 72.dp
|
||||
val innerSize = if (isCapturing) 48.dp else 60.dp
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(outerSize)
|
||||
.clip(CircleShape)
|
||||
.border(4.dp, Color.White, CircleShape)
|
||||
.clickable(
|
||||
enabled = !isCapturing,
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
)
|
||||
.semantics {
|
||||
contentDescription = "Capture photo with tilt-shift effect"
|
||||
if (isCapturing) stateDescription = "Processing photo"
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(innerSize)
|
||||
.clip(CircleShape)
|
||||
.background(if (isCapturing) AppColors.Accent else Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isProcessing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.Black,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounded thumbnail of the last captured photo.
|
||||
* Tapping opens the image in the default photo viewer.
|
||||
*/
|
||||
@Composable
|
||||
private fun LastPhotoThumbnail(
|
||||
thumbnail: Bitmap?,
|
||||
onTap: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = thumbnail != null,
|
||||
enter = fadeIn() + scaleIn(initialScale = 0.6f),
|
||||
exit = fadeOut(),
|
||||
modifier = modifier
|
||||
) {
|
||||
thumbnail?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Last captured photo. Tap to open in viewer.",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.size(52.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.border(2.dp, Color.White, RoundedCornerShape(10.dp))
|
||||
.clickable(role = Role.Button, onClick = onTap)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue