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