Skip to content

Commit 8cf17c9

Browse files
authored
Merge pull request #224 from tommybuonomo/worktree-maintenance-deps-upgrade
fix: resolve RTL crashes, RTL reset bug, and fast-scroll color glitch
2 parents 18537fd + 98f84fa commit 8cf17c9

5 files changed

Lines changed: 161 additions & 116 deletions

File tree

viewpagerdotsindicator/src/main/kotlin/com/tbuonomo/viewpagerdotsindicator/BaseDotsIndicator.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,22 @@ abstract class BaseDotsIndicator @JvmOverloads constructor(
214214

215215
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
216216
super.onLayout(changed, left, top, right, bottom)
217-
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1 && layoutDirection == View.LAYOUT_DIRECTION_RTL) {
218-
layoutDirection = View.LAYOUT_DIRECTION_LTR
219-
rotation = 180f
220-
requestLayout()
217+
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
218+
when {
219+
layoutDirection == View.LAYOUT_DIRECTION_RTL -> {
220+
layoutDirection = View.LAYOUT_DIRECTION_LTR
221+
rotation = 180f
222+
requestLayout()
223+
}
224+
rotation == 180f -> {
225+
val parentIsRtl = (parent as? View)?.layoutDirection == View.LAYOUT_DIRECTION_RTL
226+
if (!parentIsRtl) {
227+
layoutDirection = View.LAYOUT_DIRECTION_INHERIT
228+
rotation = 0f
229+
requestLayout()
230+
}
231+
}
232+
}
221233
}
222234
}
223235

viewpagerdotsindicator/src/main/kotlin/com/tbuonomo/viewpagerdotsindicator/DotsIndicator.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ class DotsIndicator @JvmOverloads constructor(
163163

164164
override fun resetPosition(position: Int) {
165165
dots[position].setWidth(dotsSize.toInt())
166-
refreshDotColor(position)
166+
val elevationItem = dots[position]
167+
val background = elevationItem.background as? DotsGradientDrawable ?: return
168+
background.setColor(dotsColor)
169+
elevationItem.setBackgroundCompat(background)
170+
elevationItem.invalidate()
167171
}
168172

169173
override val pageCount: Int

viewpagerdotsindicator/src/main/kotlin/com/tbuonomo/viewpagerdotsindicator/WormDotsIndicator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ class WormDotsIndicator @JvmOverloads constructor(
189189
widthFinalPosition = dotsSize
190190
}
191191
in 0.1f..0.9f -> {
192-
xFinalPosition = x
193-
widthFinalPosition = nextX - x + dotsSize
192+
xFinalPosition = minOf(x, nextX)
193+
widthFinalPosition = kotlin.math.abs(nextX - x) + dotsSize
194194
}
195195
else -> {
196196
xFinalPosition = nextX

viewpagerdotsindicator/src/main/kotlin/com/tbuonomo/viewpagerdotsindicator/compose/type/SpringIndicatorType.kt

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import androidx.compose.ui.graphics.Color
1010
import androidx.compose.ui.layout.onGloballyPositioned
1111
import androidx.compose.ui.layout.positionInParent
1212
import androidx.compose.ui.platform.LocalDensity
13+
import androidx.compose.ui.platform.LocalLayoutDirection
1314
import androidx.compose.ui.unit.Dp
15+
import androidx.compose.ui.unit.LayoutDirection
1416
import androidx.compose.ui.unit.dp
1517
import com.tbuonomo.viewpagerdotsindicator.compose.Dot
1618
import com.tbuonomo.viewpagerdotsindicator.compose.model.DotGraphic
@@ -27,60 +29,67 @@ class SpringIndicatorType(
2729
dotSpacing: Dp,
2830
onDotClicked: ((Int) -> Unit)?,
2931
) {
32+
val parentLayoutDirection = LocalLayoutDirection.current
3033
var firstDotPositionX: Float by remember(dotCount, dotsGraphic) { mutableStateOf(-1f) }
3134
var lastDotPositionX: Float by remember(dotCount, dotsGraphic) { mutableStateOf(-1f) }
32-
Box(modifier = modifier) {
33-
LazyRow(
34-
modifier = Modifier
35-
.fillMaxWidth(), content = {
36-
items(dotCount) { dotIndex ->
37-
val dotModifier = when (dotIndex) {
38-
0 -> {
39-
Modifier.onGloballyPositioned {
40-
firstDotPositionX = it.positionInParent().x
35+
// Force LTR on the outer Box so that absoluteOffset uses physical left-to-right coordinates.
36+
// The LazyRow restores the original direction to keep item order correct in RTL.
37+
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
38+
Box(modifier = modifier) {
39+
CompositionLocalProvider(LocalLayoutDirection provides parentLayoutDirection) {
40+
LazyRow(
41+
modifier = Modifier
42+
.fillMaxWidth(), content = {
43+
items(dotCount) { dotIndex ->
44+
val dotModifier = when (dotIndex) {
45+
0 -> {
46+
Modifier.onGloballyPositioned {
47+
firstDotPositionX = it.positionInParent().x
48+
}
49+
}
50+
dotCount - 1 -> {
51+
Modifier.onGloballyPositioned {
52+
lastDotPositionX = it.positionInParent().x
53+
}
54+
}
55+
else -> Modifier
4156
}
57+
Dot(dotsGraphic, dotModifier.clickable {
58+
onDotClicked?.invoke(dotIndex)
59+
})
4260
}
43-
dotCount - 1 -> {
44-
Modifier.onGloballyPositioned {
45-
lastDotPositionX = it.positionInParent().x
46-
}
47-
}
48-
else -> Modifier
61+
}, horizontalArrangement = Arrangement.spacedBy(
62+
dotSpacing, alignment = Alignment.CenterHorizontally
63+
),
64+
contentPadding = PaddingValues(start = dotSpacing, end = dotSpacing)
65+
)
66+
}
67+
if (firstDotPositionX != -1f && lastDotPositionX != -1f) {
68+
val centeredOffset by remember {
69+
derivedStateOf {
70+
(dotsGraphic.size - selectorDotGraphic.size) / 2
4971
}
50-
Dot(dotsGraphic, dotModifier.clickable {
51-
onDotClicked?.invoke(dotIndex)
52-
})
5372
}
54-
}, horizontalArrangement = Arrangement.spacedBy(
55-
dotSpacing, alignment = Alignment.CenterHorizontally
56-
),
57-
contentPadding = PaddingValues(start = dotSpacing, end = dotSpacing)
58-
)
59-
if (firstDotPositionX != -1f && lastDotPositionX != -1f) {
60-
val centeredOffset by remember {
61-
derivedStateOf {
62-
(dotsGraphic.size - selectorDotGraphic.size) / 2
73+
val density = LocalDensity.current.density
74+
val foregroundDotPositionDp by remember(globalOffsetProvider) {
75+
derivedStateOf {
76+
computeSelectorDotPositionDp(
77+
firstDotPositionX,
78+
lastDotPositionX,
79+
dotCount,
80+
globalOffsetProvider(),
81+
density,
82+
centeredOffset
83+
)
84+
}
6385
}
64-
}
65-
val density = LocalDensity.current.density
66-
val foregroundDotPositionDp by remember(globalOffsetProvider) {
67-
derivedStateOf {
68-
computeSelectorDotPositionDp(
69-
firstDotPositionX,
70-
lastDotPositionX,
71-
dotCount,
72-
globalOffsetProvider(),
73-
density,
74-
centeredOffset
86+
Dot(
87+
selectorDotGraphic, Modifier.absoluteOffset(
88+
x = foregroundDotPositionDp,
89+
y = centeredOffset
7590
)
76-
}
77-
}
78-
Dot(
79-
selectorDotGraphic, Modifier.offset(
80-
x = foregroundDotPositionDp,
81-
y = centeredOffset
8291
)
83-
)
92+
}
8493
}
8594
}
8695
}
@@ -98,4 +107,4 @@ class SpringIndicatorType(
98107
val foregroundDotPositionDp = (foregroundDotPositionX / density).dp
99108
return foregroundDotPositionDp + centeredOffset
100109
}
101-
}
110+
}

viewpagerdotsindicator/src/main/kotlin/com/tbuonomo/viewpagerdotsindicator/compose/type/WormIndicatorType.kt

Lines changed: 83 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import androidx.compose.ui.graphics.Color
1010
import androidx.compose.ui.layout.onGloballyPositioned
1111
import androidx.compose.ui.layout.positionInParent
1212
import androidx.compose.ui.platform.LocalDensity
13+
import androidx.compose.ui.platform.LocalLayoutDirection
1314
import androidx.compose.ui.unit.Dp
15+
import androidx.compose.ui.unit.LayoutDirection
1416
import androidx.compose.ui.unit.dp
1517
import com.tbuonomo.viewpagerdotsindicator.compose.Dot
1618
import com.tbuonomo.viewpagerdotsindicator.compose.model.DotGraphic
19+
import kotlin.math.abs
1720
import kotlin.math.floor
1821

1922
class 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

Comments
 (0)