Skip to content

Commit 8a7e935

Browse files
System message dynamic height (#23)
* feat: Adjust system message height and back button behavior Implements dynamic height changes for the system message text field in the PhotoReasoningScreen based on focus and keyboard visibility: - Focused + Keyboard open: 600dp - Focused + Keyboard closed: 1000dp - Not focused: 120dp Additionally, modifies back button behavior: - If the system message field is focused with the keyboard closed (1000dp height), the first back press deselects the field, changing its height to 120dp. - Subsequent back presses perform the default navigation. Keyboard visibility is detected in MainActivity and propagated to the PhotoReasoningScreen. * fix: Add missing import for onFocusChanged Adds the import `androidx.compose.ui.focus.onFocusChanged` to `PhotoReasoningScreen.kt` to resolve a build compilation error. * fix: Correct system message TextField behavior Addresses your feedback on the system message TextField in PhotoReasoningScreen: - Sets focused height with keyboard to 450dp (was 600dp). - Dynamically adjusts minLines and maxLines of the OutlinedTextField to ensure the text input area expands with the component's height. - Modifies the BackHandler to explicitly clear focus from the TextField (in addition to collapsing it) when it's focused without the keyboard and back is pressed. This ensures it can be re-expanded correctly on subsequent focus. These changes improve the usability and appearance of the system message input field. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent ef21d53 commit 8a7e935

2 files changed

Lines changed: 62 additions & 7 deletions

File tree

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import android.content.SharedPreferences
1111
import android.content.pm.PackageManager
1212
import android.net.Uri
1313
import android.os.Build
14+
import android.graphics.Rect
1415
import android.os.Bundle
1516
import android.provider.Settings
1617
import android.util.Log
18+
import android.view.View
19+
import android.view.ViewTreeObserver
1720
import android.widget.Toast
1821
import androidx.activity.ComponentActivity
1922
import androidx.activity.compose.setContent
@@ -70,6 +73,11 @@ import kotlinx.coroutines.launch
7073

7174
class MainActivity : ComponentActivity() {
7275

76+
// Keyboard Visibility
77+
private val _isKeyboardOpen = MutableStateFlow(false)
78+
val isKeyboardOpen: StateFlow<Boolean> = _isKeyboardOpen.asStateFlow()
79+
private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
80+
7381
private var photoReasoningViewModel: PhotoReasoningViewModel? = null
7482
private lateinit var apiKeyManager: ApiKeyManager
7583
private var showApiKeyDialog by mutableStateOf(false)
@@ -286,6 +294,26 @@ class MainActivity : ComponentActivity() {
286294
// Initial check for accessibility service status
287295
refreshAccessibilityServiceStatus()
288296

297+
// Keyboard visibility listener
298+
val rootView = findViewById<View>(android.R.id.content)
299+
onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
300+
val rect = Rect()
301+
rootView.getWindowVisibleDisplayFrame(rect)
302+
val screenHeight = rootView.rootView.height
303+
val keypadHeight = screenHeight - rect.bottom
304+
if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is a common threshold
305+
if (!_isKeyboardOpen.value) {
306+
_isKeyboardOpen.value = true
307+
Log.d(TAG, "Keyboard visible")
308+
}
309+
} else {
310+
if (_isKeyboardOpen.value) {
311+
_isKeyboardOpen.value = false
312+
Log.d(TAG, "Keyboard hidden")
313+
}
314+
}
315+
}
316+
rootView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
289317

290318
Log.d(TAG, "onCreate: Calling setContent.")
291319
setContent {
@@ -752,6 +780,11 @@ class MainActivity : ComponentActivity() {
752780
billingClient.endConnection()
753781
Log.d(TAG, "onDestroy: BillingClient connection ended.")
754782
}
783+
// Remove keyboard listener
784+
onGlobalLayoutListener?.let {
785+
findViewById<View>(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(it)
786+
Log.d(TAG, "onDestroy: Keyboard layout listener removed.")
787+
}
755788
if (this == instance) {
756789
instance = null
757790
Log.d(TAG, "onDestroy: MainActivity instance cleared.")

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.graphics.drawable.BitmapDrawable
66
import android.net.Uri
77
import android.provider.Settings
88
import android.widget.Toast // Added for Toast message
9+
import androidx.activity.compose.BackHandler
910
import androidx.activity.compose.rememberLauncherForActivityResult
1011
import androidx.activity.result.PickVisualMediaRequest
1112
import androidx.activity.result.contract.ActivityResultContracts
@@ -49,9 +50,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
4950
import androidx.compose.runtime.setValue
5051
import androidx.compose.ui.Alignment
5152
import androidx.compose.ui.Modifier
53+
import androidx.compose.ui.focus.onFocusChanged
5254
import androidx.compose.ui.draw.drawBehind
5355
import androidx.compose.ui.graphics.Color
5456
import androidx.compose.ui.platform.LocalContext
57+
import androidx.compose.ui.platform.LocalFocusManager
5558
import androidx.compose.ui.res.stringResource
5659
import androidx.compose.ui.tooling.preview.Preview
5760
import androidx.compose.ui.unit.dp
@@ -88,6 +91,7 @@ internal fun PhotoReasoningRoute(
8891

8992
// Observe the accessibility service status from MainActivity
9093
val isAccessibilityServiceEffectivelyEnabled by mainActivity?.isAccessibilityServiceEnabledFlow?.collectAsState() ?: mutableStateOf(false)
94+
val isKeyboardOpen by mainActivity?.isKeyboardOpen?.collectAsState() ?: mutableStateOf(false)
9195

9296
// Launcher for opening accessibility settings
9397
val accessibilitySettingsLauncher = rememberLauncherForActivityResult(
@@ -168,7 +172,8 @@ internal fun PhotoReasoningRoute(
168172
val vm = it.getPhotoReasoningViewModel()
169173
vm?.clearChatHistory(context)
170174
}
171-
}
175+
},
176+
isKeyboardOpen = isKeyboardOpen
172177
)
173178
}
174179

@@ -183,12 +188,20 @@ fun PhotoReasoningScreen(
183188
onReasonClicked: (String, List<Uri>) -> Unit = { _, _ -> },
184189
isAccessibilityServiceEnabled: Boolean = false,
185190
onEnableAccessibilityService: () -> Unit = {},
186-
onClearChatHistory: () -> Unit = {}
191+
onClearChatHistory: () -> Unit = {},
192+
isKeyboardOpen: Boolean
187193
) {
188194
var userQuestion by rememberSaveable { mutableStateOf("") }
189195
val imageUris = rememberSaveable(saver = UriSaver()) { mutableStateListOf() }
196+
var isSystemMessageFocused by rememberSaveable { mutableStateOf(false) }
190197
val listState = rememberLazyListState()
191198
val context = LocalContext.current // Get context for Toast
199+
val focusManager = LocalFocusManager.current
200+
201+
BackHandler(enabled = isSystemMessageFocused && !isKeyboardOpen) {
202+
focusManager.clearFocus() // Clear focus first
203+
isSystemMessageFocused = false // Then update the state that controls height
204+
}
192205

193206
val pickMedia = rememberLauncherForActivityResult(
194207
ActivityResultContracts.PickVisualMedia()
@@ -223,15 +236,23 @@ fun PhotoReasoningScreen(
223236
color = MaterialTheme.colorScheme.onPrimaryContainer
224237
)
225238
Spacer(modifier = Modifier.height(8.dp))
239+
val systemMessageHeight = when {
240+
isSystemMessageFocused && isKeyboardOpen -> 450.dp // Changed from 600.dp
241+
isSystemMessageFocused && !isKeyboardOpen -> 1000.dp
242+
else -> 120.dp
243+
}
244+
val currentMinLines = if (systemMessageHeight == 120.dp) 3 else 1
245+
val currentMaxLines = if (systemMessageHeight == 120.dp) 5 else Int.MAX_VALUE
226246
OutlinedTextField(
227247
value = systemMessage,
228248
onValueChange = onSystemMessageChanged,
229249
placeholder = { Text("Enter a system message here that will be sent with every request") },
230250
modifier = Modifier
231251
.fillMaxWidth()
232-
.height(120.dp),
233-
maxLines = 5,
234-
minLines = 3
252+
.height(systemMessageHeight)
253+
.onFocusChanged { focusState -> isSystemMessageFocused = focusState.isFocused },
254+
minLines = currentMinLines,
255+
maxLines = currentMaxLines
235256
)
236257
}
237258
}
@@ -633,13 +654,14 @@ fun PhotoReasoningScreenPreviewWithContent() {
633654
text = "I am here to help you. What do you want to know?",
634655
participant = PhotoParticipant.MODEL
635656
)
636-
)
657+
),
658+
isKeyboardOpen = false
637659
)
638660
}
639661

640662
@Composable
641663
@Preview(showSystemUi = true)
642664
fun PhotoReasoningScreenPreviewEmpty() {
643-
PhotoReasoningScreen()
665+
PhotoReasoningScreen(isKeyboardOpen = false)
644666
}
645667

0 commit comments

Comments
 (0)