@@ -20,9 +20,13 @@ import androidx.compose.foundation.verticalScroll
20
20
import androidx.compose.runtime.*
21
21
import androidx.compose.ui.Alignment
22
22
import androidx.compose.ui.Modifier
23
+ import androidx.compose.ui.composed
23
24
import androidx.compose.ui.focus.FocusRequester
24
25
import androidx.compose.ui.focus.focusRequester
25
26
import androidx.compose.ui.graphics.graphicsLayer
27
+ import androidx.compose.ui.layout.*
28
+ import androidx.compose.ui.unit.Constraints
29
+ import androidx.compose.ui.unit.Dp
26
30
import androidx.compose.ui.unit.dp
27
31
import com.konyaco.fluent.FluentTheme
28
32
import com.konyaco.fluent.LocalTextStyle
@@ -35,9 +39,11 @@ import com.konyaco.fluent.icons.regular.Navigation
35
39
import com.konyaco.fluent.icons.regular.Search
36
40
import kotlinx.coroutines.delay
37
41
import kotlinx.coroutines.launch
42
+ import kotlin.math.roundToInt
38
43
39
44
private val LocalExpand = compositionLocalOf { false }
40
45
private val LocalNavigationLevel = compositionLocalOf { 0 }
46
+ private val LocalSelectedItemPosition = compositionLocalOf<MutableTransitionState <Float >? > { null }
41
47
42
48
@Composable
43
49
fun SideNav (
@@ -76,9 +82,13 @@ fun SideNav(
76
82
}
77
83
}
78
84
}
85
+ val positionState = remember {
86
+ MutableTransitionState (0f )
87
+ }
79
88
CompositionLocalProvider (
80
89
LocalExpand provides expanded,
81
- LocalNavigationLevel provides 0
90
+ LocalNavigationLevel provides 0 ,
91
+ LocalSelectedItemPosition provides positionState,
82
92
) {
83
93
autoSuggestionBox?.let {
84
94
val focusRequester = remember {
@@ -160,8 +170,20 @@ fun SideNavItem(
160
170
hovered -> FluentTheme .colors.subtleFill.secondary
161
171
else -> FluentTheme .colors.subtleFill.transparent
162
172
}
163
-
164
- Column (modifier = modifier) {
173
+ var currentPosition by remember {
174
+ mutableStateOf(0f )
175
+ }
176
+ val selectedState = LocalSelectedItemPosition .current
177
+ LaunchedEffect (selected, currentPosition) {
178
+ if (selected) {
179
+ selectedState?.targetState = currentPosition
180
+ }
181
+ }
182
+ Column (
183
+ modifier = modifier.onGloballyPositioned {
184
+ currentPosition = it.positionInRoot().y
185
+ }
186
+ ) {
165
187
Box (Modifier .height(40 .dp).fillMaxWidth().padding(4 .dp, 2 .dp)) {
166
188
val navigationLevelPadding = 28 .dp * LocalNavigationLevel .current
167
189
Layer (
@@ -264,7 +286,7 @@ interface AutoSuggestionBoxScope {
264
286
265
287
internal class AutoSuggestionBoxScopeImpl (
266
288
private val focusRequest : FocusRequester
267
- ): AutoSuggestionBoxScope {
289
+ ) : AutoSuggestionBoxScope {
268
290
override fun Modifier.focusHandle () = focusRequester(focusRequest)
269
291
}
270
292
@@ -288,9 +310,108 @@ fun NavigationItemSeparator(
288
310
289
311
@Composable
290
312
private fun Indicator (modifier : Modifier , display : Boolean ) {
291
- val height by updateTransition(display).animateDp(transitionSpec = {
292
- if (targetState) tween(FluentDuration .ShortDuration , easing = FluentEasing .FastInvokeEasing )
293
- else tween(FluentDuration .QuickDuration , easing = FluentEasing .SoftDismissEasing )
294
- }, targetValueByState = { if (it) 16 .dp else 0 .dp })
295
- Box (modifier.size(3 .dp, height).background(FluentTheme .colors.fillAccent.default, CircleShape ))
313
+ val selectionState = LocalSelectedItemPosition .current
314
+ val indicatorState = remember {
315
+ MutableTransitionState (display)
316
+ }
317
+ indicatorState.targetState = display
318
+ val animationModifier = if (selectionState != null ) {
319
+ Modifier .indicatorOffsetAnimation(16 .dp, indicatorState, selectionState)
320
+ } else {
321
+ val height by updateTransition(display).animateDp(transitionSpec = {
322
+ if (targetState) tween(FluentDuration .ShortDuration , easing = FluentEasing .FastInvokeEasing )
323
+ else tween(FluentDuration .QuickDuration , easing = FluentEasing .SoftDismissEasing )
324
+ }, targetValueByState = { if (it) 16 .dp else 0 .dp })
325
+ Modifier .height(height)
326
+ }
327
+ Box (modifier.width(3 .dp).then(animationModifier).background(FluentTheme .colors.fillAccent.default, CircleShape ))
328
+ }
329
+
330
+ private fun Modifier.indicatorOffsetAnimation (
331
+ size : Dp ,
332
+ indicatorState : MutableTransitionState <Boolean >,
333
+ selectedPosition : MutableTransitionState <Float >,
334
+ isVertical : Boolean = true
335
+ ) = composed {
336
+ val fraction by updateTransition(indicatorState).animateFloat(
337
+ transitionSpec = {
338
+ tween(FluentDuration .VeryLongDuration , easing = FluentEasing .PointToPointEasing )
339
+ },
340
+ targetValueByState = { if (it) 1f else 0f }
341
+ )
342
+ // Delay set selected position
343
+ if (indicatorState.isIdle && indicatorState.targetState) {
344
+ updateTransition(selectedPosition).animateFloat(transitionSpec = {
345
+ tween(
346
+ FluentDuration .QuickDuration ,
347
+ easing = FluentEasing .FastInvokeEasing
348
+ )
349
+ }) { it }
350
+ }
351
+ layout { measurable, constraints ->
352
+ val stickSize = size.toPx()
353
+ val containerSize = if (isVertical) {
354
+ constraints.maxHeight
355
+ } else {
356
+ constraints.maxWidth
357
+ }
358
+ val goBackward = selectedPosition.currentState > selectedPosition.targetState
359
+ val contentPadding = ((containerSize - stickSize) / 2 ).coerceAtLeast(0f )
360
+ val extendSize = containerSize - contentPadding
361
+ val currentFraction = if (indicatorState.targetState) {
362
+ fraction
363
+ } else {
364
+ 1 - fraction
365
+ }
366
+ val segmentFraction = when {
367
+ currentFraction > 0.75 -> (currentFraction - 0.75f ) * 4
368
+ currentFraction > 0.5 -> (currentFraction - 0.5f ) * 4
369
+ currentFraction > 0.25 -> (currentFraction - 0.25f ) * 4
370
+ else -> currentFraction * 4
371
+ }
372
+ val currentSize = if (! indicatorState.targetState) {
373
+ when {
374
+ currentFraction <= 0.25 -> androidx.compose.ui.util.lerp(stickSize, extendSize, segmentFraction)
375
+ currentFraction <= 0.5f -> androidx.compose.ui.util.lerp(extendSize, 0f , segmentFraction)
376
+ else -> 0f
377
+ }
378
+ } else {
379
+ when {
380
+ currentFraction > 0.75f -> androidx.compose.ui.util.lerp(
381
+ extendSize,
382
+ stickSize,
383
+ segmentFraction
384
+ )
385
+ currentFraction > 0.5f -> androidx.compose.ui.util.lerp(0f , extendSize, segmentFraction)
386
+ else -> 0f
387
+ }
388
+ }
389
+ val placeable = if (isVertical) {
390
+ measurable.measure(Constraints .fixed(constraints.maxWidth, currentSize.roundToInt().coerceAtLeast(0 )))
391
+ } else {
392
+ measurable.measure(Constraints .fixed(currentSize.roundToInt().coerceAtLeast(0 ), constraints.maxHeight))
393
+ }
394
+
395
+ layout(
396
+ width = if (isVertical) placeable.width else constraints.maxWidth,
397
+ height = if (isVertical) constraints.maxHeight else placeable.height
398
+ ) {
399
+ val offset = when {
400
+ goBackward && ! indicatorState.targetState && currentFraction <= 0.25f -> extendSize - currentSize
401
+ goBackward && ! indicatorState.targetState -> 0f
402
+ ! goBackward && ! indicatorState.targetState && currentFraction <= 0.25f -> contentPadding
403
+ ! goBackward && ! indicatorState.targetState -> containerSize - currentSize
404
+ goBackward && currentFraction > 0.75f -> contentPadding
405
+ goBackward && currentFraction > 0.5f -> containerSize - currentSize
406
+ ! goBackward && currentFraction > 0.75f -> extendSize - currentSize
407
+ ! goBackward && currentFraction > 0.5f -> 0f
408
+ else -> 0f
409
+ }
410
+ if (isVertical) {
411
+ placeable.place(0 , offset.roundToInt())
412
+ } else {
413
+ placeable.place(offset.roundToInt(), 0 )
414
+ }
415
+ }
416
+ }
296
417
}
0 commit comments