One style for list items

This commit is contained in:
Sad Ellie 2022-08-13 23:53:39 +03:00
parent 5e82e3f9c5
commit e0ee67ee9c
5 changed files with 151 additions and 133 deletions

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.screens.setttings.components package com.sadellie.unitto.screens.common
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@ -25,6 +25,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -67,7 +68,7 @@ import com.sadellie.unitto.R
* @param leadingItem Additional composable: buttons, switches, checkboxes or something else. * @param leadingItem Additional composable: buttons, switches, checkboxes or something else.
*/ */
@Composable @Composable
private fun BasicSettingsListItem( private fun BasicUnittoListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: String, label: String,
supportText: String? = null, supportText: String? = null,
@ -76,8 +77,7 @@ private fun BasicSettingsListItem(
) { ) {
Row( Row(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@ -88,7 +88,9 @@ private fun BasicSettingsListItem(
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
text = label text = label,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
supportText?.let { supportText?.let {
Text( Text(
@ -115,17 +117,19 @@ private fun BasicSettingsListItem(
* @param onClick Action to perform when user clicks on this component (whole component is clickable). * @param onClick Action to perform when user clicks on this component (whole component is clickable).
*/ */
@Composable @Composable
fun SettingsListItem( fun UnittoListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: String, label: String,
supportText: String? = null, supportText: String? = null,
onClick: () -> Unit, onClick: () -> Unit,
) = BasicSettingsListItem( ) = BasicUnittoListItem(
modifier = modifier.clickable( modifier = modifier
interactionSource = remember { MutableInteractionSource() }, .clickable(
indication = rememberRipple(), interactionSource = remember { MutableInteractionSource() },
onClick = onClick indication = rememberRipple(),
), onClick = onClick
)
.padding(horizontal = 16.dp, vertical = 12.dp),
label = label, label = label,
supportText = supportText supportText = supportText
) )
@ -140,17 +144,19 @@ fun SettingsListItem(
* you new value. * you new value.
*/ */
@Composable @Composable
fun SettingsListItem( fun UnittoListItem(
label: String, label: String,
supportText: String? = null, supportText: String? = null,
switchState: Boolean, switchState: Boolean,
onSwitchChange: (Boolean) -> Unit onSwitchChange: (Boolean) -> Unit
) = BasicSettingsListItem( ) = BasicUnittoListItem(
modifier = Modifier.clickable( modifier = Modifier
interactionSource = remember { MutableInteractionSource() }, .clickable(
indication = rememberRipple(), interactionSource = remember { MutableInteractionSource() },
onClick = { onSwitchChange(!switchState) } indication = rememberRipple(),
), onClick = { onSwitchChange(!switchState) }
)
.padding(horizontal = 16.dp, vertical = 12.dp),
label = label, label = label,
supportText = supportText supportText = supportText
) { ) {
@ -168,59 +174,65 @@ fun SettingsListItem(
* @param onSelectedChange Action to perform when drop-down menu item is selected. * @param onSelectedChange Action to perform when drop-down menu item is selected.
*/ */
@Composable @Composable
fun <T> SettingsListItem( fun <T> UnittoListItem(
label: String, label: String,
supportText: String? = null, supportText: String? = null,
allOptions: Map<T, String>, allOptions: Map<T, String>,
selected: T, selected: T,
onSelectedChange: (T) -> Unit onSelectedChange: (T) -> Unit
) = BasicSettingsListItem(Modifier, label, supportText) { ) {
var dropDownExpanded by rememberSaveable { mutableStateOf(false) } BasicUnittoListItem(
var currentOption by rememberSaveable { mutableStateOf(selected) } modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
val dropDownRotation: Float by animateFloatAsState( label = label,
targetValue = if (dropDownExpanded) 180f else 0f, supportText = supportText
animationSpec = tween(easing = FastOutSlowInEasing)
)
ExposedDropdownMenuBox(
modifier = Modifier,
expanded = dropDownExpanded,
onExpandedChange = { dropDownExpanded = it }
) { ) {
OutlinedTextField( var dropDownExpanded by rememberSaveable { mutableStateOf(false) }
modifier = Modifier.widthIn(1.dp), var currentOption by rememberSaveable { mutableStateOf(selected) }
value = allOptions[currentOption] ?: selected.toString(), val dropDownRotation: Float by animateFloatAsState(
onValueChange = {}, targetValue = if (dropDownExpanded) 180f else 0f,
readOnly = true, animationSpec = tween(easing = FastOutSlowInEasing)
singleLine = true,
enabled = false,
colors = TextFieldDefaults.outlinedTextFieldColors(
disabledBorderColor = MaterialTheme.colorScheme.outline,
disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
),
trailingIcon = {
Icon(
imageVector = Icons.Outlined.ArrowDropDown,
modifier = Modifier.rotate(dropDownRotation),
contentDescription = stringResource(R.string.drop_down_description)
)
}
) )
ExposedDropdownMenu(
modifier = Modifier.exposedDropdownSize(), ExposedDropdownMenuBox(
modifier = Modifier,
expanded = dropDownExpanded, expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = false } onExpandedChange = { dropDownExpanded = it }
) { ) {
allOptions.forEach { OutlinedTextField(
DropdownMenuItem( modifier = Modifier.widthIn(1.dp),
text = { Text(it.value) }, value = allOptions[currentOption] ?: selected.toString(),
onClick = { onValueChange = {},
currentOption = it.key readOnly = true,
onSelectedChange(it.key) singleLine = true,
dropDownExpanded = false enabled = false,
} colors = TextFieldDefaults.outlinedTextFieldColors(
) disabledBorderColor = MaterialTheme.colorScheme.outline,
disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
),
trailingIcon = {
Icon(
imageVector = Icons.Outlined.ArrowDropDown,
modifier = Modifier.rotate(dropDownRotation),
contentDescription = stringResource(R.string.drop_down_description)
)
}
)
ExposedDropdownMenu(
modifier = Modifier.exposedDropdownSize(),
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = false }
) {
allOptions.forEach {
DropdownMenuItem(
text = { Text(it.value) },
onClick = {
currentOption = it.key
onSelectedChange(it.key)
dropDownExpanded = false
}
)
}
} }
} }
} }
@ -235,11 +247,21 @@ fun <T> SettingsListItem(
* @param leadingItem Additional composable: buttons, switches, checkboxes or something else. * @param leadingItem Additional composable: buttons, switches, checkboxes or something else.
*/ */
@Composable @Composable
fun SettingsListItem( fun UnittoListItem(
label: String, label: String,
supportText: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
leadingItem: @Composable RowScope.() -> Unit = {}, leadingItem: @Composable RowScope.() -> Unit = {},
trailingItem: @Composable RowScope.() -> Unit = {} trailingItem: @Composable RowScope.() -> Unit = {}
) { ) {
BasicSettingsListItem(modifier = modifier, label = label, trailingItem = trailingItem, leadingItem = leadingItem) BasicUnittoListItem(
modifier = modifier.padding(paddingValues),
label = label,
supportText = supportText,
trailingItem = trailingItem,
leadingItem = leadingItem
)
} }

View File

@ -26,11 +26,8 @@ import androidx.compose.animation.with
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -38,23 +35,20 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.screens.common.UnittoListItem
/** /**
* Represents one list item. Once clicked will navigate up. * Represents one list item. Once clicked will navigate up.
@ -78,41 +72,28 @@ private fun BasicUnitListItem(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(), // You can also change the color and radius of the ripple indication = rememberRipple(),
onClick = { selectAction(unit) } onClick = { selectAction(unit) }
) )
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
) { ) {
Row( UnittoListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.background( .background(
if (isSelected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent, if (isSelected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent,
RoundedCornerShape(24.dp) RoundedCornerShape(24.dp)
), ),
horizontalArrangement = Arrangement.SpaceBetween, paddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, label = stringResource(id = unit.displayName),
) { supportText = shortNameLabel,
Column( trailingItem = {
Modifier
.padding(horizontal = 16.dp)
.weight(1f)
) {
Text(
text = stringResource(id = unit.displayName),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = shortNameLabel,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = { favoriteAction(unit); isFavorite = !isFavorite }) {
AnimatedContent( AnimatedContent(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(false),
onClick = { favoriteAction(unit); isFavorite = !isFavorite }
),
targetState = isFavorite, targetState = isFavorite,
transitionSpec = { transitionSpec = {
(scaleIn() with scaleOut()).using(SizeTransform(clip = false)) (scaleIn() with scaleOut()).using(SizeTransform(clip = false))
@ -124,7 +105,7 @@ private fun BasicUnitListItem(
) )
} }
} }
} )
} }
} }

View File

@ -36,9 +36,9 @@ import com.sadellie.unitto.data.preferences.PRECISIONS
import com.sadellie.unitto.data.preferences.SEPARATORS import com.sadellie.unitto.data.preferences.SEPARATORS
import com.sadellie.unitto.screens.common.Header import com.sadellie.unitto.screens.common.Header
import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar
import com.sadellie.unitto.screens.common.UnittoListItem
import com.sadellie.unitto.screens.openLink import com.sadellie.unitto.screens.openLink
import com.sadellie.unitto.screens.setttings.components.AlertDialogWithList import com.sadellie.unitto.screens.setttings.components.AlertDialogWithList
import com.sadellie.unitto.screens.setttings.components.SettingsListItem
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@ -62,47 +62,51 @@ fun SettingsScreen(
// THEME // THEME
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(id = R.string.unit_groups_setting), label = stringResource(id = R.string.unit_groups_setting),
onClick = { navControllerAction(UNIT_GROUPS_SCREEN) } onClick = { navControllerAction(UNIT_GROUPS_SCREEN) }
) )
} }
// PRECISION // PRECISION
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.precision_setting), label = stringResource(R.string.precision_setting),
supportText = stringResource(R.string.precision_setting_support) supportText = stringResource(R.string.precision_setting_support),
) { dialogState = DialogState.PRECISION } onClick = { dialogState = DialogState.PRECISION }
)
} }
// SEPARATOR // SEPARATOR
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.separator_setting), label = stringResource(R.string.separator_setting),
supportText = stringResource(R.string.separator_setting_support) supportText = stringResource(R.string.separator_setting_support),
) { dialogState = DialogState.SEPARATOR } onClick = { dialogState = DialogState.SEPARATOR }
)
} }
// OUTPUT FORMAT // OUTPUT FORMAT
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.output_format_setting), label = stringResource(R.string.output_format_setting),
supportText = stringResource(R.string.output_format_setting_support) supportText = stringResource(R.string.output_format_setting_support),
) { dialogState = DialogState.OUTPUT_FORMAT } onClick = { dialogState = DialogState.OUTPUT_FORMAT }
)
} }
// THEME // THEME
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.theme_setting), label = stringResource(R.string.theme_setting),
supportText = stringResource(R.string.theme_setting_support) supportText = stringResource(R.string.theme_setting_support),
) { navControllerAction(THEMES_SCREEN) } onClick = { navControllerAction(THEMES_SCREEN) }
)
} }
// CURRENCY RATE NOTE // CURRENCY RATE NOTE
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.currency_rates_note_setting), label = stringResource(R.string.currency_rates_note_setting),
onClick = { dialogState = DialogState.CURRENCY_RATE } onClick = { dialogState = DialogState.CURRENCY_RATE }
) )
@ -113,24 +117,34 @@ fun SettingsScreen(
// TERMS AND CONDITIONS // TERMS AND CONDITIONS
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.terms_and_conditions), label = stringResource(R.string.terms_and_conditions),
onClick = { openLink(mContext, "http://sadellie.github.io/unitto/terms-app.html") } onClick = {
openLink(
mContext,
"http://sadellie.github.io/unitto/terms-app.html"
)
}
) )
} }
// PRIVACY POLICY // PRIVACY POLICY
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.privacy_policy), label = stringResource(R.string.privacy_policy),
onClick = { openLink(mContext, "http://sadellie.github.io/unitto/privacy-app.html") } onClick = {
openLink(
mContext,
"http://sadellie.github.io/unitto/privacy-app.html"
)
}
) )
} }
// ANALYTICS // ANALYTICS
if (BuildConfig.ANALYTICS) { if (BuildConfig.ANALYTICS) {
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.send_usage_statistics), label = stringResource(R.string.send_usage_statistics),
supportText = stringResource(R.string.send_usage_statistics_support), supportText = stringResource(R.string.send_usage_statistics_support),
switchState = viewModel.userPrefs.enableAnalytics switchState = viewModel.userPrefs.enableAnalytics
@ -140,7 +154,7 @@ fun SettingsScreen(
// THIRD PARTY // THIRD PARTY
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.third_party_licenses), label = stringResource(R.string.third_party_licenses),
onClick = { navControllerAction(ABOUT_SCREEN) } onClick = { navControllerAction(ABOUT_SCREEN) }
) )
@ -149,19 +163,20 @@ fun SettingsScreen(
// RATE THIS APP // RATE THIS APP
if (BuildConfig.STORE_LINK.isNotEmpty()) { if (BuildConfig.STORE_LINK.isNotEmpty()) {
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.rate_this_app), label = stringResource(R.string.rate_this_app),
onClick = { openLink(mContext, BuildConfig.STORE_LINK) } onClick = { openLink(mContext, BuildConfig.STORE_LINK) }
) )
} }
} }
// APP VERSION // APP VERSION
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(id = R.string.app_version_name_setting), label = stringResource(id = R.string.app_version_name_setting),
supportText = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" supportText = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
) {} onClick = {}
)
} }
} }
} }

View File

@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar
import com.sadellie.unitto.screens.setttings.components.SettingsListItem import com.sadellie.unitto.screens.common.UnittoListItem
import io.github.sadellie.themmo.ThemingMode import io.github.sadellie.themmo.ThemingMode
import io.github.sadellie.themmo.ThemmoController import io.github.sadellie.themmo.ThemmoController
@ -44,7 +44,7 @@ fun ThemesScreen(
) { paddingValues -> ) { paddingValues ->
LazyColumn(contentPadding = paddingValues) { LazyColumn(contentPadding = paddingValues) {
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.color_theme), label = stringResource(R.string.color_theme),
allOptions = mapOf( allOptions = mapOf(
ThemingMode.AUTO to stringResource(R.string.force_auto_mode), ThemingMode.AUTO to stringResource(R.string.force_auto_mode),
@ -60,7 +60,7 @@ fun ThemesScreen(
} }
item { item {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.enable_dynamic_colors), label = stringResource(R.string.enable_dynamic_colors),
supportText = stringResource(R.string.enable_dynamic_colors_support), supportText = stringResource(R.string.enable_dynamic_colors_support),
switchState = themmoController.isDynamicThemeEnabled, switchState = themmoController.isDynamicThemeEnabled,
@ -77,7 +77,7 @@ fun ThemesScreen(
enter = expandVertically() + fadeIn(), enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(), exit = shrinkVertically() + fadeOut(),
) { ) {
SettingsListItem( UnittoListItem(
label = stringResource(R.string.force_amoled_mode), label = stringResource(R.string.force_amoled_mode),
supportText = stringResource(R.string.force_amoled_mode_support), supportText = stringResource(R.string.force_amoled_mode_support),
switchState = themmoController.isAmoledThemeEnabled, switchState = themmoController.isAmoledThemeEnabled,

View File

@ -44,7 +44,7 @@ import androidx.compose.ui.unit.dp
import com.sadellie.unitto.R import com.sadellie.unitto.R
import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar
import com.sadellie.unitto.screens.common.Header import com.sadellie.unitto.screens.common.Header
import com.sadellie.unitto.screens.setttings.components.SettingsListItem import com.sadellie.unitto.screens.common.UnittoListItem
import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.detectReorder import org.burnoutcrew.reorderable.detectReorder
import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.detectReorderAfterLongPress
@ -86,7 +86,7 @@ fun UnitGroupsScreen(
) )
val cornerRadius = animateDpAsState(if (isDragging) 16.dp else 0.dp) val cornerRadius = animateDpAsState(if (isDragging) 16.dp else 0.dp)
SettingsListItem( UnittoListItem(
modifier = Modifier modifier = Modifier
.padding(horizontal = cornerRadius.value) .padding(horizontal = cornerRadius.value)
.clip(RoundedCornerShape(cornerRadius.value)) .clip(RoundedCornerShape(cornerRadius.value))
@ -132,7 +132,7 @@ fun UnitGroupsScreen(
} }
items(hiddenUnits.value, { it }) { items(hiddenUnits.value, { it }) {
SettingsListItem( UnittoListItem(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.clickable { viewModel.returnUnitGroup(it) } .clickable { viewModel.returnUnitGroup(it) }