Skip to content

Commit 53e4df4

Browse files
committed
Camera UI now rotates
1 parent acea5eb commit 53e4df4

6 files changed

Lines changed: 182 additions & 55 deletions

File tree

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/BottomCameraControls.kt

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import androidx.compose.runtime.Composable
1010
import androidx.compose.ui.Alignment
1111
import androidx.compose.ui.Modifier
1212
import androidx.compose.ui.draw.clip
13+
import androidx.compose.ui.draw.rotate
1314
import androidx.compose.ui.graphics.Color
15+
import androidx.compose.ui.graphics.vector.ImageVector
1416
import androidx.compose.ui.platform.LocalContext
1517
import androidx.compose.ui.res.stringResource
1618
import androidx.compose.ui.semantics.contentDescription
@@ -31,6 +33,7 @@ fun BottomCameraControls(
3133
onModeChange: (CaptureMode) -> Unit,
3234
isLoading: Boolean,
3335
navController: NavController,
36+
iconRotation: Float = 0f,
3437
) {
3538
val context = LocalContext.current
3639

@@ -40,36 +43,26 @@ fun BottomCameraControls(
4043
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
4144
horizontalAlignment = Alignment.CenterHorizontally,
4245
) {
43-
// Mode toggle chips
46+
// Mode toggle buttons
4447
Row(
4548
modifier = Modifier.padding(bottom = 16.dp),
49+
horizontalArrangement = Arrangement.spacedBy(12.dp),
4650
) {
47-
FilterChip(
51+
ModeToggleButton(
4852
selected = captureMode == CaptureMode.PHOTO,
4953
onClick = { onModeChange(CaptureMode.PHOTO) },
5054
enabled = !isRecording && !isLoading,
51-
label = { Text(stringResource(R.string.camera_mode_photo)) },
52-
leadingIcon = {
53-
Icon(
54-
imageVector = Icons.Filled.Camera,
55-
contentDescription = null,
56-
modifier = Modifier.size(18.dp),
57-
)
58-
}
55+
icon = Icons.Filled.Camera,
56+
contentDescription = stringResource(R.string.camera_mode_photo),
57+
iconRotation = iconRotation,
5958
)
60-
Spacer(modifier = Modifier.width(8.dp))
61-
FilterChip(
59+
ModeToggleButton(
6260
selected = captureMode == CaptureMode.VIDEO,
6361
onClick = { onModeChange(CaptureMode.VIDEO) },
6462
enabled = !isRecording && !isLoading,
65-
label = { Text(stringResource(R.string.camera_mode_video)) },
66-
leadingIcon = {
67-
Icon(
68-
imageVector = Icons.Filled.Videocam,
69-
contentDescription = null,
70-
modifier = Modifier.size(18.dp),
71-
)
72-
}
63+
icon = Icons.Filled.Videocam,
64+
contentDescription = stringResource(R.string.camera_mode_video),
65+
iconRotation = iconRotation,
7366
)
7467
}
7568

@@ -85,7 +78,9 @@ fun BottomCameraControls(
8578
Icon(
8679
imageVector = Icons.Filled.Settings,
8780
contentDescription = stringResource(R.string.camera_settings_button),
88-
modifier = Modifier.size(32.dp),
81+
modifier = Modifier
82+
.size(32.dp)
83+
.rotate(iconRotation),
8984
)
9085
}
9186

@@ -110,7 +105,9 @@ fun BottomCameraControls(
110105
imageVector = Icons.Filled.Camera,
111106
contentDescription = stringResource(id = R.string.camera_capture_content_description),
112107
tint = MaterialTheme.colorScheme.onPrimary,
113-
modifier = Modifier.size(32.dp),
108+
modifier = Modifier
109+
.size(32.dp)
110+
.rotate(iconRotation),
114111
)
115112
}
116113
}
@@ -146,7 +143,9 @@ fun BottomCameraControls(
146143
imageVector = if (isRecording) Icons.Filled.Stop else Icons.Filled.FiberManualRecord,
147144
contentDescription = null,
148145
tint = Color.White,
149-
modifier = Modifier.size(32.dp),
146+
modifier = Modifier
147+
.size(32.dp)
148+
.rotate(iconRotation),
150149
)
151150
}
152151
}
@@ -161,9 +160,42 @@ fun BottomCameraControls(
161160
Icon(
162161
imageVector = Icons.Filled.PhotoLibrary,
163162
contentDescription = stringResource(id = R.string.camera_gallery_content_description),
164-
modifier = Modifier.size(32.dp),
163+
modifier = Modifier
164+
.size(32.dp)
165+
.rotate(iconRotation),
165166
)
166167
}
167168
}
168169
}
170+
}
171+
172+
@Composable
173+
private fun ModeToggleButton(
174+
selected: Boolean,
175+
onClick: () -> Unit,
176+
enabled: Boolean,
177+
icon: ImageVector,
178+
contentDescription: String,
179+
iconRotation: Float,
180+
) {
181+
FilledIconToggleButton(
182+
checked = selected,
183+
onCheckedChange = { onClick() },
184+
enabled = enabled,
185+
modifier = Modifier.size(48.dp),
186+
colors = IconButtonDefaults.filledIconToggleButtonColors(
187+
containerColor = Color.Transparent,
188+
contentColor = Color.White.copy(alpha = 0.6f),
189+
checkedContainerColor = MaterialTheme.colorScheme.primaryContainer,
190+
checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
191+
),
192+
) {
193+
Icon(
194+
imageVector = icon,
195+
contentDescription = contentDescription,
196+
modifier = Modifier
197+
.size(24.dp)
198+
.rotate(iconRotation),
199+
)
200+
}
169201
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraContent.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package com.darkrockstudios.app.securecamera.camera
33
import android.Manifest
44
import android.annotation.SuppressLint
55
import android.content.pm.ActivityInfo
6+
import android.view.OrientationEventListener
67
import androidx.activity.compose.LocalActivity
8+
import androidx.compose.animation.core.animateFloatAsState
9+
import androidx.compose.animation.core.tween
710
import androidx.compose.foundation.layout.Box
811
import androidx.compose.foundation.layout.PaddingValues
912
import androidx.compose.foundation.layout.fillMaxSize
1013
import androidx.compose.runtime.*
1114
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.platform.LocalContext
1216
import com.darkrockstudios.app.securecamera.KeepScreenOnEffect
1317
import com.darkrockstudios.app.securecamera.navigation.NavController
1418
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -32,6 +36,57 @@ private fun LockScreenOrientationPortrait() {
3236
}
3337
}
3438

39+
/**
40+
* Tracks device orientation using the accelerometer and returns the rotation angle
41+
* that UI elements should be rotated to appear upright.
42+
*
43+
* Returns an animated float representing degrees: 0f, 90f, 180f, or 270f
44+
* The animation smoothly transitions between orientations.
45+
*/
46+
@Composable
47+
fun rememberDeviceRotation(): Float {
48+
val context = LocalContext.current
49+
var targetRotation by remember { mutableFloatStateOf(0f) }
50+
51+
DisposableEffect(context) {
52+
val orientationListener = object : OrientationEventListener(context) {
53+
override fun onOrientationChanged(orientation: Int) {
54+
if (orientation == ORIENTATION_UNKNOWN) return
55+
56+
// Map device orientation to UI rotation
57+
// When device is rotated clockwise, UI should rotate counter-clockwise to stay upright
58+
val newRotation = when (orientation) {
59+
in 45 until 135 -> 270f // Device rotated to landscape (left)
60+
in 135 until 225 -> 180f // Device upside down
61+
in 225 until 315 -> 90f // Device rotated to landscape (right)
62+
else -> 0f // Portrait
63+
}
64+
65+
if (newRotation != targetRotation) {
66+
targetRotation = newRotation
67+
}
68+
}
69+
}
70+
71+
if (orientationListener.canDetectOrientation()) {
72+
orientationListener.enable()
73+
}
74+
75+
onDispose {
76+
orientationListener.disable()
77+
}
78+
}
79+
80+
// Animate the rotation smoothly
81+
val animatedRotation by animateFloatAsState(
82+
targetValue = targetRotation,
83+
animationSpec = tween(durationMillis = 300),
84+
label = "device_rotation"
85+
)
86+
87+
return animatedRotation
88+
}
89+
3590
@OptIn(ExperimentalPermissionsApi::class)
3691
@Composable
3792
internal fun CameraContent(
@@ -70,6 +125,8 @@ internal fun CameraContent(
70125
)
71126
}
72127

128+
val deviceRotation = rememberDeviceRotation()
129+
73130
Box(
74131
modifier = modifier
75132
.fillMaxSize()
@@ -87,6 +144,7 @@ internal fun CameraContent(
87144
capturePhoto = capturePhoto,
88145
navController = navController,
89146
paddingValues = paddingValues,
147+
iconRotation = deviceRotation,
90148
)
91149
} else {
92150
NoCameraPermission(navController, permissionsState)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/CameraControls.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.runtime.*
1818
import androidx.compose.runtime.saveable.rememberSaveable
1919
import androidx.compose.ui.Alignment
2020
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.draw.rotate
2122
import androidx.compose.ui.graphics.Color
2223
import androidx.compose.ui.platform.LocalContext
2324
import androidx.compose.ui.platform.testTag
@@ -47,6 +48,7 @@ fun CameraControls(
4748
capturePhoto: MutableState<Boolean?>,
4849
navController: NavController,
4950
paddingValues: PaddingValues,
51+
iconRotation: Float = 0f,
5052
) {
5153
val scope = rememberCoroutineScope()
5254
var isFlashOn by rememberSaveable(cameraController.flashMode) { mutableStateOf(cameraController.flashMode == ImageCapture.FLASH_MODE_ON) }
@@ -139,13 +141,15 @@ fun CameraControls(
139141
cameraController,
140142
modifier = Modifier
141143
.align(Alignment.TopCenter)
142-
.padding(top = paddingValues.calculateTopPadding().plus(64.dp))
144+
.padding(top = paddingValues.calculateTopPadding().plus(64.dp)),
145+
textRotation = iconRotation,
143146
)
144147

145148
LevelIndicator(
146149
modifier = Modifier
147150
.align(Alignment.Center)
148-
.padding(top = paddingValues.calculateTopPadding().plus(16.dp))
151+
.padding(top = paddingValues.calculateTopPadding().plus(16.dp)),
152+
deviceRotation = iconRotation,
149153
)
150154

151155
if (isRecording) {
@@ -171,6 +175,7 @@ fun CameraControls(
171175
Icon(
172176
imageVector = Icons.Filled.MoreVert,
173177
contentDescription = stringResource(id = R.string.camera_more_options_content_description),
178+
modifier = Modifier.rotate(iconRotation),
174179
)
175180
}
176181
}
@@ -197,7 +202,8 @@ fun CameraControls(
197202
},
198203
onLensToggle = { cameraController.toggleLens() },
199204
onClose = { isTopControlsVisible = false },
200-
paddingValues = paddingValues
205+
paddingValues = paddingValues,
206+
iconRotation = iconRotation,
201207
)
202208

203209
BottomCameraControls(
@@ -210,7 +216,8 @@ fun CameraControls(
210216
navController = navController,
211217
onCapture = { doCapturePhoto() },
212218
onToggleRecording = { doToggleRecording() },
213-
onModeChange = { mode -> cameraController.switchCaptureMode(mode) }
219+
onModeChange = { mode -> cameraController.switchCaptureMode(mode) },
220+
iconRotation = iconRotation,
214221
)
215222
}
216223
}

0 commit comments

Comments
 (0)