Skip to content

Commit 3d69b03

Browse files
Refactor app bar and status bar for improved UI and edge-to-edge display
This commit addresses the following UI requirements: 1. **Enabled Edge-to-Edge Display:** The application now draws content under the system status bar for a more immersive experience. This was achieved by setting `WindowCompat.setDecorFitsSystemWindows(window, false)` in `MainActivity`. 2. **Transparent Status Bar:** The status bar is now transparent, allowing the app's background color to show through. System UI icons (time, battery, etc.) automatically adjust their color (light/dark) based on the app's theme. This was implemented using Accompanist SystemUiController. 3. **Animated App Title in `MenuScreen`:** - A new `SharedTopAppBar` composable was created, featuring an animated app title ("Screen Operator"). - On app open/`MenuScreen` display, each letter of the app name animates (fades in and slides up) with a slight stagger. - Each letter is rendered in a distinct dark color, ensuring good contrast against both light and dark theme backgrounds. - `MenuScreen` now uses `Scaffold` to host this `SharedTopAppBar`. 4. **`PhotoReasoningScreen` Adjustments:** - This screen does not display the app bar or app name, as per requirements. - Its content is correctly padded to account for the status bar, ensuring no overlap while maintaining the edge-to-edge feel. 5. **General Code Structure:** - Created new composables `AnimatedAppTitle.kt` and `SharedTopAppBar.kt` in `common.ui` package for better organization. - Updated padding and layout structures in `MenuScreen.kt` and `PhotoReasoningScreen.kt` to support the new UI. - Ensured Composable Previews are wrapped in the app theme for consistency. The changes provide a more modern and visually appealing interface, adhering to Material Design principles for app bars and system UI integration.
1 parent e5b437c commit 3d69b03

6 files changed

Lines changed: 177 additions & 25 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@ dependencies {
8787
debugImplementation("androidx.compose.ui:ui-test-manifest")
8888

8989
implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
90+
implementation("com.google.accompanist:accompanist-systemuicontroller:0.30.0")
9091
}
9192

app/src/main/kotlin/com/google/ai/sample/MainActivity.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import android.widget.Toast
2121
import androidx.activity.ComponentActivity
2222
import androidx.activity.compose.setContent
2323
import androidx.activity.result.contract.ActivityResultContracts
24+
import androidx.compose.foundation.isSystemInDarkTheme
2425
import androidx.compose.foundation.layout.Arrangement
2526
import androidx.compose.foundation.layout.Box
2627
import androidx.compose.foundation.layout.Column
@@ -44,10 +45,12 @@ import androidx.compose.runtime.remember
4445
import androidx.compose.runtime.setValue
4546
import androidx.compose.ui.Alignment
4647
import androidx.compose.ui.Modifier
48+
import androidx.compose.ui.graphics.Color
4749
import androidx.compose.ui.platform.LocalContext
4850
import androidx.compose.ui.unit.dp
4951
import androidx.compose.ui.window.Dialog
5052
import androidx.core.content.ContextCompat
53+
import androidx.core.view.WindowCompat
5154
import androidx.lifecycle.lifecycleScope
5255
import androidx.navigation.NavHostController
5356
import androidx.navigation.compose.NavHost
@@ -63,6 +66,7 @@ import com.android.billingclient.api.Purchase
6366
import com.android.billingclient.api.PurchasesUpdatedListener
6467
import com.android.billingclient.api.QueryProductDetailsParams
6568
import com.android.billingclient.api.QueryPurchasesParams
69+
import com.google.accompanist.systemuicontroller.rememberSystemUiController
6670
import com.google.ai.sample.feature.multimodal.PhotoReasoningRoute
6771
import com.google.ai.sample.feature.multimodal.PhotoReasoningViewModel
6872
import com.google.ai.sample.ui.theme.GenerativeAISample
@@ -235,6 +239,7 @@ class MainActivity : ComponentActivity() {
235239

236240
override fun onCreate(savedInstanceState: Bundle?) {
237241
Log.d(TAG, "onCreate: Activity creating.")
242+
WindowCompat.setDecorFitsSystemWindows(window, false)
238243
super.onCreate(savedInstanceState)
239244
instance = this
240245
Log.d(TAG, "onCreate: MainActivity instance set.")
@@ -320,10 +325,19 @@ class MainActivity : ComponentActivity() {
320325
Log.d(TAG, "setContent: Composable content rendering. Current trial state: $currentTrialState")
321326
navController = rememberNavController()
322327
GenerativeAISample {
328+
val systemUiController = rememberSystemUiController()
329+
val useDarkIcons = !isSystemInDarkTheme()
330+
323331
Surface(
324332
modifier = Modifier.fillMaxSize(),
325333
color = MaterialTheme.colorScheme.background
326334
) {
335+
LaunchedEffect(systemUiController, useDarkIcons) {
336+
systemUiController.setStatusBarColor(
337+
color = Color.Transparent,
338+
darkIcons = useDarkIcons
339+
)
340+
}
327341
Log.d(TAG, "setContent: Rendering AppNavigation.")
328342
AppNavigation(navController)
329343

app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.google.ai.sample
22

33
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.PaddingValues
45
import androidx.compose.foundation.layout.Row
56
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
68
import androidx.compose.foundation.layout.fillMaxWidth
79
import androidx.compose.foundation.layout.height
810
import androidx.compose.foundation.layout.padding
@@ -12,7 +14,9 @@ import androidx.compose.material3.Button
1214
import androidx.compose.material3.Card
1315
import androidx.compose.material3.DropdownMenu
1416
import androidx.compose.material3.DropdownMenuItem
17+
import androidx.compose.material3.ExperimentalMaterial3Api
1518
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Scaffold
1620
import androidx.compose.material3.Text
1721
import androidx.compose.material3.TextButton
1822
import androidx.compose.runtime.Composable
@@ -35,13 +39,16 @@ import androidx.compose.ui.text.style.TextDecoration
3539
import androidx.compose.ui.platform.LocalUriHandler
3640
import androidx.compose.ui.unit.sp
3741
import android.widget.Toast
42+
import com.google.ai.sample.common.ui.SharedTopAppBar
43+
import com.google.ai.sample.ui.theme.GenerativeAISample
3844

3945
data class MenuItem(
4046
val routeId: String,
4147
val titleResId: Int,
4248
val descriptionResId: Int
4349
)
4450

51+
@OptIn(ExperimentalMaterial3Api::class)
4552
@Composable
4653
fun MenuScreen(
4754
onItemClicked: (String) -> Unit = { },
@@ -60,19 +67,26 @@ fun MenuScreen(
6067
var selectedModel by remember { mutableStateOf(currentModel) }
6168
var expanded by remember { mutableStateOf(false) }
6269

63-
LazyColumn(
64-
Modifier
65-
.padding(top = 16.dp, bottom = 16.dp)
66-
) {
67-
// API Key Management Button
68-
item {
69-
Card(
70-
modifier = Modifier
71-
.fillMaxWidth()
72-
.padding(horizontal = 16.dp, vertical = 8.dp)
73-
) {
74-
Row(
70+
Scaffold(
71+
topBar = {
72+
SharedTopAppBar()
73+
}
74+
) { innerPadding ->
75+
LazyColumn(
76+
modifier = Modifier
77+
.padding(innerPadding)
78+
.fillMaxSize(),
79+
contentPadding = PaddingValues(vertical = 8.dp)
80+
) {
81+
// API Key Management Button
82+
item {
83+
Card(
7584
modifier = Modifier
85+
.fillMaxWidth()
86+
.padding(horizontal = 16.dp)
87+
) {
88+
Row(
89+
modifier = Modifier
7690
.padding(all = 16.dp)
7791
.fillMaxWidth(),
7892
verticalAlignment = Alignment.CenterVertically
@@ -98,7 +112,7 @@ fun MenuScreen(
98112
Card(
99113
modifier = Modifier
100114
.fillMaxWidth()
101-
.padding(horizontal = 16.dp, vertical = 8.dp)
115+
.padding(horizontal = 16.dp)
102116
) {
103117
Column(
104118
modifier = Modifier
@@ -165,7 +179,7 @@ fun MenuScreen(
165179
Card(
166180
modifier = Modifier
167181
.fillMaxWidth()
168-
.padding(horizontal = 16.dp, vertical = 8.dp)
182+
.padding(horizontal = 16.dp)
169183
) {
170184
Column(
171185
modifier = Modifier
@@ -203,7 +217,7 @@ fun MenuScreen(
203217
Card(
204218
modifier = Modifier
205219
.fillMaxWidth()
206-
.padding(horizontal = 16.dp, vertical = 8.dp)
220+
.padding(horizontal = 16.dp)
207221
) {
208222
Row(
209223
modifier = Modifier
@@ -239,7 +253,7 @@ fun MenuScreen(
239253
Card(
240254
modifier = Modifier
241255
.fillMaxWidth()
242-
.padding(horizontal = 16.dp, vertical = 8.dp)
256+
.padding(horizontal = 16.dp)
243257
) {
244258
val annotatedText = buildAnnotatedString {
245259
append("Screenshots are saved in Pictures/Screenshots and you should delete them afterwards. Google has discontinued free API access to Gemini 2.5 Pro without a deposited billing account. There are rate limits for free use of Gemini models. The less powerful the models are, the more you can use them. The limits range from a maximum of 10 to 30 calls per minute. After each screenshot (every 2-3 seconds) the LLM must respond again. More information is available at ")
@@ -278,20 +292,26 @@ fun MenuScreen(
278292
@Preview(showSystemUi = true)
279293
@Composable
280294
fun MenuScreenPreview() {
281-
// Preview with trial not expired
282-
MenuScreen(isTrialExpired = false, isPurchased = false)
295+
GenerativeAISample {
296+
// Preview with trial not expired
297+
MenuScreen(isTrialExpired = false, isPurchased = false)
298+
}
283299
}
284300

285301
@Preview(showSystemUi = true)
286302
@Composable
287303
fun MenuScreenPurchasedPreview() {
288-
MenuScreen(isTrialExpired = false, isPurchased = true)
304+
GenerativeAISample {
305+
MenuScreen(isTrialExpired = false, isPurchased = true)
306+
}
289307
}
290308

291309
@Preview(showSystemUi = true)
292310
@Composable
293311
fun MenuScreenTrialExpiredPreview() {
294-
// Preview with trial expired
295-
MenuScreen(isTrialExpired = true, isPurchased = false)
312+
GenerativeAISample {
313+
// Preview with trial expired
314+
MenuScreen(isTrialExpired = true, isPurchased = false)
315+
}
296316
}
297317

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.google.ai.sample.common.ui
2+
3+
import androidx.compose.animation.core.*
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.material3.Text
6+
import androidx.compose.runtime.*
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.draw.alpha
9+
import androidx.compose.ui.graphics.Color
10+
import androidx.compose.ui.platform.LocalDensity
11+
import androidx.compose.ui.unit.dp
12+
import androidx.compose.ui.unit.sp
13+
import kotlinx.coroutines.delay
14+
15+
@Composable
16+
fun AnimatedAppTitle(
17+
appName: String = "Screen Operator",
18+
modifier: Modifier = Modifier
19+
) {
20+
val letters = appName.toList()
21+
22+
// Define a list of distinct dark colors for the letters
23+
// These colors should offer good contrast on both light (0xFFFFFBFE) and dark (0xFF1C1B1F) backgrounds.
24+
val darkLetterColors = listOf(
25+
Color(0xFF0D47A1), // Dark Blue
26+
Color(0xFF4A148C), // Dark Purple
27+
Color(0xFF004D40), // Dark Teal
28+
Color(0xFFBF360C), // Dark Orange/Brown
29+
Color(0xFF263238), // Dark Blue Grey
30+
Color(0xFF1B5E20), // Dark Green
31+
Color(0xFF3E2723), // Dark Brown
32+
Color(0xFF880E4F), // Dark Pink/Magenta
33+
Color(0xFF01579B), // Another Dark Blue
34+
Color(0xFF311B92), // Deep Purple
35+
Color(0xFF006064), // Dark Cyan
36+
Color(0xFFD84315), // Deep Orange
37+
Color(0xFF37474F), // Another Blue Grey
38+
Color(0xFF2E7D32), // Medium Dark Green
39+
Color(0xFF4E342E), // Another Dark Brown
40+
Color(0xFFA04000), // Saturated Brown
41+
Color(0xFF1A237E), // Indigo
42+
)
43+
44+
Row(modifier = modifier) {
45+
letters.forEachIndexed { index, letter ->
46+
var visible by remember { mutableStateOf(false) }
47+
val density = LocalDensity.current
48+
49+
val alpha by animateFloatAsState(
50+
targetValue = if (visible) 1f else 0f,
51+
animationSpec = tween(durationMillis = 500),
52+
label = "alpha_anim_${index}"
53+
)
54+
55+
val offsetY by animateDpAsState(
56+
targetValue = if (visible) 0.dp else 20.dp,
57+
animationSpec = tween(durationMillis = 500),
58+
label = "offsetY_anim_${index}"
59+
)
60+
61+
LaunchedEffect(key1 = Unit) {
62+
delay(index * 100L) // Stagger the animation start for each letter
63+
visible = true
64+
}
65+
66+
Text(
67+
text = letter.toString(),
68+
fontSize = 22.sp, // Standard TopAppBar title size
69+
color = darkLetterColors[index % darkLetterColors.size],
70+
modifier = Modifier
71+
.alpha(alpha)
72+
.offset(y = offsetY)
73+
)
74+
}
75+
}
76+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.google.ai.sample.common.ui
2+
3+
import androidx.compose.material3.ExperimentalMaterial3Api
4+
import androidx.compose.material3.SmallTopAppBar
5+
import androidx.compose.material3.TopAppBarDefaults
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.graphics.Color
9+
10+
@OptIn(ExperimentalMaterial3Api::class)
11+
@Composable
12+
fun SharedTopAppBar(
13+
modifier: Modifier = Modifier
14+
) {
15+
SmallTopAppBar(
16+
title = {
17+
AnimatedAppTitle() // Uses the default app name "Screen Operator"
18+
},
19+
colors = TopAppBarDefaults.smallTopAppBarColors(
20+
containerColor = Color.Transparent // Make AppBar transparent
21+
),
22+
modifier = modifier
23+
// SmallTopAppBar automatically handles window insets for the status bar
24+
// when used correctly within a Scaffold or if windowInsets are provided.
25+
// No explicit padding needed here if relying on its default behavior.
26+
)
27+
}

app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ import androidx.compose.foundation.layout.Box
1414
import androidx.compose.foundation.layout.Column
1515
import androidx.compose.foundation.layout.Row
1616
import androidx.compose.foundation.layout.Spacer
17+
import androidx.compose.foundation.layout.WindowInsets
18+
import androidx.compose.foundation.layout.fillMaxSize
1719
import androidx.compose.foundation.layout.fillMaxWidth
1820
import androidx.compose.foundation.layout.height
1921
import androidx.compose.foundation.layout.padding
2022
import androidx.compose.foundation.layout.requiredSize
23+
import androidx.compose.foundation.layout.statusBars
24+
import androidx.compose.foundation.layout.windowInsetsPadding
2125
import androidx.compose.foundation.lazy.LazyColumn
2226
import androidx.compose.foundation.lazy.LazyRow
2327
import androidx.compose.foundation.lazy.items
@@ -68,6 +72,7 @@ import com.google.ai.sample.MainActivity
6872
import coil.size.Precision
6973
import com.google.ai.sample.R
7074
import com.google.ai.sample.ScreenOperatorAccessibilityService
75+
import com.google.ai.sample.ui.theme.GenerativeAISample
7176
import com.google.ai.sample.util.Command
7277
import com.google.ai.sample.util.UriSaver
7378
import kotlinx.coroutines.launch
@@ -217,6 +222,8 @@ fun PhotoReasoningScreen(
217222

218223
Column(
219224
modifier = Modifier
225+
.fillMaxSize()
226+
.windowInsetsPadding(WindowInsets.statusBars)
220227
.padding(all = 16.dp)
221228
) {
222229
Card(
@@ -637,9 +644,10 @@ fun ErrorChatBubble(
637644
@Preview
638645
@Composable
639646
fun PhotoReasoningScreenPreviewWithContent() {
640-
PhotoReasoningScreen(
641-
uiState = PhotoReasoningUiState.Success("This is a preview of the photo reasoning screen."),
642-
commandExecutionStatus = "Command executed: Take screenshot",
647+
GenerativeAISample {
648+
PhotoReasoningScreen(
649+
uiState = PhotoReasoningUiState.Success("This is a preview of the photo reasoning screen."),
650+
commandExecutionStatus = "Command executed: Take screenshot",
643651
detectedCommands = listOf(
644652
Command.TakeScreenshot,
645653
Command.ClickButton("OK")
@@ -659,9 +667,15 @@ fun PhotoReasoningScreenPreviewWithContent() {
659667
)
660668
}
661669

670+
)
671+
}
672+
}
673+
662674
@Composable
663675
@Preview(showSystemUi = true)
664676
fun PhotoReasoningScreenPreviewEmpty() {
665-
PhotoReasoningScreen(isKeyboardOpen = false)
677+
GenerativeAISample {
678+
PhotoReasoningScreen(isKeyboardOpen = false)
679+
}
666680
}
667681

0 commit comments

Comments
 (0)