Skip to content

Commit 56f10cb

Browse files
committed
feature: konfeature screen items animation
1 parent f8957ba commit 56f10cb

5 files changed

Lines changed: 99 additions & 37 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.redmadrobot.debug.plugin.konfeature.ui
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.core.tween
5+
import androidx.compose.animation.expandVertically
6+
import androidx.compose.animation.fadeIn
7+
import androidx.compose.animation.fadeOut
8+
import androidx.compose.animation.shrinkVertically
9+
import androidx.compose.foundation.lazy.LazyItemScope
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
13+
private const val ENTER_DURATION_MILLIS = 220
14+
private const val EXIT_DURATION_MILLIS = 180
15+
16+
@Composable
17+
internal fun LazyItemScope.AnimatedFilterItem(
18+
visible: Boolean,
19+
modifier: Modifier = Modifier,
20+
content: @Composable () -> Unit,
21+
) {
22+
AnimatedVisibility(
23+
visible = visible,
24+
enter = fadeIn(animationSpec = tween(durationMillis = ENTER_DURATION_MILLIS)) +
25+
expandVertically(animationSpec = tween(durationMillis = ENTER_DURATION_MILLIS)),
26+
exit = fadeOut(animationSpec = tween(durationMillis = EXIT_DURATION_MILLIS)) +
27+
shrinkVertically(animationSpec = tween(durationMillis = EXIT_DURATION_MILLIS)),
28+
modifier = modifier.animateItem(),
29+
) {
30+
content()
31+
}
32+
}

plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,31 +126,30 @@ private fun LazyListScope.konfeatureItems(
126126
onBooleanToggle: (String, Boolean) -> Unit,
127127
) {
128128
items(
129-
items = state.filteredItems,
130-
key = { item ->
131-
when (item) {
132-
is KonfeatureItem.Config -> "config_${item.name}"
133-
is KonfeatureItem.Value -> "value_${item.key}"
134-
}
135-
},
129+
items = state.items,
130+
key = { item -> item.itemKey },
136131
) { item ->
132+
val isMatchingFilter = item.itemKey in state.matchingKeys
133+
137134
when (item) {
138135
is KonfeatureItem.Config -> {
139136
val isCollapsed = !state.isSearchActive && item.name in state.collapsedConfigs
140137
val overrideCount = state.values.count { value ->
141138
value.configName == item.name && value.isDebugSource
142139
}
143-
ConfigGroupHeader(
144-
name = item.description.takeIf { it.isNotEmpty() } ?: item.name,
145-
overrideCount = overrideCount,
146-
isCollapsed = isCollapsed,
147-
onClick = { onHeaderClick(item.name) },
148-
)
140+
AnimatedFilterItem(visible = isMatchingFilter) {
141+
ConfigGroupHeader(
142+
name = item.description.takeIf { it.isNotEmpty() } ?: item.name,
143+
overrideCount = overrideCount,
144+
isCollapsed = isCollapsed,
145+
onClick = { onHeaderClick(item.name) },
146+
)
147+
}
149148
}
150149

151150
is KonfeatureItem.Value -> {
152-
val isVisible = state.isSearchActive || item.configName !in state.collapsedConfigs
153-
if (isVisible) {
151+
val isVisible = isMatchingFilter && (state.isSearchActive || item.configName !in state.collapsedConfigs)
152+
AnimatedFilterItem(visible = isVisible) {
154153
ConfigValueItem(
155154
item = item,
156155
onEditClick = onEditClick,

plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ internal class KonfeatureViewModel(
101101
.debounce(timeoutMillis = SEARCH_QUERY_DELAY_MILLIS)
102102
.onEach { query ->
103103
_state.update { state ->
104-
state.copy(filteredItems = filterItems(state.configs, state.values, query))
104+
state.copy(matchingKeys = computeMatchingKeys(state.values, query))
105105
}
106106
}
107107
.launchIn(viewModelScope)
@@ -110,10 +110,32 @@ internal class KonfeatureViewModel(
110110
private suspend fun updateItems() {
111111
val (configs, values) = withContext(Dispatchers.IO) { getItems(konfeature) }
112112
val searchQuery = _searchQueryFlow.value
113-
val filteredItems = filterItems(configs, values, searchQuery)
113+
val items = buildItems(configs, values)
114+
val matchingKeys = computeMatchingKeys(values, searchQuery)
114115

115116
_state.update { state ->
116-
state.copy(configs = configs, values = values, filteredItems = filteredItems)
117+
state.copy(
118+
configs = configs,
119+
values = values,
120+
items = items,
121+
matchingKeys = matchingKeys,
122+
)
123+
}
124+
}
125+
126+
private fun buildItems(
127+
configs: Map<String, KonfeatureItem.Config>,
128+
values: List<KonfeatureItem.Value>,
129+
): List<KonfeatureItem> {
130+
return buildList {
131+
var previousValue: KonfeatureItem.Value? = null
132+
for (value in values) {
133+
if (previousValue?.configName != value.configName) {
134+
configs[value.configName]?.let { config -> add(config) }
135+
}
136+
add(value)
137+
previousValue = value
138+
}
117139
}
118140
}
119141

@@ -181,25 +203,23 @@ internal class KonfeatureViewModel(
181203
}
182204
}
183205

184-
private suspend fun filterItems(
185-
configs: Map<String, KonfeatureItem.Config>,
206+
private suspend fun computeMatchingKeys(
186207
values: List<KonfeatureItem.Value>,
187-
query: String
188-
): List<KonfeatureItem> {
208+
query: String,
209+
): Set<String> {
189210
return withContext(Dispatchers.Default) {
190-
buildList {
191-
var previousValue: KonfeatureItem.Value? = null
192-
193-
for (value in values) {
194-
if (value.key.contains(query, ignoreCase = true)) {
195-
if (previousValue?.configName != value.configName) {
196-
configs[value.configName]?.let { config -> add(config) }
197-
}
198-
add(value)
199-
previousValue = value
200-
}
201-
}
211+
if (query.isBlank()) {
212+
values.toMatchingKeys()
213+
} else {
214+
val matchingValues = values.filter { it.key.contains(query, ignoreCase = true) }
215+
matchingValues.toMatchingKeys()
202216
}
203217
}
204218
}
219+
220+
private fun List<KonfeatureItem.Value>.toMatchingKeys(): Set<String> {
221+
return this.flatMapTo(destination = mutableSetOf()) { value ->
222+
listOf("config_${value.configName}", "value_${value.key}")
223+
}
224+
}
205225
}

plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureItem.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ package com.redmadrobot.debug.plugin.konfeature.ui.data
33
import androidx.compose.ui.graphics.Color
44

55
internal sealed interface KonfeatureItem {
6+
val itemKey: String
7+
68
data class Config(
79
val name: String,
810
val description: String,
9-
) : KonfeatureItem
11+
) : KonfeatureItem {
12+
override val itemKey: String
13+
get() = "config_$name"
14+
}
1015

1116
data class Value(
1217
val key: String,
@@ -17,6 +22,9 @@ internal sealed interface KonfeatureItem {
1722
val sourceColor: Color,
1823
val isDebugSource: Boolean
1924
) : KonfeatureItem {
25+
override val itemKey: String
26+
get() = "value_$key"
27+
2028
val editAvailable: Boolean
2129
get() = when (value) {
2230
is Boolean,
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.redmadrobot.debug.plugin.konfeature.ui.data
22

3+
private const val ITEM_VALUE_PREFIX_KEY = "value_"
4+
35
internal data class KonfeatureViewState(
46
val searchQuery: String = "",
57
val collapsedConfigs: Set<String> = emptySet(),
68
val configs: Map<String, KonfeatureItem.Config> = emptyMap(),
79
val values: List<KonfeatureItem.Value> = emptyList(),
8-
val filteredItems: List<KonfeatureItem> = emptyList(),
10+
val items: List<KonfeatureItem> = emptyList(),
11+
val matchingKeys: Set<String> = emptySet(),
912
val editDialogState: EditDialogState? = null
1013
) {
1114
val isSearchActive: Boolean
1215
get() = searchQuery.isNotBlank()
1316
val shouldShowEmptySearchItemsHint
14-
get() = isSearchActive && filteredItems.none { it is KonfeatureItem.Value }
17+
get() = isSearchActive && matchingKeys.none { it.startsWith(ITEM_VALUE_PREFIX_KEY) }
1518
}

0 commit comments

Comments
 (0)