@@ -10,10 +10,13 @@ import androidx.compose.ui.graphics.Color
1010import androidx.compose.ui.layout.onGloballyPositioned
1111import androidx.compose.ui.layout.positionInParent
1212import androidx.compose.ui.platform.LocalDensity
13+ import androidx.compose.ui.platform.LocalLayoutDirection
1314import androidx.compose.ui.unit.Dp
15+ import androidx.compose.ui.unit.LayoutDirection
1416import androidx.compose.ui.unit.dp
1517import com.tbuonomo.viewpagerdotsindicator.compose.Dot
1618import com.tbuonomo.viewpagerdotsindicator.compose.model.DotGraphic
19+ import kotlin.math.abs
1720import kotlin.math.floor
1821
1922class WormIndicatorType (
@@ -28,81 +31,98 @@ class WormIndicatorType(
2831 dotSpacing : Dp ,
2932 onDotClicked : ((Int ) -> Unit )? ,
3033 ) {
34+ val parentLayoutDirection = LocalLayoutDirection .current
35+ val isRtl = parentLayoutDirection == LayoutDirection .Rtl
3136 var firstDotPositionX: Float by remember(dotCount) { mutableStateOf(- 1f ) }
3237 var lastDotPositionX: Float by remember(dotCount) { mutableStateOf(- 1f ) }
33- Box (modifier = modifier) {
34- LazyRow (
35- modifier = Modifier
36- .fillMaxWidth(), content = {
37- items(dotCount) { dotIndex ->
38- val dotModifier = when (dotIndex) {
39- 0 -> {
40- Modifier .onGloballyPositioned {
41- firstDotPositionX = it.positionInParent().x
38+ // Force LTR on the outer Box so that absoluteOffset uses physical left-to-right coordinates.
39+ // The LazyRow restores the original direction to keep item order correct in RTL.
40+ CompositionLocalProvider (LocalLayoutDirection provides LayoutDirection .Ltr ) {
41+ Box (modifier = modifier) {
42+ CompositionLocalProvider (LocalLayoutDirection provides parentLayoutDirection) {
43+ LazyRow (
44+ modifier = Modifier
45+ .fillMaxWidth(), content = {
46+ items(dotCount) { dotIndex ->
47+ val dotModifier = when (dotIndex) {
48+ 0 -> {
49+ Modifier .onGloballyPositioned {
50+ firstDotPositionX = it.positionInParent().x
51+ }
52+ }
53+ dotCount - 1 -> {
54+ Modifier .onGloballyPositioned {
55+ lastDotPositionX = it.positionInParent().x
56+ }
57+ }
58+ else -> Modifier
4259 }
60+ Dot (dotsGraphic, dotModifier.clickable {
61+ onDotClicked?.invoke(dotIndex)
62+ })
4363 }
44- dotCount - 1 -> {
45- Modifier .onGloballyPositioned {
46- lastDotPositionX = it.positionInParent().x
47- }
48- }
49- else -> Modifier
64+ }, horizontalArrangement = Arrangement .spacedBy(
65+ dotSpacing, alignment = Alignment .CenterHorizontally
66+ ),
67+ contentPadding = PaddingValues (start = dotSpacing, end = dotSpacing)
68+ )
69+ }
70+ if (firstDotPositionX != - 1f && lastDotPositionX != - 1f ) {
71+ val centeredOffset by remember {
72+ derivedStateOf {
73+ (dotsGraphic.size - wormDotGraphic.size) / 2
5074 }
51- Dot (dotsGraphic, dotModifier.clickable {
52- onDotClicked?.invoke(dotIndex)
53- })
5475 }
55- }, horizontalArrangement = Arrangement .spacedBy(
56- dotSpacing, alignment = Alignment .CenterHorizontally
57- ),
58- contentPadding = PaddingValues (start = dotSpacing, end = dotSpacing)
59- )
60- if (firstDotPositionX != - 1f && lastDotPositionX != - 1f ) {
61- val centeredOffset by remember {
62- derivedStateOf {
63- (dotsGraphic.size - wormDotGraphic.size) / 2
76+ val density = LocalDensity .current.density
77+ val distanceBetween2DotsDp by remember {
78+ derivedStateOf {
79+ ((abs(lastDotPositionX - firstDotPositionX) / (dotCount - 1 )) / density).dp
80+ }
6481 }
65- }
66- val density = LocalDensity .current.density
67- val distanceBetween2DotsDp by remember {
68- derivedStateOf {
69- (((lastDotPositionX - firstDotPositionX) / (dotCount - 1 )) / density).dp
82+ val signedDistanceBetween2DotsDp by remember {
83+ derivedStateOf {
84+ (((lastDotPositionX - firstDotPositionX) / (dotCount - 1 )) / density).dp
85+ }
7086 }
71- }
72- val selectorDotWidthDp by remember(lastDotPositionX, firstDotPositionX) {
73- derivedStateOf {
74- distanceBetween2DotsDp + wormDotGraphic.size
87+ val selectorDotWidthDp by remember(lastDotPositionX, firstDotPositionX) {
88+ derivedStateOf {
89+ distanceBetween2DotsDp + wormDotGraphic.size
90+ }
7591 }
76- }
77- val paddingStartAndEnd : Pair < Dp , Dp > by remember(globalOffsetProvider()) {
78- derivedStateOf {
79- val endPaddingOffset = 1f - ((globalOffsetProvider() % 1.0f ) * 2f ).coerceIn(0f , 1f )
80- val startPaddingOffset = ((globalOffsetProvider() % 1.0f - 0.5f ) * 2f ).coerceIn( 0f , 1f )
81- val startPadding = distanceBetween2DotsDp * startPaddingOffset
82- val endPadding = distanceBetween2DotsDp * endPaddingOffset
83- startPadding to endPadding
92+ val paddingStartAndEnd : Pair < Dp , Dp > by remember(globalOffsetProvider()) {
93+ derivedStateOf {
94+ val endPaddingOffset = 1f - ((globalOffsetProvider() % 1.0f ) * 2f ).coerceIn( 0f , 1f )
95+ val startPaddingOffset = ((globalOffsetProvider() % 1.0f - 0.5f ) * 2f ).coerceIn(0f , 1f )
96+ val startPadding = distanceBetween2DotsDp * startPaddingOffset
97+ val endPadding = distanceBetween2DotsDp * endPaddingOffset
98+ startPadding to endPadding
99+ }
84100 }
85- }
86- val foregroundDotOffsetDp by remember(globalOffsetProvider) {
87- derivedStateOf {
88- val foregroundDotPositionX =
89- firstDotPositionX + (lastDotPositionX - firstDotPositionX) / (dotCount - 1 ) * floor(
90- globalOffsetProvider().toDouble()
91- )
92- (foregroundDotPositionX / density).dp + centeredOffset
101+ // In RTL the worm expands toward the left (physical), so start/end padding roles swap.
102+ val effectivePaddingStart = if (isRtl) paddingStartAndEnd.second else paddingStartAndEnd.first
103+ val effectivePaddingEnd = if (isRtl) paddingStartAndEnd.first else paddingStartAndEnd.second
104+ val foregroundDotOffsetDp by remember(globalOffsetProvider) {
105+ derivedStateOf {
106+ val foregroundDotPositionX =
107+ firstDotPositionX + (lastDotPositionX - firstDotPositionX) / (dotCount - 1 ) * floor(
108+ globalOffsetProvider().toDouble()
109+ )
110+ // Shift anchor left by one step in RTL so the box spans from dot P+1 to dot P.
111+ (foregroundDotPositionX / density).dp + centeredOffset + minOf(0 .dp, signedDistanceBetween2DotsDp)
112+ }
93113 }
114+ Dot (
115+ wormDotGraphic,
116+ Modifier
117+ .absoluteOffset(
118+ x = foregroundDotOffsetDp,
119+ y = centeredOffset
120+ )
121+ .width(selectorDotWidthDp)
122+ .padding(start = effectivePaddingStart, end = effectivePaddingEnd)
123+ )
94124 }
95- Dot (
96- wormDotGraphic,
97- Modifier
98- .offset(
99- x = foregroundDotOffsetDp,
100- y = centeredOffset
101- )
102- .width(selectorDotWidthDp)
103- .padding(start = paddingStartAndEnd.first, end = paddingStartAndEnd.second)
104- )
105125 }
106126 }
107127 }
108- }
128+ }
0 commit comments