Open simonronec opened 3 years ago
Couldn't find version numbers for the following packages in the issue:
react-native
Can you update the issue to include version numbers for those packages? The version numbers must match the format 1.2.3.
The versions mentioned in the issue for the following packages differ from the latest versions on npm:
react-native-pager-view
(found: 5.0.12
, latest: 5.4.1
)expo
(found: 42.0.0
, latest: 42.0.3
)Can you verify that the issue still exists after upgrading to the latest versions of these packages?
Anyone have any ideas for fixing this, was working fin in v2 of this lib. The problem in the current lib is that there is a scale animation now for the auto width indicator.
My best guess is that you would need to break apart the Indicator into the following:
<Container>
<Left />
<Center />
<Right />
</Conteiner>
Center gets the scale animation. Container gets the left animation, and then you have to style Left and Right with your border radius.
Ended up being a lot simpler that expected.
Basically just had to change the scaleX to width. I know that scaleX is better in every way except for the border-radius. @satya164 if you want me to open a PR with this change, I am more than happy to. I think it's also acceptable to just point people towards having their own indicator using the bellow rather than having this change in the lib.
import * as React from 'react'
import {
Animated,
Easing,
StyleSheet,
I18nManager,
StyleProp,
ViewStyle,
Platform,
} from 'react-native'
import type {
Route,
SceneRendererProps,
NavigationState,
} from 'react-native-tab-view'
export type GetTabWidth = (index: number) => number
export type Props<T extends Route> = SceneRendererProps & {
state: NavigationState<T>
width: string | number
style?: StyleProp<ViewStyle>
getTabWidth: GetTabWidth
}
export class TabBarIndicator<T extends Route> extends React.Component<
Props<T>
> {
componentDidMount() {
this.fadeInIndicator()
}
componentDidUpdate() {
this.fadeInIndicator()
}
private fadeInIndicator = () => {
const { state: navigationState, layout, width, getTabWidth } = this.props
if (
!this.isIndicatorShown &&
width === 'auto' &&
layout.width &&
// We should fade-in the indicator when we have widths for all the tab items
navigationState.routes.every((_, i) => getTabWidth(i))
) {
this.isIndicatorShown = true
Animated.timing(this.opacity, {
toValue: 1,
duration: 150,
easing: Easing.in(Easing.linear),
useNativeDriver: true,
}).start()
}
}
private isIndicatorShown = false
private opacity = new Animated.Value(this.props.width === 'auto' ? 0 : 1)
private getTranslateX = (
position: Animated.AnimatedInterpolation,
routes: Route[],
getTabWidth: GetTabWidth,
) => {
const inputRange = routes.map((_, i) => i)
// every index contains widths at all previous indices
const outputRange = routes.reduce<number[]>((acc, _, i) => {
if (i === 0) return [0]
return [...acc, acc[i - 1] + getTabWidth(i - 1)]
}, [])
const translateX = position.interpolate({
inputRange,
outputRange,
extrapolate: 'clamp',
})
return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1)
}
render() {
const {
position,
state: navigationState,
getTabWidth,
width,
style,
layout,
} = this.props
const { routes } = navigationState
const transform = []
if (layout.width) {
const translateX =
routes.length > 1
? this.getTranslateX(position, routes, getTabWidth)
: 0
transform.push({ translateX })
}
if (width === 'auto') {
transform.push({ translateX: 0.5 })
}
const getWidth = () => {
if (width === 'auto') {
const inputRange = routes.map((_, i) => i)
const outputRange = inputRange.map(getTabWidth)
return {
width:
routes.length > 1
? position.interpolate({
inputRange,
outputRange,
extrapolate: 'clamp',
})
: outputRange[0],
}
}
return {}
}
return (
<Animated.View
style={[
styles.indicator,
{ width: width === 'auto' ? 1 : width },
// If layout is not available, use `left` property for positioning the indicator
// This avoids rendering delay until we are able to calculate translateX
// If platform is macos use `left` property as `transform` is broken at the moment.
// See: https://github.com/microsoft/react-native-macos/issues/280
layout.width && Platform.OS !== 'macos'
? { left: 0 }
: { left: `${(100 / routes.length) * navigationState.index}%` },
{ transform },
width === 'auto' ? { opacity: this.opacity } : null,
getWidth(),
style,
]}
/>
)
}
}
const styles = StyleSheet.create({
indicator: {
backgroundColor: '#ffeb3b',
position: 'absolute',
left: 0,
bottom: 0,
right: 0,
height: 2,
},
})
The above only works on web which is where I do most of my development because width transforms don't work with nativeDriver.
Here is a solution that works for nativeDriver.
It's still a bit dirty, but I tried to comment all the math that is happening. I rewrote the component to be a function component. I am using fp-ts to do all the array manipulation. You can sub out for native methods easily.
The basic principle is the same as my first comment where you do the scaleX on Center
. Container
moves to the right location in the tabBar
, and Right
moves within Container
to compensate for the new size of Center
.
@satya164 I did notice a bug in your code as well, when you do return [...acc, acc[i - 1] + getTabWidth(i - 1)];
you actually make the indicator 1px wider than the the tab because you are scaling up from 1px width. I resolved this in my component by just subtracting the extra px when I need the scaleX amount which is different that the tabWidth.
I found another optimization when the opacity is toggled. We don't need to see if all of the tabs have widths, just the one that is active.
@roneconesimplicity let me know if this works for you.
import styled from '@emotion/native'
import React from 'react'
import {
Animated,
StyleProp,
ViewStyle,
StyleSheet,
Easing,
Platform,
I18nManager,
} from 'react-native'
import type {
NavigationState,
Route,
SceneRendererProps,
} from 'react-native-tab-view'
import { useEffect, useRef, useState } from 'react'
import { A, O, pipe } from 'Helpers/fp-ts-imports'
import { GetTabWidth } from 'react-native-tab-view/lib/typescript/TabBarIndicator'
const BORDER_RADIUS = 8
// Use a large OVERLAP value so that the border draws correctly on iOS.
const OVERLAP = 8
type TabBarIndicatorProps<T extends Route> = SceneRendererProps & {
state: NavigationState<T>
width: string | number
style?: StyleProp<ViewStyle>
getTabWidth: GetTabWidth
}
export const TabBarIndicator = <T extends Route>(
props: TabBarIndicatorProps<T>,
) => {
const {
position,
state: { routes, index },
getTabWidth,
width,
layout,
style,
} = props
const isAuto = width === 'auto'
const opacity = useRef(new Animated.Value(isAuto ? 0 : 1)).current
const [isIndicatorShown, setIsIndicatorShown] = useState(false)
const tabIndices = pipe(
routes,
A.mapWithIndex((i) => i),
)
const tabWidths = pipe(
tabIndices,
A.map((i) => getTabWidth(i) - 1),
)
const tabCenterWidths = pipe(
tabWidths,
// The tab center should be as wide as the tab minus Left and Right, or
// 2 * BORDER_RADIUS
A.map((i) => i - BORDER_RADIUS * 2),
)
const tabLocations = pipe(
tabIndices,
A.reduce<number, Array<number>>([], (b, a) => {
// The first item is zero away from the left side.
if (a === 0) {
return [0]
}
return [
...b,
...pipe(
b,
// Get the previous saved left value.
A.lookup(a - 1),
// Add this left value to the previous tab width.
O.map((x) => [x + getTabWidth(a - 1)]),
O.getOrElse<Array<number>>(() => []),
),
]
}),
)
const rightLocations = pipe(
tabIndices,
A.mapWithIndex(
// We want to place Right to the right of Center, but with the OVERLAP
// underneath Center. Take tab width and remove BORDER_RADIUS for Left
// and Right. We then have to remove the OVERLAP for both ends.
(i, a) => getTabWidth(i) - 1 - BORDER_RADIUS * 2 - OVERLAP * 2,
),
)
useEffect(() => {
if (
!isIndicatorShown &&
isAuto &&
layout.width !== 0 &&
// We should fade-in the indicator when we have widths for all the tab items
tabWidths[index] !== 0
) {
setIsIndicatorShown(true)
Animated.timing(opacity, {
toValue: 1,
duration: 150,
easing: Easing.in(Easing.linear),
useNativeDriver: true,
}).start()
}
}, [isIndicatorShown, isAuto, layout.width, tabWidths])
const getTranslateX = ({ outputRange }: { outputRange: Array<number> }) => {
// every index contains widths at all previous indices
const translateX = position.interpolate({
inputRange: tabIndices,
outputRange,
extrapolate: 'clamp',
})
return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1)
}
const tabPositionTransform = layout.width
? [
{
translateX:
routes.length > 1
? getTranslateX({ outputRange: tabLocations })
: 0,
},
]
: []
const rightPositionTransform = layout.width
? [
{
translateX:
routes.length > 1
? getTranslateX({ outputRange: rightLocations })
: 0,
},
]
: []
const centerScaleTransform = isAuto
? [
{
scaleX:
routes.length > 1
? position.interpolate({
inputRange: tabIndices,
outputRange: tabCenterWidths,
extrapolate: 'clamp',
})
: pipe(
tabWidths,
A.lookup(0),
O.getOrElse(() => 0),
),
},
{ translateX: 0.5 },
]
: []
return (
<TabIndicatorContainer
style={[
styles.indicator,
isAuto ? { width: 1, opacity } : { width },
// If layout is not available, use `left` property for positioning the indicator
// This avoids rendering delay until we are able to calculate translateX
// If platform is macos use `left` property as `transform` is broken at the moment.
// See: https://github.com/microsoft/react-native-macos/issues/280
layout.width && Platform.OS !== 'macos'
? { left: 0 }
: { left: `${(100 / routes.length) * index}%` },
{ transform: tabPositionTransform },
style,
]}
>
<Left />
<Center style={{ transform: centerScaleTransform }} />
<Right style={{ transform: rightPositionTransform }} />
</TabIndicatorContainer>
)
}
const TabIndicatorContainer = styled(Animated.View)``
const Center = styled(Animated.View)`
width: 1px;
height: 40px;
background: black;
left: -${OVERLAP.toString()}px;
`
const Left = styled.View`
width: ${(BORDER_RADIUS + OVERLAP).toString()}px;
height: 40px;
background: black;
border-top-left-radius: ${BORDER_RADIUS.toString()}px;
border-bottom-left-radius: ${BORDER_RADIUS.toString()}px;
`
const Right = styled(Animated.View)`
width: ${(BORDER_RADIUS + OVERLAP).toString()}px;
height: 40px;
background: black;
border-top-right-radius: ${BORDER_RADIUS.toString()}px;
border-bottom-right-radius: ${BORDER_RADIUS.toString()}px;
`
const styles = StyleSheet.create({
indicator: {
backgroundColor: 'transparent',
position: 'absolute',
left: 0,
bottom: 0,
right: 0,
height: 2,
flexDirection: 'row',
},
})
Thanks @izakfilmalter, there is a couple of mistakes:
const centerScaleTransform = isAuto
? [
{
scaleX:
routes.length > 1
? position.interpolate({
inputRange: tabIndices,
outputRange: tabCenterWidths,
extrapolate: 'clamp',
})
: pipe(
tabWidths, // <-- here should be tabCenterWidths
A.lookup(0),
O.getOrElse(() => 0),
),
},
{ translateX: 0.5 },
]
: []
Also mistake in rightPositionTransform
const rightPositionTransform = layout.width
? [
{
translateX:
routes.length > 1
? getTranslateX({ outputRange: rightLocations })
: 0, // <-- here should be rightLocations[0]
},
]
: []
Without these corrections the center and the right portion have wrong positioning if there is just one tab in the tab bar.
I am facing the same issue. Any progress?
Found a related issue (closed): https://github.com/satya164/react-native-tab-view/issues/1323
+1
Current behavior
Border radius of indicator is not respected when tabStyle width is set to auto. Works okay when not using tab width auto.
Expected behavior
Should be working
Reproduction
https://snack.expo.dev/@ronec_onesimplicity/border-radius-not-woking
Platform
Environment