From 4f435f1d32d2be4c5ed89939663361799e8800fb Mon Sep 17 00:00:00 2001 From: hbb20 Date: Mon, 18 Dec 2023 10:08:23 -0700 Subject: [PATCH] add compose-support --- .../compose/ComposeDemoActivity.kt | 346 +++++++++++++++++- buildSrc/src/main/java/Dependencies.kt | 20 +- .../compose/CountryPickerComposable.kt | 208 ++++++----- .../src/main/res/drawable/ic_cp_search.xml | 5 + 4 files changed, 480 insertions(+), 99 deletions(-) create mode 100644 countrypicker/src/main/res/drawable/ic_cp_search.xml diff --git a/app/src/main/java/com/hbb20/androidcountrypicker/compose/ComposeDemoActivity.kt b/app/src/main/java/com/hbb20/androidcountrypicker/compose/ComposeDemoActivity.kt index 790cd70..97f7cdb 100644 --- a/app/src/main/java/com/hbb20/androidcountrypicker/compose/ComposeDemoActivity.kt +++ b/app/src/main/java/com/hbb20/androidcountrypicker/compose/ComposeDemoActivity.kt @@ -3,17 +3,49 @@ package com.hbb20.androidcountrypicker.compose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hbb20.androidcountrypicker.R +import com.hbb20.contrypicker.flagpack1.FlagPack1 +import com.hbb20.countrypicker.compose.CountryFlagLayout import com.hbb20.countrypicker.compose.CountryPicker +import com.hbb20.countrypicker.compose.CountryPickerDialog +import com.hbb20.countrypicker.compose.rememberAutoDetectedCountryCode +import com.hbb20.countrypicker.compose.rememberCPDataStore +import com.hbb20.countrypicker.compose.rememberCountry +import com.hbb20.countrypicker.compose.rememberCountryFlag +import com.hbb20.countrypicker.flagprovider.CPFlagImageProvider +import com.hbb20.countrypicker.flagprovider.DefaultEmojiFlagProvider class ComposeDemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -26,15 +58,325 @@ class ComposeDemoActivity : ComponentActivity() { color = MaterialTheme.colors.background ) { val (countryCode, setCountryCode) = remember { mutableStateOf("In") } - Column { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { CountryPicker(alpha2Code = countryCode) { setCountryCode(it?.alpha2) } + OutOfBox() + AutoDetectedInitialCountry() + UseFlagPack() + UseCustomFlagDrawables() + UseNoFlag() + PhoneCodePicker() + CountryCurrencyPicker() } } } } } + + @Composable + private fun OutOfBox() { + CardTemplate( + "Out of box", + "This is how it works without any other config changes" + ) { + val (countryCode, setCountryCode) = remember { mutableStateOf(null) } + CountryPicker(alpha2Code = countryCode) { + setCountryCode(it?.alpha2) + } + } + } + + @Composable + private fun AutoDetectedInitialCountry() { + CardTemplate( + "Initial auto detected country", + "Detect country and set it as initial country. Optionally pass order of sources (SIM, NETWORK, LOCALE) for country detection.", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + // Note: for state management, + // if you need to hoist the selected country code outside of compose + // then use CPCountryDetector::detectCountry() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + CountryPicker(alpha2Code = countryCode) { + setCountryCode(it?.alpha2) + } + } + } + + + @Composable + private fun UseFlagPack() { + CardTemplate( + "Use flag pack", + "Use flag pack images instead of emoji flags", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + CountryPicker( + alpha2Code = countryCode, + flagProvider = CPFlagImageProvider( + FlagPack1.alpha2ToFlag, + FlagPack1.missingFlagPlaceHolder + ) + ) { + setCountryCode(it?.alpha2) + } + } + } + + @Composable + private fun UseNoFlag() { + CardTemplate( + "Dont show flag", + "Dont show flags", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + CountryPicker( + alpha2Code = countryCode, + flagProvider = null, + ) { + setCountryCode(it?.alpha2) + } + } + } + + + @Composable + private fun UseCustomFlagDrawables() { + CardTemplate( + "Custom flag images", + "Use your flag drawables", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + CountryPicker( + alpha2Code = countryCode, + flagProvider = CPFlagImageProvider( + // map of alpha2 to drawable resource id + alpha2ToFlag = mapOf( + "IN" to R.drawable.ic_flag, + "US" to R.drawable.ic_flag, + "GB" to R.drawable.ic_flag, + ), + // drawable resource id for missing flag + missingFlagPlaceHolder = R.drawable.ic_flag + ) + ) { + setCountryCode(it?.alpha2) + } + } + } + + @Composable + private fun PhoneCodePicker() { + CardTemplate( + "Use as Country Phone Code Picker", + "Commonly used to select country with phone input box. Note: This does not auto format the phone number. Phone number formatting is beyond the scope of this library.", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + val cpDataStore = rememberCPDataStore() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + val (phoneNumber, setPhoneNumber) = remember { mutableStateOf("") } + val (showPickerDialog, setShowPickerDialog) = remember { mutableStateOf(false) } + val flagProvider = DefaultEmojiFlagProvider() + + Row( + modifier = Modifier.height(IntrinsicSize.Min), + verticalAlignment = CenterVertically + ) { + // + Row(modifier = Modifier + .fillMaxHeight() + .clickable { setShowPickerDialog(true) } + .padding(8.dp), verticalAlignment = CenterVertically) { + val country = rememberCountry(countryCode) + val flag = rememberCountryFlag(country, flagProvider) + + CountryFlagLayout(flag) + Spacer(modifier = Modifier.padding(4.dp)) + val text = + if (country == null) cpDataStore.messageGroup.selectionPlaceholderText else "+${country.phoneCode} " + Text(text = text) + Icon( + painter = painterResource(id = R.drawable.ic_arrow_drop_down), + contentDescription = null, + modifier = Modifier + .padding(4.dp) + .size(16.dp) + ) + } + TextField( + value = phoneNumber, + onValueChange = { setPhoneNumber(it) }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + } + + if(showPickerDialog){ + CountryPickerDialog( + cpDataStore = cpDataStore, + flagProvider = flagProvider, + onDismissRequest = { setShowPickerDialog(false) }, + countryRowLayout = { country, flagProvider, onClicked -> + Row(modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clickable { onClicked(country) } + .padding(12.dp), + verticalAlignment = CenterVertically) { + CountryFlagLayout(rememberCountryFlag(country, flagProvider)) + Spacer(modifier = Modifier.padding(4.dp)) + Text( + text = country.name, + modifier = Modifier.weight(1f) + ) + Text(text = "+${country.phoneCode}") + } + }, + queryFilter = { country, query -> + val properties = listOf( + country.name, + country.englishName, + country.alpha2, + country.alpha3, + country.phoneCode.toString(), + ) + properties.any { it.contains(query, ignoreCase = true) } + }, + ) { + setCountryCode(it?.alpha2) + // your logic to use phone code + } + } + } + } + + @Composable + private fun CountryCurrencyPicker() { + CardTemplate( + "Use as Country's Currency Picker", + "Commonly used to select currency with money input. " + + "\nNote: This does not auto format the phone number." + + " Phone number formatting is beyond the scope of this library.", + ) { + val initialCountryCode = rememberAutoDetectedCountryCode() + val (countryCode, setCountryCode) = remember { mutableStateOf(initialCountryCode) } + val (amount, setAmount) = remember { mutableStateOf("") } + CountryPicker(alpha2Code = countryCode, + cpDataStore = rememberCPDataStore { originalCountryList -> + originalCountryList.sortedBy { it.currencyName } + .filterNot { it.currencyCode.isBlank() } + }, + selectedCountryLayout = { country, flag, emptySelectionText, modifier, showCountryPickerDialog -> + Row( + modifier = Modifier.height(IntrinsicSize.Min), + verticalAlignment = CenterVertically + ) { + Row(modifier = modifier + .fillMaxHeight() + .clickable { showCountryPickerDialog() } + .padding(8.dp), verticalAlignment = CenterVertically) { + CountryFlagLayout(flag) + Spacer(modifier = Modifier.padding(4.dp)) + val text = + if (country == null) emptySelectionText else "${country.currencyCode} ${country.currencySymbol}" + Text(text = text) + } + TextField( + value = amount, + onValueChange = { setAmount(it) }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + } + }, + pickerDialog = { cpDataStore, flagProvider, onDismissRequest, onCountrySelected -> + CountryPickerDialog( + cpDataStore = cpDataStore, + flagProvider = flagProvider, + onDismissRequest = onDismissRequest, + countryRowLayout = { country, flagProvider, onClicked -> + Row(modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .clickable { onClicked(country) } + .padding(12.dp), + verticalAlignment = CenterVertically) { + CountryFlagLayout(rememberCountryFlag(country, flagProvider)) + Spacer(modifier = Modifier.padding(4.dp)) + Column(modifier = Modifier.weight(1f)) { + + Text( + text = "${country.currencyName} (${country.currencySymbol})", + style = MaterialTheme.typography.subtitle2 + ) + Text( + text = country.name, + style = MaterialTheme.typography.body2 + ) + } + Text( + text = "${country.currencyCode}", + style = MaterialTheme.typography.subtitle1 + ) + } + }, + queryFilter = { country, query -> + val sanitizedQuery = query.trim().removePrefix("+") + val properties = listOf( + country.name, + country.englishName, + country.alpha2, + country.alpha3, + country.currencyName, + country.currencyCode, + country.currencySymbol, + ) + properties.any { it.contains(sanitizedQuery, ignoreCase = true) } + }, + ) { + setCountryCode(it?.alpha2) + val currencyCode = it?.currencyCode + // your logic to use currency code + } + } + ) { + setCountryCode(it?.alpha2) + } + } + } + + + @Composable + private fun CardTemplate( + title: String, + body: String, + countryPickerLayout: @Composable () -> Unit + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.surface) + .padding(16.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = title, + style = MaterialTheme.typography.subtitle1, + color = Color.Black + ) + Text( + text = body, + style = MaterialTheme.typography.body2, + color = Color.DarkGray + ) + Spacer(modifier = Modifier.padding(8.dp)) + countryPickerLayout() + } + } + } } @Composable @@ -51,4 +393,4 @@ fun GreetingPreview() { MaterialTheme { Greeting("Android") } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 3e1c3ef..9b8c66c 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -26,7 +26,7 @@ object Versions { const val MOSHI = "1.9.3" // v1.10+ causes build dependency error. needs investigation. const val MOSHI_SEALED = "0.2.0" const val RETROFIT = "2.9.0" - const val NAVIGATION_GRAPH = "2.7.2" + const val NAVIGATION_GRAPH = "2.7.5" const val SCARLET_VERSION = "0.1.11" const val KOTLIN = "1.4.21" const val KTLINT_GRADLE = "9.4.1" @@ -49,20 +49,20 @@ object Versions { // https://github.com/google/ksp/releases // First half of KSP version shows compatible KOTLIN version, // For example, ksp version 1.8.0-1.0.9 means it's compatible with KOTLIN 1.8.0 - const val COMPOSE_COMPILER = "1.5.3" - const val KOTLIN_GRADLE_PLUGIN = "1.9.10" // STOP : See 'Linked Dependencies' comment - const val KSP = "$KOTLIN_GRADLE_PLUGIN-1.0.13" // STOP : See 'Linked Dependencies' comment + const val COMPOSE_COMPILER = "1.5.4" + const val KOTLIN_GRADLE_PLUGIN = "1.9.20" // STOP : See 'Linked Dependencies' comment + const val KSP = "$KOTLIN_GRADLE_PLUGIN-1.0.14" // STOP : See 'Linked Dependencies' comment } object Deps { const val activityKtx = "androidx.activity:activity-ktx:1.2.0-rc01" const val appCompat = "androidx.appcompat:appcompat:1.6.1" // BOM to library mapping: https://developer.android.com/jetpack/compose/bom/bom-mapping - const val composeBoM = "androidx.compose:compose-bom:2023.09.00" + const val composeBoM = "androidx.compose:compose-bom:2023.10.01" const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.4" const val emoji = "androidx.emoji:emoji:1.1.0" - const val fragmentKtx = "androidx.fragment:fragment-ktx:1.6.1" - const val googleMaterialDesign = "com.google.android.material:material:1.11.0-alpha02" + const val fragmentKtx = "androidx.fragment:fragment-ktx:1.6.2" + const val googleMaterialDesign = "com.google.android.material:material:1.12.0-alpha01" const val viewModels = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" const val timber = "com.jakewharton.timber:timber:5.0.1" const val apacheCSV = "org.apache.commons:commons-csv:1.7" @@ -105,12 +105,12 @@ fun DependencyHandlerScope.implementTesting() { // https://github.com/robolectric/robolectric/issues/5848 Will be able to update to 4.4 only after mockk can be updated to > 1.10.0 - add(TEST_IMPLEMENTATION, "org.robolectric:robolectric:4.10.3") + add(TEST_IMPLEMENTATION, "org.robolectric:robolectric:4.11.1") add(TEST_IMPLEMENTATION, "androidx.arch.core:core-testing:2.2.0") add(TEST_IMPLEMENTATION, "androidx.test:core:1.5.0") add(TEST_IMPLEMENTATION, "androidx.test:core-ktx:1.5.0") - add(TEST_IMPLEMENTATION, "io.mockk:mockk:1.13.7") + add(TEST_IMPLEMENTATION, "io.mockk:mockk:1.13.8") add(TEST_IMPLEMENTATION, "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.COROUTINES}") add(TEST_IMPLEMENTATION, "junit:junit:4.13.2") } @@ -135,7 +135,7 @@ fun DependencyHandlerScope.implementCompose() { // Integration with observables add(IMPLEMENTATION, "androidx.compose.runtime:runtime") add(IMPLEMENTATION, "androidx.compose.runtime:runtime-livedata") - add(IMPLEMENTATION, "androidx.activity:activity-compose:1.7.2") + add(IMPLEMENTATION, "androidx.activity:activity-compose:1.8.1") // COMPOSE ACCOMPANIST add(IMPLEMENTATION, "com.google.accompanist:accompanist-drawablepainter:${Versions.COMPOSE_ACCOMPANIST}") diff --git a/countrypicker/src/main/java/com/hbb20/countrypicker/compose/CountryPickerComposable.kt b/countrypicker/src/main/java/com/hbb20/countrypicker/compose/CountryPickerComposable.kt index 04edaa3..a3eadb3 100644 --- a/countrypicker/src/main/java/com/hbb20/countrypicker/compose/CountryPickerComposable.kt +++ b/countrypicker/src/main/java/com/hbb20/countrypicker/compose/CountryPickerComposable.kt @@ -16,7 +16,9 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Card @@ -25,9 +27,9 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextButton +import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.runtime.Composable @@ -38,19 +40,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.emoji.text.EmojiCompat +import com.hbb20.countrypicker.R import com.hbb20.countrypicker.compose.CountryFlag.EmojiFlag import com.hbb20.countrypicker.compose.CountryFlag.ImageFlag import com.hbb20.countrypicker.datagenerator.CPDataStoreGenerator @@ -131,7 +135,7 @@ fun CountryPicker( val (showPickerDialog, setShowPickerDialog) = remember { mutableStateOf(false) } val launchPickerDialog = remember { { setShowPickerDialog(true) } } val country = rememberCountry(alpha2Code, cpDataStore) - val countryFlag = rememberFlag(country, flagProvider) + val countryFlag = rememberCountryFlag(country, flagProvider) selectedCountryLayout( country, countryFlag, @@ -189,23 +193,7 @@ private fun DefaultSelectedCountryLayout( ) { val textStyle = MaterialTheme.typography.body1 if (countryFlag != null) { - when (countryFlag) { - is EmojiFlag -> { - Text( - text = countryFlag.emoji.toString(), - style = MaterialTheme.typography.body1, - color = LocalContentColor.current - ) - } - - is ImageFlag -> { - Image( - painter = painterResource(id = countryFlag.flagImageRes), - contentDescription = null, - modifier = Modifier.height(textStyle.lineHeight.toDp()) - ) - } - } + CountryFlagLayout(countryFlag, emojiFlagTextStyle = textStyle) Spacer(modifier = Modifier.width(8.dp)) } Text(text = textToShow, style = textStyle, color = LocalContentColor.current) @@ -218,9 +206,39 @@ private fun DefaultSelectedCountryLayout( } @Composable -private fun rememberFlag( +fun CountryFlagLayout( + countryFlag: CountryFlag?, + modifier: Modifier = Modifier, + emojiFlagTextStyle: TextStyle = MaterialTheme.typography.body1, + imageFlagHeight: Dp = emojiFlagTextStyle.fontSize.toDp(), +) { + when (countryFlag) { + is EmojiFlag -> { + Text( + text = countryFlag.emoji.toString(), + style = emojiFlagTextStyle, + color = LocalContentColor.current, + modifier = modifier + ) + } + + is ImageFlag -> { + Image( + painter = painterResource(id = countryFlag.flagImageRes), + contentDescription = null, + modifier = modifier.height(imageFlagHeight) + ) + } + + null -> { /* do nothing */ + } + } +} + +@Composable +fun rememberCountryFlag( country: CPCountry?, - flagProvider: CPFlagProvider? + flagProvider: CPFlagProvider? = DefaultEmojiFlagProvider() ) = remember(country, flagProvider) { if (country != null && flagProvider != null) { when (flagProvider) { @@ -267,7 +285,14 @@ fun CountryPickerDialog( fillHeight: Boolean = showFilter, allowClearSelection: Boolean = true, queryFilter: (country: CPCountry, filterQuery: String) -> Boolean = { country, filterQuery -> - defaultFilter(country, filterQuery) + defaultCountrySearchFilter(country, filterQuery) + }, + countryRowLayout: @Composable (( + country: CPCountry, + flagProvider: CPFlagProvider?, + onClicked: (CPCountry) -> Unit, + ) -> Unit) = { country, flagProvider, onClicked -> + CountryListItemRowLayout(country, flagProvider, onClicked) }, onDismissRequest: () -> Unit, onCountrySelected: (CPCountry?) -> Unit, @@ -285,7 +310,8 @@ fun CountryPickerDialog( fillHeight = fillHeight, flagProvider = flagProvider, allowClearSelection = allowClearSelection, - queryFilter = queryFilter + queryFilter = queryFilter, + countryRowLayout = countryRowLayout, ) } } @@ -300,8 +326,9 @@ private fun DefaultCountryPickerDialogContent( showFilter: Boolean, fillHeight: Boolean, allowClearSelection: Boolean, + modifier: Modifier = Modifier, queryFilter: (country: CPCountry, filterQuery: String) -> Boolean = { country, filterQuery -> - defaultFilter(country, filterQuery) + defaultCountrySearchFilter(country, filterQuery) }, searchFieldLayout: @Composable (( searchQuery: String, @@ -310,17 +337,25 @@ private fun DefaultCountryPickerDialogContent( ) -> Unit) = { searchQuery, setSearchQuery, cpDataStore -> DefaultSearchField(searchQuery, setSearchQuery, cpDataStore) }, + countryRowLayout: @Composable (( + country: CPCountry, + flagProvider: CPFlagProvider?, + onClicked: (CPCountry) -> Unit, + ) -> Unit) = { country, flagProvider, onClicked -> + CountryListItemRowLayout(country, flagProvider, onClicked) + }, onDismissRequest: () -> Unit, ) { Card( - modifier = Modifier - .fillMaxWidth() + modifier = modifier + .fillMaxWidth(0.8f) .padding(16.dp), shape = RoundedCornerShape(16.dp), ) { val countryList = remember(cpDataStore) { cpDataStore.countryList } + val scrollState = rememberLazyListState() val quickAccessCountries = remember(quickAccessCountriesCodes, countryList) { quickAccessCountriesCodes.orEmpty().mapNotNull { alpha2 -> countryList.firstOrNull { it.alpha2.equals(alpha2, ignoreCase = true) } @@ -332,20 +367,21 @@ private fun DefaultCountryPickerDialogContent( } val filteredQuickAccessCountries = remember(showFilter, quickAccessCountries, searchQuery) { if (showFilter == false) emptyList() - else quickAccessCountries.filter { queryFilter(it, searchQuery) }.distinctBy(CPCountry::alpha2) + else quickAccessCountries.filter { queryFilter(it, searchQuery) } + .distinctBy(CPCountry::alpha2) + } + LaunchedEffect(key1 = filteredList, key2 = filteredQuickAccessCountries) { + scrollState.scrollToItem(0) } Column { if (showFilter) { searchFieldLayout(searchQuery, setSearchQuery, cpDataStore) } - LazyColumn(modifier = Modifier.weight(1f, fill = fillHeight)) { + LazyColumn(modifier = Modifier.weight(1f, fill = fillHeight), state = scrollState) { if (filteredQuickAccessCountries.isNotEmpty()) { filteredQuickAccessCountries.forEach { country -> item("quickAccess-" + country.alpha2) { - CountryListItemRowLayout( - country = country, - flagProvider = flagProvider, - ) { + countryRowLayout(country, flagProvider) { onCountrySelected(it) onDismissRequest() } @@ -355,10 +391,7 @@ private fun DefaultCountryPickerDialogContent( } filteredList.forEach { country -> item(country.alpha2) { - CountryListItemRowLayout( - country = country, - flagProvider = flagProvider, - ) { + countryRowLayout(country, flagProvider) { onCountrySelected(it) onDismissRequest() } @@ -367,7 +400,10 @@ private fun DefaultCountryPickerDialogContent( } if (allowClearSelection) { TextButton( - onClick = { onCountrySelected(null) }, + onClick = { + onCountrySelected(null) + onDismissRequest() + }, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) ) { Text( @@ -390,35 +426,43 @@ private fun DefaultSearchField( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current - val searchFieldFocusRequester = remember { - FocusRequester() - } - OutlinedTextField( - value = searchQuery, - onValueChange = { setSearchQuery(it) }, - placeholder = { - Text( - text = cpDataStore.messageGroup.searchHint, - style = MaterialTheme.typography.body1, - color = MaterialTheme.colors.onSurface + + Column { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Icon( + painter = painterResource(id = R.drawable.ic_cp_search), + contentDescription = null, + modifier = Modifier.padding(16.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) ) - }, - textStyle = MaterialTheme.typography.body1, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions(onAny = { - keyboardController?.hide() - focusManager.clearFocus() - }), - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 16.dp) - .fillMaxWidth() - .focusRequester(searchFieldFocusRequester), - ) + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) { + BasicTextField( + value = searchQuery, + onValueChange = { setSearchQuery(it) }, + textStyle = MaterialTheme.typography.body1, + cursorBrush = SolidColor( + TextFieldDefaults.outlinedTextFieldColors() + .cursorColor(isError = false).value + ), + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onAny = { + keyboardController?.hide() + focusManager.clearFocus() + }), + modifier = Modifier + .fillMaxWidth() + ) - LaunchedEffect(key1 = Unit) { - searchFieldFocusRequester.requestFocus() + if (searchQuery.isEmpty()) { + Text( + text = cpDataStore.messageGroup.searchHint, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) + ) + } + } + } } } @@ -428,10 +472,11 @@ private fun CountryListItemRowLayout( flagProvider: CPFlagProvider?, onClicked: (CPCountry) -> Unit, ) { - val countryFlag = rememberFlag(country, flagProvider) + val countryFlag = rememberCountryFlag(country, flagProvider) Row( modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) .clickable { onClicked(country) } @@ -439,23 +484,7 @@ private fun CountryListItemRowLayout( verticalAlignment = Alignment.CenterVertically ) { if (countryFlag != null) { - when (countryFlag) { - is EmojiFlag -> { - Text( - text = countryFlag.emoji.toString(), - style = MaterialTheme.typography.body1, - color = LocalContentColor.current - ) - } - - is ImageFlag -> { - Image( - painter = painterResource(id = countryFlag.flagImageRes), - contentDescription = null, - modifier = Modifier.height(MaterialTheme.typography.body1.lineHeight.toDp()) - ) - } - } + CountryFlagLayout(countryFlag) Spacer(modifier = Modifier.width(16.dp)) } Column(modifier = Modifier.weight(1f)) { @@ -468,7 +497,7 @@ private fun CountryListItemRowLayout( } } -private fun defaultFilter( +fun defaultCountrySearchFilter( country: CPCountry, filterQuery: String, ) = if (filterQuery.isBlank()) true @@ -497,10 +526,10 @@ fun PreviewSomeDialogContent() { cpDataStore = rememberCPDataStore(countryListTransformer = countryMasterListTransformer), flagProvider = DefaultEmojiFlagProvider(), quickAccessCountriesCodes = null, - showFilter = false, + showFilter = true, fillHeight = false, allowClearSelection = true, - queryFilter = { cpCountry, query -> defaultFilter(cpCountry, query) }, + queryFilter = { cpCountry, query -> defaultCountrySearchFilter(cpCountry, query) }, onDismissRequest = {}, onCountrySelected = {}, ) @@ -518,6 +547,11 @@ fun rememberCountry( } } +/** + * For state management, + * if you need to hoist the selected country code outside of compose + * then use CPCountryDetector::detectCountry() + */ @Composable fun rememberAutoDetectedCountryCode( sourceOrder: List = listOf( diff --git a/countrypicker/src/main/res/drawable/ic_cp_search.xml b/countrypicker/src/main/res/drawable/ic_cp_search.xml new file mode 100644 index 0000000..d29c6ea --- /dev/null +++ b/countrypicker/src/main/res/drawable/ic_cp_search.xml @@ -0,0 +1,5 @@ + + + + +