15
15
*/
16
16
package com.android.ai.samples.imagen
17
17
18
- import android.content.Context
19
18
import android.content.Intent
20
- import android.net.Uri
21
19
import androidx.compose.foundation.Image
20
+ import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
22
21
import androidx.compose.foundation.layout.Column
23
- import androidx.compose.foundation.layout.Row
24
22
import androidx.compose.foundation.layout.Spacer
23
+ import androidx.compose.foundation.layout.WindowInsets
24
+ import androidx.compose.foundation.layout.aspectRatio
25
25
import androidx.compose.foundation.layout.fillMaxSize
26
26
import androidx.compose.foundation.layout.fillMaxWidth
27
27
import androidx.compose.foundation.layout.height
28
+ import androidx.compose.foundation.layout.ime
28
29
import androidx.compose.foundation.layout.padding
29
30
import androidx.compose.foundation.layout.size
31
+ import androidx.compose.foundation.layout.windowInsetsBottomHeight
30
32
import androidx.compose.foundation.layout.wrapContentSize
31
33
import androidx.compose.foundation.rememberScrollState
34
+ import androidx.compose.foundation.text.KeyboardActions
35
+ import androidx.compose.foundation.text.KeyboardOptions
32
36
import androidx.compose.foundation.verticalScroll
33
37
import androidx.compose.material.icons.Icons
34
38
import androidx.compose.material.icons.filled.Code
35
39
import androidx.compose.material.icons.filled.SmartToy
36
40
import androidx.compose.material3.Button
41
+ import androidx.compose.material3.ButtonDefaults
37
42
import androidx.compose.material3.Card
38
43
import androidx.compose.material3.ExperimentalMaterial3Api
39
44
import androidx.compose.material3.Icon
@@ -46,30 +51,40 @@ import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
46
51
import androidx.compose.runtime.Composable
47
52
import androidx.compose.runtime.collectAsState
48
53
import androidx.compose.runtime.getValue
49
- import androidx.compose.runtime.livedata.observeAsState
50
54
import androidx.compose.runtime.mutableStateOf
51
- import androidx.compose.runtime.remember
55
+ import androidx.compose.runtime.saveable.rememberSaveable
52
56
import androidx.compose.runtime.setValue
53
57
import androidx.compose.ui.Alignment
54
58
import androidx.compose.ui.Modifier
55
59
import androidx.compose.ui.graphics.asImageBitmap
56
60
import androidx.compose.ui.layout.ContentScale
57
61
import androidx.compose.ui.platform.LocalContext
58
62
import androidx.compose.ui.res.stringResource
63
+ import androidx.compose.ui.text.input.ImeAction
59
64
import androidx.compose.ui.text.style.TextAlign
65
+ import androidx.compose.ui.tooling.preview.Preview
60
66
import androidx.compose.ui.unit.dp
61
- import androidx.compose.ui.unit.sp
67
+ import androidx.core.net.toUri
62
68
import androidx.hilt.navigation.compose.hiltViewModel
63
69
64
70
@OptIn(ExperimentalMaterial3Api ::class )
65
71
@Composable
66
72
fun ImagenScreen (viewModel : ImagenViewModel = hiltViewModel()) {
67
- val context = LocalContext .current
68
- val isGenerating by viewModel.isGenerating.observeAsState(false )
69
- val generatedBitmap by viewModel.imageGenerated.collectAsState()
73
+ val uiState: ImagenUIState by viewModel.uiState.collectAsState()
70
74
71
- val placeholder = stringResource(R .string.placeholder_prompt)
72
- var editTextValue by remember { mutableStateOf(placeholder) }
75
+ ImagenScreen (
76
+ uiState = uiState,
77
+ onGenerateClick = viewModel::generateImage,
78
+ )
79
+ }
80
+
81
+ @Composable
82
+ @OptIn(ExperimentalMaterial3Api ::class )
83
+ private fun ImagenScreen (
84
+ uiState : ImagenUIState ,
85
+ onGenerateClick : (String ) -> Unit
86
+ ) {
87
+ val isGenerating = uiState is ImagenUIState .Loading
73
88
74
89
Scaffold (
75
90
modifier = Modifier ,
@@ -83,79 +98,173 @@ fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) {
83
98
Text (text = stringResource(R .string.title_image_generation_screen))
84
99
},
85
100
actions = {
86
- SeeCodeButton (context )
101
+ SeeCodeButton ()
87
102
},
88
103
)
89
104
},
90
105
) { innerPadding ->
91
106
Column (
92
107
Modifier
93
- .padding(12 .dp)
94
108
.verticalScroll(rememberScrollState())
109
+ .padding(16 .dp)
95
110
.padding(innerPadding),
96
111
) {
97
- Card (
98
- modifier = Modifier .size(
99
- width = 400 .dp,
100
- height = 400 .dp,
101
- ).align(Alignment .CenterHorizontally ),
102
- ) {
103
- generatedBitmap?.let {
104
- Image (
105
- bitmap = it.asImageBitmap(),
106
- contentDescription = " Picture" ,
107
- contentScale = ContentScale .Fit ,
108
- modifier = Modifier .fillMaxSize(),
109
- )
110
- }
111
- if (isGenerating) {
112
- Text (
113
- text = stringResource(R .string.generating_label),
114
- modifier = Modifier
115
- .fillMaxSize()
116
- .wrapContentSize(Alignment .Center ),
117
- textAlign = TextAlign .Center ,
118
- )
119
- }
120
- }
121
- Spacer (modifier = Modifier .height(24 .dp))
122
- TextField (
123
- value = editTextValue,
124
- onValueChange = { editTextValue = it },
125
- label = { Text (stringResource(R .string.prompt_label)) },
126
- modifier = Modifier .fillMaxWidth(),
112
+ GeneratedContent (
113
+ uiState = uiState,
114
+ modifier = Modifier
115
+ .fillMaxWidth()
116
+ .aspectRatio(1f )
127
117
)
128
- Row {
129
- Button (
130
- modifier = Modifier .padding(vertical = 8 .dp),
131
- onClick = {
132
- viewModel.generateImage(editTextValue)
133
- },
134
- enabled = ! isGenerating,
135
- ) {
136
- Icon (Icons .Default .SmartToy , contentDescription = " Robot" )
137
- Text (modifier = Modifier .padding(start = 8 .dp), text = stringResource(R .string.generate_button))
138
- }
118
+
119
+ Spacer (modifier = Modifier .height(16 .dp))
120
+
121
+ GenerationInput (
122
+ onGenerateClick = onGenerateClick,
123
+ enabled = ! isGenerating,
124
+ modifier = Modifier .fillMaxSize(),
125
+ )
126
+
127
+ // Ensure the screen scrolls when the keyboard appears
128
+ Spacer (Modifier .windowInsetsBottomHeight(WindowInsets .ime))
129
+ }
130
+ }
131
+ }
132
+
133
+ @Composable
134
+ private fun GeneratedContent (uiState : ImagenUIState , modifier : Modifier = Modifier ) {
135
+ Card (
136
+ modifier = modifier,
137
+ ) {
138
+ when (uiState) {
139
+ ImagenUIState .Initial -> {
140
+ // no-op
141
+ }
142
+
143
+ ImagenUIState .Loading -> {
144
+ Text (
145
+ text = stringResource(R .string.generating_label),
146
+ modifier = Modifier
147
+ .fillMaxSize()
148
+ .wrapContentSize(Alignment .Center ),
149
+ textAlign = TextAlign .Center ,
150
+ )
151
+ }
152
+
153
+ is ImagenUIState .ImageGenerated -> {
154
+ Image (
155
+ bitmap = uiState.bitmap.asImageBitmap(),
156
+ contentDescription = uiState.contentDescription,
157
+ contentScale = ContentScale .Fit ,
158
+ modifier = Modifier .fillMaxSize(),
159
+ )
160
+ }
161
+
162
+ is ImagenUIState .Error -> {
163
+ Text (
164
+ text = uiState.message,
165
+ modifier = Modifier
166
+ .fillMaxSize()
167
+ .wrapContentSize(Alignment .Center ),
168
+ textAlign = TextAlign .Center ,
169
+ )
139
170
}
140
171
}
141
172
}
142
173
}
143
174
144
175
@Composable
145
- fun SeeCodeButton (context : Context ) {
176
+ private fun GenerationInput (onGenerateClick : (String ) -> Unit , enabled : Boolean , modifier : Modifier = Modifier ) {
177
+ val placeholder = stringResource(R .string.placeholder_prompt)
178
+ var textFieldValue by rememberSaveable { mutableStateOf(placeholder) }
179
+
180
+ Column (
181
+ verticalArrangement = spacedBy(8 .dp),
182
+ modifier = modifier,
183
+ ) {
184
+ TextField (
185
+ value = textFieldValue,
186
+ onValueChange = { textFieldValue = it },
187
+ label = { Text (stringResource(R .string.prompt_label)) },
188
+ modifier = Modifier .fillMaxWidth(),
189
+ enabled = enabled,
190
+ keyboardOptions = KeyboardOptions (imeAction = ImeAction .Send ),
191
+ keyboardActions = KeyboardActions (
192
+ onSend = {
193
+ onGenerateClick(textFieldValue)
194
+ },
195
+ ),
196
+ )
197
+ Button (
198
+ onClick = {
199
+ onGenerateClick(textFieldValue)
200
+ },
201
+ enabled = enabled,
202
+ contentPadding = ButtonDefaults .ButtonWithIconContentPadding ,
203
+ modifier = modifier.fillMaxWidth(),
204
+ ) {
205
+ Icon (
206
+ Icons .Default .SmartToy ,
207
+ contentDescription = null ,
208
+ modifier = Modifier .size(ButtonDefaults .IconSize ),
209
+ )
210
+ Spacer (Modifier .size(ButtonDefaults .IconSpacing ))
211
+ Text (text = stringResource(R .string.generate_button))
212
+ }
213
+ }
214
+ }
215
+
216
+ @Composable
217
+ private fun SeeCodeButton () {
218
+ val context = LocalContext .current
146
219
val githubLink = " https://github.com/android/ai-samples/tree/main/ai-catalog/samples/imagen"
147
220
Button (
148
221
onClick = {
149
- val intent = Intent (Intent .ACTION_VIEW , Uri .parse(githubLink ))
222
+ val intent = Intent (Intent .ACTION_VIEW , githubLink.toUri( ))
150
223
context.startActivity(intent)
151
224
},
152
- modifier = Modifier .padding(end = 8 .dp) ,
225
+ contentPadding = ButtonDefaults . ButtonWithIconContentPadding ,
153
226
) {
154
- Icon (Icons .Filled .Code , contentDescription = " See code" )
227
+ Icon (Icons .Filled .Code , contentDescription = null )
228
+ Spacer (Modifier .size(ButtonDefaults .IconSpacing ))
155
229
Text (
156
- modifier = Modifier .padding(start = 8 .dp),
157
- fontSize = 12 .sp,
158
230
text = stringResource(R .string.see_code),
159
231
)
160
232
}
161
233
}
234
+
235
+ @Preview
236
+ @Composable
237
+ @OptIn(ExperimentalMaterial3Api ::class )
238
+ private fun ImagenScreenPreview () {
239
+ ImagenScreen (
240
+ uiState = ImagenUIState .Initial ,
241
+ onGenerateClick = {},
242
+ )
243
+ }
244
+
245
+ @Preview
246
+ @Composable
247
+ private fun GeneratedContentPreview () {
248
+ GeneratedContent (
249
+ uiState = ImagenUIState .Initial ,
250
+ modifier = Modifier .size(400 .dp),
251
+ )
252
+ }
253
+
254
+ @Preview
255
+ @Composable
256
+ private fun GeneratedContentLoadingPreview () {
257
+ GeneratedContent (
258
+ uiState = ImagenUIState .Loading ,
259
+ modifier = Modifier .size(400 .dp),
260
+ )
261
+ }
262
+
263
+ @Preview
264
+ @Composable
265
+ private fun GenerationInputPreview () {
266
+ GenerationInput (
267
+ onGenerateClick = {},
268
+ enabled = true ,
269
+ )
270
+ }
0 commit comments