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.foundation.clickable
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.Row
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.layout.size
25 import androidx.compose.foundation.layout.wrapContentSize
26 import androidx.compose.foundation.lazy.grid.GridCells
27 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
28 import androidx.compose.material.icons.Icons
29 import androidx.compose.material.icons.filled.ExpandMore
30 import androidx.compose.material3.Icon
31 import androidx.compose.material3.LocalContentColor
32 import androidx.compose.material3.Text
33 import androidx.compose.runtime.Composable
34 import androidx.compose.runtime.CompositionLocalProvider
35 import androidx.compose.ui.Alignment
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.draw.scale
38 import androidx.compose.ui.graphics.Color
39 import androidx.compose.ui.graphics.painter.Painter
40 import androidx.compose.ui.platform.LocalConfiguration
41 import androidx.compose.ui.platform.testTag
42 import androidx.compose.ui.res.dimensionResource
43 import androidx.compose.ui.res.stringResource
44 import androidx.compose.ui.semantics.contentDescription
45 import androidx.compose.ui.semantics.semantics
46 import androidx.compose.ui.text.style.TextAlign
47 import androidx.compose.ui.unit.dp
48 import com.google.jetpackcamera.feature.preview.PreviewMode
49 import com.google.jetpackcamera.feature.preview.R
50 import com.google.jetpackcamera.feature.preview.quicksettings.CameraAspectRatio
51 import com.google.jetpackcamera.feature.preview.quicksettings.CameraCaptureMode
52 import com.google.jetpackcamera.feature.preview.quicksettings.CameraConcurrentCameraMode
53 import com.google.jetpackcamera.feature.preview.quicksettings.CameraDynamicRange
54 import com.google.jetpackcamera.feature.preview.quicksettings.CameraFlashMode
55 import com.google.jetpackcamera.feature.preview.quicksettings.CameraLensFace
56 import com.google.jetpackcamera.feature.preview.quicksettings.CameraLowLightBoost
57 import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsEnum
58 import com.google.jetpackcamera.settings.model.AspectRatio
59 import com.google.jetpackcamera.settings.model.CaptureMode
60 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
61 import com.google.jetpackcamera.settings.model.DynamicRange
62 import com.google.jetpackcamera.settings.model.FlashMode
63 import com.google.jetpackcamera.settings.model.ImageOutputFormat
64 import com.google.jetpackcamera.settings.model.LensFacing
65 import com.google.jetpackcamera.settings.model.LowLightBoost
66 import kotlin.math.min
67
68 // completed components ready to go into preview screen
69
70 @Composable
71 fun ExpandedQuickSetRatio(
72 setRatio: (aspectRatio: AspectRatio) -> Unit,
73 currentRatio: AspectRatio,
74 modifier: Modifier = Modifier
75 ) {
76 val buttons: Array<@Composable () -> Unit> =
77 arrayOf(
78 {
79 QuickSetRatio(
80 modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON),
81 onClick = { setRatio(AspectRatio.THREE_FOUR) },
82 ratio = AspectRatio.THREE_FOUR,
83 currentRatio = currentRatio,
84 isHighlightEnabled = true
85 )
86 },
87 {
88 QuickSetRatio(
89 modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON),
90 onClick = { setRatio(AspectRatio.NINE_SIXTEEN) },
91 ratio = AspectRatio.NINE_SIXTEEN,
92 currentRatio = currentRatio,
93 isHighlightEnabled = true
94 )
95 },
96 {
97 QuickSetRatio(
98 modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON),
99 onClick = { setRatio(AspectRatio.ONE_ONE) },
100 ratio = AspectRatio.ONE_ONE,
101 currentRatio = currentRatio,
102 isHighlightEnabled = true
103 )
104 }
105 )
106 ExpandedQuickSetting(modifier = modifier, quickSettingButtons = buttons)
107 }
108
109 @Composable
QuickSetHdrnull110 fun QuickSetHdr(
111 modifier: Modifier = Modifier,
112 onClick: (dynamicRange: DynamicRange, imageOutputFormat: ImageOutputFormat) -> Unit,
113 selectedDynamicRange: DynamicRange,
114 selectedImageOutputFormat: ImageOutputFormat,
115 hdrDynamicRange: DynamicRange,
116 hdrImageFormat: ImageOutputFormat,
117 hdrDynamicRangeSupported: Boolean,
118 previewMode: PreviewMode,
119 enabled: Boolean
120 ) {
121 val enum =
122 if (selectedDynamicRange == hdrDynamicRange ||
123 selectedImageOutputFormat == hdrImageFormat
124 ) {
125 CameraDynamicRange.HDR
126 } else {
127 CameraDynamicRange.SDR
128 }
129
130 QuickSettingUiItem(
131 modifier = modifier,
132 enum = enum,
133 onClick = {
134 val newDynamicRange =
135 if (selectedDynamicRange == DynamicRange.SDR && hdrDynamicRangeSupported) {
136 hdrDynamicRange
137 } else {
138 DynamicRange.SDR
139 }
140 val newImageOutputFormat =
141 if (!hdrDynamicRangeSupported ||
142 previewMode is PreviewMode.ExternalImageCaptureMode
143 ) {
144 hdrImageFormat
145 } else {
146 ImageOutputFormat.JPEG
147 }
148 onClick(newDynamicRange, newImageOutputFormat)
149 },
150 isHighLighted = (selectedDynamicRange != DynamicRange.SDR),
151 enabled = enabled
152 )
153 }
154
155 @Composable
QuickSetLowLightBoostnull156 fun QuickSetLowLightBoost(
157 modifier: Modifier = Modifier,
158 onClick: (lowLightBoost: LowLightBoost) -> Unit,
159 selectedLowLightBoost: LowLightBoost
160 ) {
161 val enum = when (selectedLowLightBoost) {
162 LowLightBoost.DISABLED -> CameraLowLightBoost.DISABLED
163 LowLightBoost.ENABLED -> CameraLowLightBoost.ENABLED
164 }
165
166 QuickSettingUiItem(
167 modifier = modifier,
168 enum = enum,
169 onClick = {
170 when (selectedLowLightBoost) {
171 LowLightBoost.DISABLED -> onClick(LowLightBoost.ENABLED)
172 LowLightBoost.ENABLED -> onClick(LowLightBoost.DISABLED)
173 }
174 },
175 isHighLighted = false
176 )
177 }
178
179 @Composable
QuickSetRationull180 fun QuickSetRatio(
181 onClick: () -> Unit,
182 ratio: AspectRatio,
183 currentRatio: AspectRatio,
184 modifier: Modifier = Modifier,
185 isHighlightEnabled: Boolean = false
186 ) {
187 val enum =
188 when (ratio) {
189 AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR
190 AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN
191 AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE
192 else -> CameraAspectRatio.ONE_ONE
193 }
194 QuickSettingUiItem(
195 modifier = modifier,
196 enum = enum,
197 onClick = { onClick() },
198 isHighLighted = isHighlightEnabled && (ratio == currentRatio)
199 )
200 }
201
202 @Composable
QuickSetFlashnull203 fun QuickSetFlash(
204 onClick: (FlashMode) -> Unit,
205 currentFlashMode: FlashMode,
206 modifier: Modifier = Modifier
207 ) {
208 val enum = when (currentFlashMode) {
209 FlashMode.OFF -> CameraFlashMode.OFF
210 FlashMode.AUTO -> CameraFlashMode.AUTO
211 FlashMode.ON -> CameraFlashMode.ON
212 }
213 QuickSettingUiItem(
214 modifier = modifier
215 .semantics {
216 contentDescription =
217 when (enum) {
218 CameraFlashMode.OFF -> "QUICK SETTINGS FLASH IS OFF"
219 CameraFlashMode.AUTO -> "QUICK SETTINGS FLASH IS AUTO"
220 CameraFlashMode.ON -> "QUICK SETTINGS FLASH IS ON"
221 }
222 },
223 enum = enum,
224 isHighLighted = currentFlashMode == FlashMode.ON,
225 onClick =
226 {
227 onClick(currentFlashMode.getNextFlashMode())
228 }
229 )
230 }
231
232 @Composable
QuickFlipCameranull233 fun QuickFlipCamera(
234 setLensFacing: (LensFacing) -> Unit,
235 currentLensFacing: LensFacing,
236 modifier: Modifier = Modifier
237 ) {
238 val enum =
239 when (currentLensFacing) {
240 LensFacing.FRONT -> CameraLensFace.FRONT
241 LensFacing.BACK -> CameraLensFace.BACK
242 }
243 QuickSettingUiItem(
244 modifier = modifier,
245 enum = enum,
246 onClick = { setLensFacing(currentLensFacing.flip()) }
247 )
248 }
249
250 @Composable
QuickSetCaptureModenull251 fun QuickSetCaptureMode(
252 setCaptureMode: (CaptureMode) -> Unit,
253 currentCaptureMode: CaptureMode,
254 modifier: Modifier = Modifier,
255 enabled: Boolean = true
256 ) {
257 val enum: CameraCaptureMode =
258 when (currentCaptureMode) {
259 CaptureMode.MULTI_STREAM -> CameraCaptureMode.MULTI_STREAM
260 CaptureMode.SINGLE_STREAM -> CameraCaptureMode.SINGLE_STREAM
261 }
262 QuickSettingUiItem(
263 modifier = modifier,
264 enum = enum,
265 onClick = {
266 when (currentCaptureMode) {
267 CaptureMode.MULTI_STREAM -> setCaptureMode(CaptureMode.SINGLE_STREAM)
268 CaptureMode.SINGLE_STREAM -> setCaptureMode(CaptureMode.MULTI_STREAM)
269 }
270 },
271 enabled = enabled
272 )
273 }
274
275 @Composable
QuickSetConcurrentCameranull276 fun QuickSetConcurrentCamera(
277 setConcurrentCameraMode: (ConcurrentCameraMode) -> Unit,
278 currentConcurrentCameraMode: ConcurrentCameraMode,
279 modifier: Modifier = Modifier,
280 enabled: Boolean = true
281 ) {
282 val enum: CameraConcurrentCameraMode =
283 when (currentConcurrentCameraMode) {
284 ConcurrentCameraMode.OFF -> CameraConcurrentCameraMode.OFF
285 ConcurrentCameraMode.DUAL -> CameraConcurrentCameraMode.DUAL
286 }
287 QuickSettingUiItem(
288 modifier = modifier,
289 enum = enum,
290 onClick = {
291 when (currentConcurrentCameraMode) {
292 ConcurrentCameraMode.OFF -> setConcurrentCameraMode(ConcurrentCameraMode.DUAL)
293 ConcurrentCameraMode.DUAL -> setConcurrentCameraMode(ConcurrentCameraMode.OFF)
294 }
295 },
296 enabled = enabled
297 )
298 }
299
300 /**
301 * Button to toggle quick settings
302 */
303 @Composable
ToggleQuickSettingsButtonnull304 fun ToggleQuickSettingsButton(
305 toggleDropDown: () -> Unit,
306 isOpen: Boolean,
307 modifier: Modifier = Modifier
308 ) {
309 Row(
310 horizontalArrangement = Arrangement.Center,
311 verticalAlignment = Alignment.CenterVertically,
312 modifier = modifier
313 ) {
314 // dropdown icon
315 Icon(
316 imageVector = Icons.Filled.ExpandMore,
317 contentDescription = if (isOpen) {
318 stringResource(R.string.quick_settings_dropdown_open_description)
319 } else {
320 stringResource(R.string.quick_settings_dropdown_closed_description)
321 },
322 modifier = Modifier
323 .testTag(QUICK_SETTINGS_DROP_DOWN)
324 .size(72.dp)
325 .clickable {
326 toggleDropDown()
327 }
328 .scale(1f, if (isOpen) -1f else 1f)
329 )
330 }
331 }
332
333 // subcomponents used to build completed components
334
335 @Composable
QuickSettingUiItemnull336 fun QuickSettingUiItem(
337 enum: QuickSettingsEnum,
338 onClick: () -> Unit,
339 modifier: Modifier = Modifier,
340 isHighLighted: Boolean = false,
341 enabled: Boolean = true
342 ) {
343 QuickSettingUiItem(
344 modifier = modifier,
345 painter = enum.getPainter(),
346 text = stringResource(id = enum.getTextResId()),
347 accessibilityText = stringResource(id = enum.getDescriptionResId()),
348 onClick = { onClick() },
349 isHighLighted = isHighLighted,
350 enabled = enabled
351 )
352 }
353
354 /**
355 * The itemized UI component representing each button in quick settings.
356 */
357 @Composable
QuickSettingUiItemnull358 fun QuickSettingUiItem(
359 text: String,
360 painter: Painter,
361 accessibilityText: String,
362 onClick: () -> Unit,
363 modifier: Modifier = Modifier,
364 isHighLighted: Boolean = false,
365 enabled: Boolean = true
366 ) {
367 Column(
368 modifier =
369 modifier
370 .wrapContentSize()
371 .padding(dimensionResource(id = R.dimen.quick_settings_ui_item_padding))
372 .clickable(onClick = onClick, enabled = enabled),
373 verticalArrangement = Arrangement.Center,
374 horizontalAlignment = Alignment.CenterHorizontally
375 ) {
376 val contentColor = (if (isHighLighted) Color.Yellow else Color.White).let {
377 // When in disabled state, material3 guidelines say the element's opacity should be 38%
378 // See: https://m3.material.io/foundations/interaction/states/applying-states#3c3032e8-b07a-42ac-a508-a32f573cc7e1
379 // and: https://developer.android.com/develop/ui/compose/designsystems/material2-material3#emphasis-and
380 if (!enabled) it.copy(alpha = 0.38f) else it
381 }
382 CompositionLocalProvider(LocalContentColor provides contentColor) {
383 Icon(
384 painter = painter,
385 contentDescription = accessibilityText,
386 modifier = Modifier.size(
387 dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size)
388 )
389 )
390
391 Text(text = text, textAlign = TextAlign.Center)
392 }
393 }
394 }
395
396 /**
397 * Should you want to have an expanded view of a single quick setting
398 */
399 @Composable
ExpandedQuickSettingnull400 fun ExpandedQuickSetting(
401 modifier: Modifier = Modifier,
402 vararg quickSettingButtons: @Composable () -> Unit
403 ) {
404 val expandedNumOfColumns =
405 min(
406 quickSettingButtons.size,
407 (
408 (
409 LocalConfiguration.current.screenWidthDp.dp - (
410 dimensionResource(
411 id = R.dimen.quick_settings_ui_horizontal_padding
412 ) * 2
413 )
414 ) /
415 (
416 dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
417 (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
418 )
419 ).toInt()
420 )
421 LazyVerticalGrid(
422 modifier = modifier.fillMaxWidth(),
423 columns = GridCells.Fixed(count = expandedNumOfColumns)
424 ) {
425 items(quickSettingButtons.size) { i ->
426 quickSettingButtons[i]()
427 }
428 }
429 }
430
431 /**
432 * Algorithm to determine dimensions of QuickSettings Icon layout
433 */
434 @Composable
QuickSettingsGridnull435 fun QuickSettingsGrid(
436 modifier: Modifier = Modifier,
437 quickSettingsButtons: List<@Composable () -> Unit>
438 ) {
439 val initialNumOfColumns =
440 min(
441 quickSettingsButtons.size,
442 (
443 (
444 LocalConfiguration.current.screenWidthDp.dp - (
445 dimensionResource(
446 id = R.dimen.quick_settings_ui_horizontal_padding
447 ) * 2
448 )
449 ) /
450 (
451 dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
452 (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
453 )
454 ).toInt()
455 )
456
457 LazyVerticalGrid(
458 modifier = modifier.fillMaxWidth(),
459 columns = GridCells.Fixed(count = initialNumOfColumns)
460 ) {
461 items(quickSettingsButtons.size) { i ->
462 quickSettingsButtons[i]()
463 }
464 }
465 }
466
467 /**
468 * The top bar indicators for quick settings items.
469 */
470 @Composable
Indicatornull471 fun Indicator(enum: QuickSettingsEnum, onClick: () -> Unit, modifier: Modifier = Modifier) {
472 Icon(
473 painter = enum.getPainter(),
474 contentDescription = stringResource(id = enum.getDescriptionResId()),
475 modifier = modifier
476 .size(dimensionResource(id = R.dimen.quick_settings_indicator_size))
477 .clickable { onClick() }
478 )
479 }
480
481 @Composable
FlashModeIndicatornull482 fun FlashModeIndicator(currentFlashMode: FlashMode, onClick: (flashMode: FlashMode) -> Unit) {
483 val enum = when (currentFlashMode) {
484 FlashMode.OFF -> CameraFlashMode.OFF
485 FlashMode.AUTO -> CameraFlashMode.AUTO
486 FlashMode.ON -> CameraFlashMode.ON
487 }
488 Indicator(
489 enum = enum,
490 onClick = {
491 onClick(currentFlashMode.getNextFlashMode())
492 }
493 )
494 }
495
496 @Composable
QuickSettingsIndicatorsnull497 fun QuickSettingsIndicators(
498 currentFlashMode: FlashMode,
499 onFlashModeClick: (flashMode: FlashMode) -> Unit,
500 modifier: Modifier = Modifier
501 ) {
502 Row(modifier) {
503 FlashModeIndicator(currentFlashMode, onFlashModeClick)
504 }
505 }
506
getNextFlashModenull507 fun FlashMode.getNextFlashMode(): FlashMode {
508 return when (this) {
509 FlashMode.OFF -> FlashMode.ON
510 FlashMode.ON -> FlashMode.AUTO
511 FlashMode.AUTO -> FlashMode.OFF
512 }
513 }
514