Search code examples
androidkotlinfocusandroid-jetpack-composetouch-event

How to know when a LazyColumn (or any Composable for that matter) has been touched?


I have the following screen built in Compose -

@Composable
fun DashboardScreen(heroesViewModel: HeroesViewModel = get()) {

    val searchState by heroesViewModel.searchState.collectAsState()
    val uiState by heroesViewModel.uiState.collectAsState()
    val focusRequester = remember { FocusRequester() }

    Column(modifier = Modifier.fillMaxSize()) {

        SearchBar(
            searchState = searchState,
            onQueryChanged = { text ->
                heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchQueryChanged(text))
            },
            onSearchFocusChange = { focused ->
                heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchBarFocusChanged(focused))
            },
            onClearQueryClicked = {
                heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ClearQueryClicked)
            },
            onBack = {},
            focusRequester
        )

        LazyColumn {
            items(uiState.modelsListResponse ?: listOf()) { model ->
                if (model is HeroListSeparatorModel)
                    HeroesListSeparatorItem(model)
                else if (model is HeroesListModel)
                    HeroesListItem(model) {
                        heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
                    }
            }
        }
    }
}

And here is my SearchBar -

@Composable
fun SearchBar(
    searchState: SearchState,
    onQueryChanged: (String) -> Unit,
    onSearchFocusChange: (Boolean) -> Unit,
    onClearQueryClicked: () -> Unit,
    onBack: () -> Unit,
    focusRequester : FocusRequester,
    modifier: Modifier = Modifier
) {
    val focusManager = LocalFocusManager.current
    val keyboardController = LocalSoftwareKeyboardController.current
    val focused = searchState.focused

    Row(
        modifier = modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {

        AnimatedVisibility(visible = focused) {
            BackButton(focusManager, keyboardController, onBack)
        }

        SearchTextField(
            searchState,
            onQueryChanged,
            onSearchFocusChange,
            onClearQueryClicked,
            focusRequester
        )
    }
}

@Composable
fun SearchTextField(
    searchState: SearchState,
    onQueryChanged: (String) -> Unit,
    onSearchFocusChanged: (Boolean) -> Unit,
    onClearQueryClicked: () -> Unit,
    focusRequester : FocusRequester,
    modifier: Modifier = Modifier
) {
    val focused = searchState.focused
    var query = searchState.query
    val searching = searchState.searching

    Surface(
        modifier = modifier
            .then(
                Modifier
                    .height(56.dp)
                    .padding(
                        top = 8.dp, bottom = 8.dp,
                        start = if (focused.not()) 16.dp else 0.dp,
                        end = 16.dp
                    )
            ),
        color = Color(0xffF5F5F5),
        shape = RoundedCornerShape(percent = 50)
    ) {

        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {

            Box(
                contentAlignment = Alignment.CenterStart,
                modifier = modifier
            ) {

                if (query.isEmpty()) {
                    SearchHint(modifier = modifier.padding(start = 24.dp, end = 8.dp))
                }

                Row(verticalAlignment = Alignment.CenterVertically) {
                    BasicTextField(
                        value = query,
                        onValueChange = {
                            query = it
                            onQueryChanged(it)
                        },
                        modifier = Modifier
                            .fillMaxSize()
                            .weight(1f)
                            .onFocusChanged { focusState ->
                                onSearchFocusChanged(focusState.isFocused)
                            }
                            .focusRequester(focusRequester)
                            .padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
                        singleLine = true
                    )
                    when {
                        searching -> {
                            CircularProgressIndicator(
                                modifier = Modifier
                                    .padding(horizontal = 16.dp)
                                    .width(25.dp)
                                    .size(24.dp)
                            )
                        }

                        query.isNotEmpty() -> {
                            IconButton(onClick = onClearQueryClicked) {
                                Icon(
                                    imageVector = Icons.Filled.Close,
                                    contentDescription = null
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun SearchHint(
    modifier: Modifier = Modifier,
    hint: String = "Enter a hero name"
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxSize()
            .then(modifier)

    ) {
        Text(
            text = hint,
            color = Color(0xff757575)
        )
    }
}


class SearchState(
    query: String,
    focused: Boolean,
    searching: Boolean,
) {
    
    var query by mutableStateOf(query)
    var focused by mutableStateOf(focused)
    var searching by mutableStateOf(searching)
    
}

What I want to achieve is the ability to know when the user has tapped or click anywhere outside of my SearchBar Composable. I want to send an event to the ViewModel so that he recomposes the screen, removing the keyboard and removing the cursor I have on my SearchBar (basically just resetting the focus).

I have tried using the focusRequester like I did in my SearchBar but without success as nothing happened, tried using the clickable {} block which is not what I need (I need the tap and not the click) and tried using Modifier.pointerInput with detectTapGestures and it did not work, not at the root LazyColumn and not at the ListItem level.

What I am looking for should be something really easy.


Solution

  • Found the answer - I ended up using isScrollInProgress variable from LazyListState class that provides a boolean indicating if the list is currently in scrolling. When the value was true I removed the focus from where I needed to remove it and it worked :)

    Attached the solution -

    val listState = rememberLazyListState()
    
    ...
    LazyColumn(state = listState) {
                items(uiState.modelsListResponse ?: listOf()) { model ->
                    heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListIsScrolling(listState.isScrollInProgress))
                    if (model is HeroListSeparatorModel)
                        HeroesListSeparatorItem(model)
                    else if (model is HeroesListModel)
                        HeroesListItem(model) {
                            heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
                        }
                }
            }