• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.jetpackcamera.feature.preview.quicksettings.ui
17 
18 import androidx.compose.animation.core.Spring
19 import androidx.compose.animation.core.animateFloatAsState
20 import androidx.compose.animation.core.spring
21 import androidx.compose.foundation.clickable
22 import androidx.compose.foundation.interaction.MutableInteractionSource
23 import androidx.compose.foundation.layout.Arrangement
24 import androidx.compose.foundation.layout.Column
25 import androidx.compose.foundation.layout.Row
26 import androidx.compose.foundation.layout.fillMaxWidth
27 import androidx.compose.foundation.layout.padding
28 import androidx.compose.foundation.layout.size
29 import androidx.compose.foundation.layout.wrapContentSize
30 import androidx.compose.foundation.lazy.grid.GridCells
31 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
32 import androidx.compose.material.icons.Icons
33 import androidx.compose.material.icons.filled.ExpandMore
34 import androidx.compose.material3.Icon
35 import androidx.compose.material3.LocalContentColor
36 import androidx.compose.material3.Text
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.CompositionLocalProvider
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.mutableStateOf
41 import androidx.compose.runtime.remember
42 import androidx.compose.runtime.setValue
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.rotate
46 import androidx.compose.ui.draw.scale
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.graphics.painter.Painter
49 import androidx.compose.ui.platform.LocalConfiguration
50 import androidx.compose.ui.platform.testTag
51 import androidx.compose.ui.res.dimensionResource
52 import androidx.compose.ui.res.stringResource
53 import androidx.compose.ui.text.style.TextAlign
54 import androidx.compose.ui.unit.dp
55 import com.google.jetpackcamera.feature.preview.FlashModeUiState
56 import com.google.jetpackcamera.feature.preview.PreviewMode
57 import com.google.jetpackcamera.feature.preview.R
58 import com.google.jetpackcamera.feature.preview.quicksettings.CameraAspectRatio
59 import com.google.jetpackcamera.feature.preview.quicksettings.CameraConcurrentCameraMode
60 import com.google.jetpackcamera.feature.preview.quicksettings.CameraDynamicRange
61 import com.google.jetpackcamera.feature.preview.quicksettings.CameraFlashMode
62 import com.google.jetpackcamera.feature.preview.quicksettings.CameraLensFace
63 import com.google.jetpackcamera.feature.preview.quicksettings.CameraStreamConfig
64 import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsEnum
65 import com.google.jetpackcamera.settings.model.AspectRatio
66 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
67 import com.google.jetpackcamera.settings.model.DEFAULT_HDR_DYNAMIC_RANGE
68 import com.google.jetpackcamera.settings.model.DEFAULT_HDR_IMAGE_OUTPUT
69 import com.google.jetpackcamera.settings.model.DynamicRange
70 import com.google.jetpackcamera.settings.model.FlashMode
71 import com.google.jetpackcamera.settings.model.ImageOutputFormat
72 import com.google.jetpackcamera.settings.model.LensFacing
73 import com.google.jetpackcamera.settings.model.StreamConfig
74 import kotlin.math.min
75 
76 // completed components ready to go into preview screen
77 
78 @Composable
79 fun FocusedQuickSetRatio(
80     setRatio: (aspectRatio: AspectRatio) -> Unit,
81     currentRatio: AspectRatio,
82     modifier: Modifier = Modifier
83 ) {
84     val buttons: Array<@Composable () -> Unit> =
85         arrayOf(
86             {
87                 QuickSetRatio(
88                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON),
89                     onClick = { setRatio(AspectRatio.THREE_FOUR) },
90                     ratio = AspectRatio.THREE_FOUR,
91                     currentRatio = currentRatio,
92                     isHighlightEnabled = true
93                 )
94             },
95             {
96                 QuickSetRatio(
97                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON),
98                     onClick = { setRatio(AspectRatio.NINE_SIXTEEN) },
99                     ratio = AspectRatio.NINE_SIXTEEN,
100                     currentRatio = currentRatio,
101                     isHighlightEnabled = true
102                 )
103             },
104             {
105                 QuickSetRatio(
106                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON),
107                     onClick = { setRatio(AspectRatio.ONE_ONE) },
108                     ratio = AspectRatio.ONE_ONE,
109                     currentRatio = currentRatio,
110                     isHighlightEnabled = true
111                 )
112             }
113         )
114     ExpandedQuickSetting(modifier = modifier, quickSettingButtons = buttons)
115 }
116 
117 @Composable
QuickSetHdrnull118 fun QuickSetHdr(
119     modifier: Modifier = Modifier,
120     onClick: (dynamicRange: DynamicRange, imageOutputFormat: ImageOutputFormat) -> Unit,
121     selectedDynamicRange: DynamicRange,
122     selectedImageOutputFormat: ImageOutputFormat,
123     hdrDynamicRangeSupported: Boolean,
124     previewMode: PreviewMode,
125     enabled: Boolean
126 ) {
127     val enum =
128         if (selectedDynamicRange == DEFAULT_HDR_DYNAMIC_RANGE ||
129             selectedImageOutputFormat == DEFAULT_HDR_IMAGE_OUTPUT
130         ) {
131             CameraDynamicRange.HDR
132         } else {
133             CameraDynamicRange.SDR
134         }
135 
136     QuickSettingUiItem(
137         modifier = modifier,
138         enum = enum,
139         onClick = {
140             val newDynamicRange =
141                 if (selectedDynamicRange == DynamicRange.SDR && hdrDynamicRangeSupported) {
142                     DEFAULT_HDR_DYNAMIC_RANGE
143                 } else {
144                     DynamicRange.SDR
145                 }
146             val newImageOutputFormat =
147                 if (!hdrDynamicRangeSupported ||
148                     previewMode is PreviewMode.ExternalImageCaptureMode
149                 ) {
150                     DEFAULT_HDR_IMAGE_OUTPUT
151                 } else {
152                     ImageOutputFormat.JPEG
153                 }
154             onClick(newDynamicRange, newImageOutputFormat)
155         },
156         isHighLighted = (selectedDynamicRange != DynamicRange.SDR),
157         enabled = enabled
158     )
159 }
160 
161 @Composable
QuickSetRationull162 fun QuickSetRatio(
163     onClick: () -> Unit,
164     ratio: AspectRatio,
165     currentRatio: AspectRatio,
166     modifier: Modifier = Modifier,
167     isHighlightEnabled: Boolean = false
168 ) {
169     val enum =
170         when (ratio) {
171             AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR
172             AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN
173             AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE
174             else -> CameraAspectRatio.ONE_ONE
175         }
176     QuickSettingUiItem(
177         modifier = modifier,
178         enum = enum,
179         onClick = { onClick() },
180         isHighLighted = isHighlightEnabled && (ratio == currentRatio)
181     )
182 }
183 
184 @Composable
QuickSetFlashnull185 fun QuickSetFlash(
186     modifier: Modifier = Modifier,
187     onClick: (FlashMode) -> Unit,
188     flashModeUiState: FlashModeUiState
189 ) {
190     when (flashModeUiState) {
191         is FlashModeUiState.Unavailable ->
192             QuickSettingUiItem(
193                 modifier = modifier,
194                 enum = CameraFlashMode.OFF,
195                 enabled = false,
196                 onClick = {}
197             )
198         is FlashModeUiState.Available ->
199             QuickSettingUiItem(
200                 modifier = modifier,
201                 enum = flashModeUiState.selectedFlashMode.toCameraFlashMode(
202                     flashModeUiState.isActive
203                 ),
204                 isHighLighted = flashModeUiState.selectedFlashMode == FlashMode.ON,
205                 onClick = {
206                     onClick(flashModeUiState.getNextFlashMode())
207                 }
208             )
209     }
210 }
211 
212 @Composable
QuickFlipCameranull213 fun QuickFlipCamera(
214     setLensFacing: (LensFacing) -> Unit,
215     currentLensFacing: LensFacing,
216     modifier: Modifier = Modifier
217 ) {
218     val enum =
219         when (currentLensFacing) {
220             LensFacing.FRONT -> CameraLensFace.FRONT
221             LensFacing.BACK -> CameraLensFace.BACK
222         }
223     QuickSettingUiItem(
224         modifier = modifier,
225         enum = enum,
226         onClick = { setLensFacing(currentLensFacing.flip()) }
227     )
228 }
229 
230 @Composable
QuickSetStreamConfignull231 fun QuickSetStreamConfig(
232     setStreamConfig: (StreamConfig) -> Unit,
233     currentStreamConfig: StreamConfig,
234     modifier: Modifier = Modifier,
235     enabled: Boolean = true
236 ) {
237     val enum: CameraStreamConfig =
238         when (currentStreamConfig) {
239             StreamConfig.MULTI_STREAM -> CameraStreamConfig.MULTI_STREAM
240             StreamConfig.SINGLE_STREAM -> CameraStreamConfig.SINGLE_STREAM
241         }
242     QuickSettingUiItem(
243         modifier = modifier,
244         enum = enum,
245         onClick = {
246             when (currentStreamConfig) {
247                 StreamConfig.MULTI_STREAM -> setStreamConfig(StreamConfig.SINGLE_STREAM)
248                 StreamConfig.SINGLE_STREAM -> setStreamConfig(StreamConfig.MULTI_STREAM)
249             }
250         },
251         enabled = enabled
252     )
253 }
254 
255 @Composable
QuickSetConcurrentCameranull256 fun QuickSetConcurrentCamera(
257     setConcurrentCameraMode: (ConcurrentCameraMode) -> Unit,
258     currentConcurrentCameraMode: ConcurrentCameraMode,
259     modifier: Modifier = Modifier,
260     enabled: Boolean = true
261 ) {
262     val enum: CameraConcurrentCameraMode =
263         when (currentConcurrentCameraMode) {
264             ConcurrentCameraMode.OFF -> CameraConcurrentCameraMode.OFF
265             ConcurrentCameraMode.DUAL -> CameraConcurrentCameraMode.DUAL
266         }
267     QuickSettingUiItem(
268         modifier = modifier,
269         enum = enum,
270         onClick = {
271             when (currentConcurrentCameraMode) {
272                 ConcurrentCameraMode.OFF -> setConcurrentCameraMode(ConcurrentCameraMode.DUAL)
273                 ConcurrentCameraMode.DUAL -> setConcurrentCameraMode(ConcurrentCameraMode.OFF)
274             }
275         },
276         enabled = enabled
277     )
278 }
279 
280 /**
281  * Button to toggle quick settings
282  */
283 @Composable
ToggleQuickSettingsButtonnull284 fun ToggleQuickSettingsButton(
285     toggleDropDown: () -> Unit,
286     isOpen: Boolean,
287     modifier: Modifier = Modifier
288 ) {
289     val rotationAngle by animateFloatAsState(
290         targetValue = if (isOpen) -180f else 0f,
291         animationSpec = spring(stiffness = Spring.StiffnessLow) // Adjust duration as needed
292     )
293     Row(
294         horizontalArrangement = Arrangement.Center,
295         verticalAlignment = Alignment.CenterVertically,
296         modifier = modifier.rotate(rotationAngle)
297     ) {
298         // dropdown icon
299         Icon(
300             imageVector = Icons.Filled.ExpandMore,
301             contentDescription = if (isOpen) {
302                 stringResource(R.string.quick_settings_dropdown_open_description)
303             } else {
304                 stringResource(R.string.quick_settings_dropdown_closed_description)
305             },
306             modifier = Modifier
307                 .testTag(QUICK_SETTINGS_DROP_DOWN)
308                 .size(72.dp)
309                 .clickable(
310                     interactionSource = remember { MutableInteractionSource() },
311                     // removes the greyish background animation that appears when clicking on a clickable
312                     indication = null,
313                     onClick = toggleDropDown
314                 )
315         )
316     }
317 }
318 
319 // subcomponents used to build completed components
320 
321 @Composable
QuickSettingUiItemnull322 fun QuickSettingUiItem(
323     enum: QuickSettingsEnum,
324     onClick: () -> Unit,
325     modifier: Modifier = Modifier,
326     isHighLighted: Boolean = false,
327     enabled: Boolean = true
328 ) {
329     QuickSettingUiItem(
330         modifier = modifier,
331         painter = enum.getPainter(),
332         text = stringResource(id = enum.getTextResId()),
333         accessibilityText = stringResource(id = enum.getDescriptionResId()),
334         onClick = { onClick() },
335         isHighLighted = isHighLighted,
336         enabled = enabled
337     )
338 }
339 
340 /**
341  * The itemized UI component representing each button in quick settings.
342  */
343 @Composable
QuickSettingUiItemnull344 fun QuickSettingUiItem(
345     text: String,
346     painter: Painter,
347     accessibilityText: String,
348     onClick: () -> Unit,
349     modifier: Modifier = Modifier,
350     isHighLighted: Boolean = false,
351     enabled: Boolean = true
352 ) {
353     val iconSize = dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size)
354 
355     var buttonClicked by remember { mutableStateOf(false) }
356     val animatedScale by animateFloatAsState(
357         targetValue = if (buttonClicked) 1.1f else 1f, // Scale up to 110%
358         animationSpec = spring(
359             dampingRatio = Spring.DampingRatioLowBouncy,
360             stiffness = Spring.StiffnessMedium
361         ),
362         finishedListener = {
363             buttonClicked = false // Reset the trigger
364         }
365     )
366     Column(
367         modifier =
368         modifier
369             .wrapContentSize()
370             .padding(dimensionResource(id = R.dimen.quick_settings_ui_item_padding))
371             .clickable(
372                 enabled = enabled,
373                 onClick = {
374                     buttonClicked = true
375                     onClick()
376                 },
377                 indication = null,
378                 interactionSource = null
379             ),
380         verticalArrangement = Arrangement.Center,
381         horizontalAlignment = Alignment.CenterHorizontally
382     ) {
383         val contentColor = (if (isHighLighted) Color.Yellow else Color.White).let {
384             // When in disabled state, material3 guidelines say the element's opacity should be 38%
385             // See: https://m3.material.io/foundations/interaction/states/applying-states#3c3032e8-b07a-42ac-a508-a32f573cc7e1
386             // and: https://developer.android.com/develop/ui/compose/designsystems/material2-material3#emphasis-and
387             if (!enabled) it.copy(alpha = 0.38f) else it
388         }
389         CompositionLocalProvider(LocalContentColor provides contentColor) {
390             Icon(
391                 painter = painter,
392                 contentDescription = accessibilityText,
393                 modifier = Modifier.size(iconSize).scale(animatedScale)
394             )
395 
396             Text(text = text, textAlign = TextAlign.Center)
397         }
398     }
399 }
400 
401 /**
402  * Should you want to have an expanded view of a single quick setting
403  */
404 @Composable
ExpandedQuickSettingnull405 fun ExpandedQuickSetting(
406     modifier: Modifier = Modifier,
407     vararg quickSettingButtons: @Composable () -> Unit
408 ) {
409     val expandedNumOfColumns =
410         min(
411             quickSettingButtons.size,
412             (
413                 (
414                     LocalConfiguration.current.screenWidthDp.dp - (
415                         dimensionResource(
416                             id = R.dimen.quick_settings_ui_horizontal_padding
417                         ) * 2
418                         )
419                     ) /
420                     (
421                         dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
422                             (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
423                         )
424                 ).toInt()
425         )
426     LazyVerticalGrid(
427         modifier = modifier.fillMaxWidth(),
428         columns = GridCells.Fixed(count = expandedNumOfColumns)
429     ) {
430         items(quickSettingButtons.size) { i ->
431             quickSettingButtons[i]()
432         }
433     }
434 }
435 
436 /**
437  * Algorithm to determine dimensions of QuickSettings Icon layout
438  */
439 @Composable
QuickSettingsGridnull440 fun QuickSettingsGrid(
441     modifier: Modifier = Modifier,
442     quickSettingsButtons: List<@Composable () -> Unit>
443 ) {
444     val initialNumOfColumns =
445         min(
446             quickSettingsButtons.size,
447             (
448                 (
449                     LocalConfiguration.current.screenWidthDp.dp - (
450                         dimensionResource(
451                             id = R.dimen.quick_settings_ui_horizontal_padding
452                         ) * 2
453                         )
454                     ) /
455                     (
456                         dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
457                             (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
458                         )
459                 ).toInt()
460         )
461 
462     LazyVerticalGrid(
463         modifier = modifier.fillMaxWidth(),
464         columns = GridCells.Fixed(count = initialNumOfColumns)
465     ) {
466         items(quickSettingsButtons.size) { i ->
467             quickSettingsButtons[i]()
468         }
469     }
470 }
471 
472 /**
473  * The top bar indicators for quick settings items.
474  */
475 @Composable
TopBarSettingIndicatornull476 fun TopBarSettingIndicator(
477     enum: QuickSettingsEnum,
478     modifier: Modifier = Modifier,
479     enabled: Boolean = true,
480     onClick: () -> Unit = {}
481 ) {
<lambda>null482     val contentColor = Color.White.let {
483         if (!enabled) it.copy(alpha = 0.38f) else it
484     }
<lambda>null485     CompositionLocalProvider(LocalContentColor provides contentColor) {
486         Icon(
487             painter = enum.getPainter(),
488             contentDescription = stringResource(id = enum.getDescriptionResId()),
489             modifier = modifier
490                 .size(dimensionResource(id = R.dimen.quick_settings_indicator_size))
491                 .clickable(
492                     interactionSource = remember { MutableInteractionSource() },
493                     indication = null,
494                     onClick = onClick,
495                     enabled = enabled
496                 )
497         )
498     }
499 }
500 
501 @Composable
FlashModeIndicatornull502 fun FlashModeIndicator(
503     flashModeUiState: FlashModeUiState,
504     onClick: (flashMode: FlashMode) -> Unit
505 ) {
506     when (flashModeUiState) {
507         is FlashModeUiState.Unavailable ->
508             TopBarSettingIndicator(
509                 enum = CameraFlashMode.OFF,
510                 enabled = false
511             )
512         is FlashModeUiState.Available ->
513             TopBarSettingIndicator(
514                 enum = flashModeUiState.selectedFlashMode.toCameraFlashMode(
515                     flashModeUiState.isActive
516                 ),
517                 onClick = {
518                     onClick(flashModeUiState.getNextFlashMode())
519                 }
520             )
521     }
522 }
523 
524 @Composable
QuickSettingsIndicatorsnull525 fun QuickSettingsIndicators(
526     modifier: Modifier = Modifier,
527     flashModeUiState: FlashModeUiState,
528     onFlashModeClick: (flashMode: FlashMode) -> Unit
529 ) {
530     Row(modifier = modifier) {
531         FlashModeIndicator(
532             flashModeUiState,
533             onFlashModeClick
534         )
535     }
536 }
537 
<lambda>null538 private fun FlashModeUiState.Available.getNextFlashMode(): FlashMode = availableFlashModes.run {
539     get((indexOf(selectedFlashMode) + 1) % size)
540 }
541 
toCameraFlashModenull542 private fun FlashMode.toCameraFlashMode(isActive: Boolean) = when (this) {
543     FlashMode.OFF -> CameraFlashMode.OFF
544     FlashMode.AUTO -> CameraFlashMode.AUTO
545     FlashMode.ON -> CameraFlashMode.ON
546     FlashMode.LOW_LIGHT_BOOST -> {
547         when (isActive) {
548             true -> CameraFlashMode.LOW_LIGHT_BOOST_ACTIVE
549             false -> CameraFlashMode.LOW_LIGHT_BOOST_INACTIVE
550         }
551     }
552 }
553