@@ -6,7 +6,11 @@ import android.content.Context
66import android.net.Uri
77import android.provider.MediaStore
88import android.util.Log
9+ import android.util.Size
910import androidx.camera.core.*
11+ import androidx.camera.core.resolutionselector.AspectRatioStrategy
12+ import androidx.camera.core.resolutionselector.ResolutionSelector
13+ import androidx.camera.core.resolutionselector.ResolutionStrategy
1014import androidx.camera.video.Quality
1115import androidx.camera.video.QualitySelector
1216import androidx.camera.video.Recorder
@@ -17,22 +21,21 @@ import androidx.lifecycle.ViewModel
1721import androidx.lifecycle.ViewModelProvider
1822import co.stonephone.stonecamera.utils.PrefStateDelegate
1923import co.stonephone.stonecamera.utils.StoneCameraInfo
24+ import co.stonephone.stonecamera.utils.getLargestMatchingSize
25+ import co.stonephone.stonecamera.utils.getLargestSensorSize
2026import co.stonephone.stonecamera.utils.selectCameraForStepZoomLevel
2127
22- class StoneCameraViewModel (context : Context ) : ViewModel() {
28+ class StoneCameraViewModel (private val context : Context ) : ViewModel() {
2329
2430 private val ZOOM_CANCEL_THRESHOLD = 0.1f
2531
26- // --------------------------------------------------------------------------------
27- // Core CameraX use-cases (built once and shared)
28- // --------------------------------------------------------------------------------
29- val imageCapture: ImageCapture = ImageCapture .Builder ().build()
32+ val supportedAspectRatios = listOf (" 4:3" , " 16:9" , " FULL" )
3033
31- val recorder: Recorder = Recorder .Builder ()
32- .setQualitySelector(QualitySelector .from(Quality .HD ))
33- .build()
34+ var selectedAspectRatio by PrefStateDelegate (context, " aspect_ratio" , " 16:9" )
35+ private set
3436
35- val videoCapture: VideoCapture <Recorder > = VideoCapture .withOutput(recorder)
37+ var flashMode by PrefStateDelegate (context, " flash_mode" , " OFF" )
38+ private set
3639
3740 // --------------------------------------------------------------------------------
3841 // Mutable state that drives the UI
@@ -72,6 +75,30 @@ class StoneCameraViewModel(context: Context) : ViewModel() {
7275 var facingCameras by mutableStateOf(emptyList<StoneCameraInfo >())
7376 private set
7477
78+ // --------------------------------------------------------------------------------
79+ // Core CameraX use-cases (built once and shared)
80+ // --------------------------------------------------------------------------------
81+ var preview: Preview = createPreview(selectedAspectRatio)
82+ private set
83+
84+ var imageCapture: ImageCapture = createImageCapture(selectedAspectRatio)
85+
86+ val recorder: Recorder = Recorder .Builder ()
87+ .setQualitySelector(QualitySelector .from(Quality .HD ))
88+ .build()
89+
90+ val videoCapture: VideoCapture <Recorder > = VideoCapture .withOutput(recorder)
91+
92+
93+ // --------------------------------------------------------------------------------
94+ // Init
95+ // --------------------------------------------------------------------------------
96+ init {
97+ // Rebuild use-cases to match our persisted prefs
98+ recreateUseCases()
99+ }
100+
101+
75102 // --------------------------------------------------------------------------------
76103 // Public methods to manipulate the above states
77104 // --------------------------------------------------------------------------------
@@ -232,30 +259,159 @@ class StoneCameraViewModel(context: Context) : ViewModel() {
232259 return ((brightnessLevel + 1.0f ) / 2.0f * (maxIndex - minIndex) + minIndex).toInt()
233260 }
234261
235- // --------------------------------------------------------------------------------
236- // Mutable state for Flash Mode
237- // --------------------------------------------------------------------------------
238- var flashMode by PrefStateDelegate (context, " flash_mode" , " OFF" )
239- private set
240-
241262 /* *
242- * Set the flash mode for the camera.
243- * Supported modes: ON, OFF, AUTO.
244- * @param mode A string representing the flash mode ("ON", "OFF", "AUTO") .
263+ * Build a ResolutionSelector that "prefers" [targetSize] if not null,
264+ * otherwise tries to fallback to an aspect ratio if [ratio] is non-null,
265+ * or does default if both are null .
245266 */
267+ private fun buildResolutionSelector (
268+ targetSize : Size ? , // e.g. 3000×3000 for 1:1
269+ ratio : Float? // e.g. 1.0f for 1:1, 1.333...f for 4:3, etc.
270+ ): ResolutionSelector {
271+ val aspectRatioConst = when {
272+ ratio == null -> null // “FULL” or unknown
273+ kotlin.math.abs(ratio - (4f / 3f )) < 0.01f -> AspectRatio .RATIO_4_3
274+ kotlin.math.abs(ratio - (16f / 9f )) < 0.01f -> AspectRatio .RATIO_16_9
275+ else -> null // e.g. 1:1 or any custom ratio
276+ }
277+
278+ // If no targetSize is found, rely entirely on aspectRatioConst or a fallback
279+ if (targetSize == null ) {
280+ // No recognized ratio => default to RATIO_4_3 fallback
281+ return if (aspectRatioConst == null ) {
282+ ResolutionSelector .Builder ()
283+ .setAspectRatioStrategy(
284+ AspectRatioStrategy (
285+ AspectRatio .RATIO_4_3 ,
286+ AspectRatioStrategy .FALLBACK_RULE_AUTO
287+ )
288+ )
289+ .build()
290+ } else {
291+ // Use the recognized built-in ratio
292+ ResolutionSelector .Builder ()
293+ .setAspectRatioStrategy(
294+ AspectRatioStrategy (
295+ aspectRatioConst,
296+ AspectRatioStrategy .FALLBACK_RULE_AUTO
297+ )
298+ )
299+ .build()
300+ }
301+ }
302+
303+ // We do have a targetSize (the largest size matching the custom ratio, or “FULL” sensor size).
304+ val resolutionStrategy = ResolutionStrategy (
305+ targetSize,
306+ ResolutionStrategy .FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
307+ )
308+
309+ // If ratio maps to 4:3 or 16:9, we can also supply an AspectRatioStrategy fallback
310+ return if (aspectRatioConst != null ) {
311+ ResolutionSelector .Builder ()
312+ .setResolutionStrategy(resolutionStrategy)
313+ .setAspectRatioStrategy(
314+ AspectRatioStrategy (
315+ aspectRatioConst,
316+ AspectRatioStrategy .FALLBACK_RULE_AUTO
317+ )
318+ )
319+ .build()
320+ } else {
321+ // ratio is something else (e.g. 1:1), so no built-in aspect ratio
322+ // rely on the resolutionStrategy alone
323+ ResolutionSelector .Builder ()
324+ .setResolutionStrategy(resolutionStrategy)
325+ .build()
326+ }
327+ }
328+
329+
330+ // --------------------------------------------------------------------------------
331+ // Flash
332+ // --------------------------------------------------------------------------------
246333 fun setFlash (mode : String ) {
247- val newFlashMode = when (mode.uppercase()) {
334+ val newFlashMode = flashModeStringToMode(mode)
335+ flashMode = mode.uppercase()
336+ imageCapture.flashMode = newFlashMode
337+ }
338+
339+ private fun flashModeStringToMode (modeStr : String ): Int {
340+ return when (modeStr.uppercase()) {
248341 " ON" -> ImageCapture .FLASH_MODE_ON
249342 " OFF" -> ImageCapture .FLASH_MODE_OFF
250343 " AUTO" -> ImageCapture .FLASH_MODE_AUTO
251344 else -> {
252- Log .e(" StoneCameraViewModel" , " Invalid flash mode: $mode . Defaulting to OFF." )
345+ Log .e(" StoneCameraViewModel" , " Invalid flash mode: $modeStr . Defaulting to OFF." )
253346 ImageCapture .FLASH_MODE_OFF
254347 }
255348 }
349+ }
256350
257- imageCapture.flashMode = newFlashMode
258- flashMode = mode.uppercase() // Update the state
351+ fun setAspectRatio (ratio : String ) {
352+ if (ratio !in supportedAspectRatios) return
353+ selectedAspectRatio = ratio
354+ recreateUseCases()
355+ }
356+
357+ private fun recreateUseCases () {
358+ preview = createPreview(selectedAspectRatio)
359+ imageCapture = createImageCapture(selectedAspectRatio).also {
360+ it.flashMode = flashModeStringToMode(flashMode)
361+ }
362+ }
363+
364+ // --------------------------------------------------------------------------------
365+ // Building with ResolutionSelector
366+ // --------------------------------------------------------------------------------
367+
368+ private fun parseRatioOrNull (ratioStr : String ): Float? {
369+ val nums = ratioStr.split(" :" ).map { it.toFloatOrNull() }
370+ if (nums.any { it == null }) return null
371+ return nums[0 ]!! / nums[1 ]!!
372+ }
373+
374+ private fun createPreview (ratioStr : String ): Preview {
375+ val ratioOrNull = parseRatioOrNull(ratioStr)
376+ val cameraId = selectedCameraId.ifEmpty { " 0" }
377+
378+ // 1) Figure out the best "preferred size" for this ratio
379+ val targetSize = if (ratioOrNull == null ) {
380+ getLargestSensorSize(cameraId, context)
381+ } else {
382+ getLargestMatchingSize(cameraId, context, ratioOrNull)
383+ }
384+
385+ // 2) Build a ResolutionSelector
386+ val resolutionSelector = buildResolutionSelector(targetSize, ratioOrNull)
387+
388+ // 3) Apply to the Preview builder
389+ return Preview .Builder ()
390+ .setResolutionSelector(resolutionSelector)
391+ .build()
392+ .also {
393+ Log .d(" StoneCameraViewModel" , " Preview => ratio=$ratioStr , size=$targetSize " )
394+ }
395+ }
396+
397+ private fun createImageCapture (ratioStr : String ): ImageCapture {
398+ val ratioOrNull = parseRatioOrNull(ratioStr)
399+ val cameraId = selectedCameraId.ifEmpty { " 0" }
400+
401+ val targetSize = if (ratioOrNull == null ) {
402+ getLargestSensorSize(cameraId, context)
403+ } else {
404+ getLargestMatchingSize(cameraId, context, ratioOrNull)
405+ }
406+
407+ val resolutionSelector = buildResolutionSelector(targetSize, ratioOrNull)
408+
409+ return ImageCapture .Builder ()
410+ .setResolutionSelector(resolutionSelector)
411+ .build()
412+ .also {
413+ Log .d(" StoneCameraViewModel" , " ImageCapture => ratio=$ratioStr , size=$targetSize " )
414+ }
259415 }
260416}
261417
0 commit comments