Skip to content

Commit f59c958

Browse files
authored
Fix/page carousel RTL android (#1612)
* Centralize the set of current index * Fix redundant scrolls when index changes * Remove carouselOffset from context and other irrelevant parts * Fix offset issue on Android RTL * Fix initial page on TabController PageCarousel
1 parent 897689a commit f59c958

File tree

8 files changed

+70
-31
lines changed

8 files changed

+70
-31
lines changed

demo/src/screens/componentScreens/TabControllerScreen/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface State {
1414
asCarousel: boolean;
1515
centerSelected: boolean;
1616
fewItems: boolean;
17+
initialIndex: number;
1718
selectedIndex: number;
1819
key: string | number;
1920
items: TabControllerItemProps[];
@@ -26,6 +27,7 @@ class TabControllerScreen extends Component<{}, State> {
2627
asCarousel: true,
2728
centerSelected: false,
2829
fewItems: false,
30+
initialIndex: 0,
2931
selectedIndex: 0,
3032
key: Date.now(),
3133
items: []
@@ -146,14 +148,14 @@ class TabControllerScreen extends Component<{}, State> {
146148
}
147149

148150
render() {
149-
const {key, /* selectedIndex, */ asCarousel, centerSelected, fewItems, items} = this.state;
151+
const {key, initialIndex, /* selectedIndex, */ asCarousel, centerSelected, fewItems, items} = this.state;
150152
return (
151153
<View flex bg-grey70>
152154
<TabController
153155
key={key}
154156
asCarousel={asCarousel}
155157
// selectedIndex={selectedIndex}
156-
initialIndex={0}
158+
initialIndex={initialIndex}
157159
onChangeIndex={this.onChangeIndex}
158160
items={items}
159161
>

generatedTypes/src/components/tabController/TabBarContext.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ interface TabControllerContext {
44
initialIndex?: number;
55
selectedIndex?: number;
66
items?: any[];
7+
itemsCount: number;
78
asCarousel?: boolean;
89
containerWidth: number;
910
pageWidth: number;
1011
/** static page index */
1112
currentPage: Reanimated.SharedValue<number>;
1213
/** transition page index (can be a fraction when transitioning between pages) */
1314
targetPage: Reanimated.SharedValue<number>;
14-
carouselOffset: Reanimated.SharedValue<number>;
15+
setCurrentIndex: (index: number) => void;
1516
}
1617
declare const TabBarContext: React.Context<TabControllerContext>;
1718
export default TabBarContext;

src/components/tabController/PageCarousel.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import Reanimated, {
55
useAnimatedReaction,
66
useAnimatedRef,
77
useAnimatedScrollHandler,
8-
useSharedValue,
9-
withTiming
8+
useSharedValue
109
} from 'react-native-reanimated';
10+
import {Constants} from 'helpers';
11+
12+
const FIX_RTL = Constants.isRTL && Constants.isAndroid;
1113

1214
/**
1315
* @description: TabController's Page Carousel
@@ -17,46 +19,70 @@ import Reanimated, {
1719
function PageCarousel({...props}) {
1820
const carousel = useAnimatedRef<Reanimated.ScrollView>();
1921
const {
22+
itemsCount,
2023
currentPage,
2124
targetPage,
22-
selectedIndex = 0,
2325
pageWidth,
24-
carouselOffset
26+
// carouselOffset,
27+
setCurrentIndex
2528
} = useContext(TabBarContext);
26-
const contentOffset = useMemo(() => ({x: selectedIndex * pageWidth, y: 0}), [selectedIndex, pageWidth]);
27-
const wasScrolledByPress = useSharedValue(false);
29+
const initialOffset = useMemo(() => ({x: currentPage.value * pageWidth, y: 0}), []);
30+
const indexChangeReason = useSharedValue<'byScroll' | 'byPress' | undefined>(undefined);
31+
32+
const scrollToInitial = useCallback(() => {
33+
if (Constants.isAndroid && currentPage.value) {
34+
scrollToItem(currentPage.value);
35+
}
36+
}, []);
37+
38+
const calcOffset = useCallback(offset => {
39+
'worklet';
40+
return FIX_RTL ? pageWidth * (itemsCount - 1) - offset : offset;
41+
},
42+
[pageWidth, itemsCount]);
2843

2944
const scrollHandler = useAnimatedScrollHandler({
3045
onScroll: e => {
31-
carouselOffset.value = e.contentOffset.x;
32-
const newIndex = e.contentOffset.x / pageWidth;
46+
// carouselOffset.value = e.contentOffset.x;
47+
const xOffset = calcOffset(e.contentOffset.x);
48+
const newIndex = xOffset / pageWidth;
3349

34-
if (wasScrolledByPress.value) {
50+
if (indexChangeReason.value === 'byPress') {
51+
// Scroll was immediate and not by gesture
3552
/* Round is for android when offset value has fraction */
36-
targetPage.value = withTiming(Math.round(newIndex));
37-
wasScrolledByPress.value = false;
53+
// targetPage.value = withTiming(Math.round(newIndex));
54+
55+
indexChangeReason.value = undefined;
3856
} else {
3957
targetPage.value = newIndex;
4058
}
4159
},
4260
onMomentumEnd: e => {
43-
const newPage = Math.round(e.contentOffset.x / pageWidth);
44-
currentPage.value = newPage;
61+
const xOffset = calcOffset(e.contentOffset.x);
62+
const newPage = Math.round(xOffset / pageWidth);
63+
indexChangeReason.value = 'byScroll';
64+
setCurrentIndex(newPage);
4565
}
4666
});
4767

4868
const scrollToItem = useCallback(index => {
49-
wasScrolledByPress.value = true;
69+
if (indexChangeReason.value === 'byScroll') {
70+
indexChangeReason.value = undefined;
71+
} else {
72+
indexChangeReason.value = 'byPress';
73+
}
74+
75+
const actualIndex = FIX_RTL ? itemsCount - index - 1 : index;
5076
// @ts-expect-error
51-
carousel.current?.scrollTo({x: index * pageWidth, animated: false});
77+
carousel.current?.scrollTo({x: actualIndex * pageWidth, animated: false});
5278
},
53-
[pageWidth]);
79+
[pageWidth, itemsCount]);
5480

5581
useAnimatedReaction(() => {
5682
return currentPage.value;
5783
},
5884
(currIndex, prevIndex) => {
59-
if (currIndex !== prevIndex) {
85+
if (prevIndex !== null && currIndex !== prevIndex) {
6086
runOnJS(scrollToItem)(currIndex);
6187
}
6288
});
@@ -75,7 +101,8 @@ function PageCarousel({...props}) {
75101
showsHorizontalScrollIndicator={false}
76102
onScroll={scrollHandler}
77103
scrollEventThrottle={16}
78-
contentOffset={contentOffset} // iOS only
104+
contentOffset={initialOffset} // iOS only
105+
onLayout={scrollToInitial} // Android only
79106
/>
80107
);
81108
}

src/components/tabController/TabBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ const TabBar = (props: Props) => {
188188
return Math.round(currentPage.value);
189189
},
190190
(currIndex, prevIndex) => {
191-
if (currIndex !== prevIndex) {
191+
if (prevIndex !== null && currIndex !== prevIndex) {
192192
runOnJS(focusIndex)(currIndex);
193193
}
194194
});

src/components/tabController/TabBarContext.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ interface TabControllerContext {
66
// DEPRECATED: use initialIndex instead
77
selectedIndex?: number;
88
items?: any[];
9+
itemsCount: number;
910
asCarousel?: boolean;
1011
containerWidth: number;
1112
pageWidth: number;
1213
/** static page index */
1314
currentPage: Reanimated.SharedValue<number>;
1415
/** transition page index (can be a fraction when transitioning between pages) */
1516
targetPage: Reanimated.SharedValue<number>;
16-
carouselOffset: Reanimated.SharedValue<number>;
17+
/* carouselOffset: Reanimated.SharedValue<number>; */
18+
setCurrentIndex: (index: number) => void;
1719
}
1820

1921
// @ts-expect-error

src/components/tabController/TabBarItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export default function TabBarItem({
124124
style,
125125
...props
126126
}: Props) {
127-
const {currentPage} = useContext(TabBarContext);
127+
const {currentPage, setCurrentIndex} = useContext(TabBarContext);
128128
const itemRef = useRef();
129129
const itemWidth = useRef(props.width);
130130
// JSON.parse(JSON.stringify is due to an issue with reanimated
@@ -140,7 +140,7 @@ export default function TabBarItem({
140140

141141
const onPress = useCallback(() => {
142142
if (!ignore) {
143-
currentPage.value = index;
143+
setCurrentIndex(index);
144144
}
145145

146146
props.onPress?.(index);

src/components/tabController/index.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// TODO: support commented props
2-
import React, {PropsWithChildren, useMemo, useEffect, useRef, useState} from 'react';
2+
import React, {PropsWithChildren, useMemo, useEffect, useRef, useState, useCallback} from 'react';
33
import _ from 'lodash';
44
import {useAnimatedReaction, useSharedValue, withTiming, runOnJS} from 'react-native-reanimated';
55
import {Constants} from '../../helpers';
@@ -93,7 +93,12 @@ function TabController({
9393
const currentPage = useSharedValue(initialIndex);
9494
/* targetPage - transitioned page index (can be a fraction when transitioning between pages) */
9595
const targetPage = useSharedValue(initialIndex);
96-
const carouselOffset = useSharedValue(initialIndex * Math.round(pageWidth));
96+
// const carouselOffset = useSharedValue(initialIndex * Math.round(pageWidth));
97+
98+
const setCurrentIndex = useCallback(index => {
99+
'worklet';
100+
currentPage.value = index;
101+
}, []);
97102

98103
useEffect(() => {
99104
if (!_.isUndefined(selectedIndex)) {
@@ -102,7 +107,7 @@ function TabController({
102107
}, [selectedIndex]);
103108

104109
useEffect(() => {
105-
currentPage.value = initialIndex;
110+
setCurrentIndex(initialIndex);
106111
}, [initialIndex]);
107112

108113
useAnimatedReaction(() => {
@@ -124,13 +129,15 @@ function TabController({
124129
/* Items */
125130
items,
126131
ignoredItems,
132+
itemsCount: items.length - ignoredItems.length,
127133
/* Animated Values */
128134
targetPage,
129135
currentPage,
130-
carouselOffset,
136+
// carouselOffset,
131137
containerWidth: screenWidth,
132138
/* Callbacks */
133-
onChangeIndex
139+
onChangeIndex,
140+
setCurrentIndex
134141
};
135142
}, [initialIndex, asCarousel, items, onChangeIndex, screenWidth]);
136143

src/components/tabController/useScrollToItem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ const useScrollToItem = <T extends ScrollToSupportedViews>(props: ScrollToItemPr
184184

185185
useEffect(() => {
186186
if (!_.isUndefined(selectedIndex)) {
187-
focusIndex(selectedIndex);
187+
focusIndex(selectedIndex, false);
188188
}
189189
}, [selectedIndex, focusIndex]);
190190

0 commit comments

Comments
 (0)