Skip to content
Merged
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Fling from './release_tests/fling';
import NestedTouchables from './release_tests/nestedTouchables';
import NestedButtons from './release_tests/nestedButtons';
import NestedGestureHandlerRootViewWithModal from './release_tests/nestedGHRootViewWithModal';
import RoundedButtons from './release_tests/roundedButtons';
import { PinchableBox } from './recipes/scaleAndRotate';
import PanAndScroll from './recipes/panAndScroll';
import { BottomSheet } from './showcase/bottomSheet';
Expand Down Expand Up @@ -115,6 +116,7 @@ const EXAMPLES: ExamplesSection[] = [
{ name: 'Fling', component: Fling },
{ name: 'Combo', component: ComboWithGHScroll },
{ name: 'Touchables', component: TouchablesIndex as React.ComponentType },
{ name: 'Rounded buttons', component: RoundedButtons },
],
},
{
Expand Down
138 changes: 138 additions & 0 deletions example/src/release_tests/roundedButtons/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { View, StyleSheet, Text, SafeAreaView } from 'react-native';
import {
GestureHandlerRootView,
ScrollView,
RectButton,
} from 'react-native-gesture-handler';

const MyButton = RectButton;

export default function ComplexUI() {
return (
<GestureHandlerRootView style={styles.container}>
<SafeAreaView style={styles.container}>
<ScrollView>
<Avatars />
<View style={styles.paddedContainer}>
<Gallery />
<Gallery />
<Gallery />
<Gallery />
<Gallery />
</View>
</ScrollView>
</SafeAreaView>
</GestureHandlerRootView>
);
}
const colors = ['#782AEB', '#38ACDD', '#57B495', '#FF6259', '#FFD61E'];

function Avatars() {
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{colors.map((color) => (
<MyButton
key={color}
style={[styles.avatars, { backgroundColor: color }]}>
<Text style={styles.avatarLabel}>{color.slice(1, 3)}</Text>
</MyButton>
))}
</ScrollView>
);
}

function Gallery() {
return (
<View style={[styles.container, styles.gap, styles.marginBottom]}>
<MyButton style={styles.fullWidthButton} />
<View style={[styles.row, styles.gap]}>
<MyButton style={styles.leftButton} />
<MyButton style={styles.rightButton} />
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
marginBottom: {
marginBottom: 20,
},
paddedContainer: {
padding: 16,
},
heading: {
fontSize: 40,
fontWeight: 'bold',
marginBottom: 24,
color: 'black',
},
gap: {
gap: 10,
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#232736',
marginVertical: 4,
borderRadius: 20,
marginBottom: 8,
},
listItemLabel: {
fontSize: 20,
flex: 1,
color: 'white',
marginLeft: 20,
},
listItemIcon: {
fontSize: 32,
},
row: {
flexDirection: 'row',
},
avatars: {
width: 90,
height: 90,
borderWidth: 2,
borderColor: '#001A72',
borderTopLeftRadius: 30,
borderTopRightRadius: 5,
borderBottomLeftRadius: 5,
borderBottomRightRadius: 30,
marginHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
},
avatarLabel: {
color: '#F8F9FF',
fontSize: 24,
fontWeight: 'bold',
},
fullWidthButton: {
width: '100%',
height: 160,
backgroundColor: '#FF6259',
borderTopRightRadius: 30,
borderTopLeftRadius: 30,
borderWidth: 1,
},
leftButton: {
flex: 1,
height: 160,
backgroundColor: '#FFD61E',
borderBottomLeftRadius: 30,
borderWidth: 5,
},
rightButton: {
flex: 1,
backgroundColor: '#782AEB',
height: 160,
borderBottomRightRadius: 30,
borderWidth: 8,
},
});
25 changes: 17 additions & 8 deletions src/components/GestureButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
StyleSheet,
StyleProp,
ViewStyle,
View,
} from 'react-native';

import createNativeWrapper from '../handlers/createNativeWrapper';
Expand All @@ -20,6 +21,7 @@ import {
NativeViewGestureHandlerPayload,
NativeViewGestureHandlerProps,
} from '../handlers/NativeViewGestureHandler';
import { splitStyleProp } from './splitStyleProp';

export interface RawButtonProps extends NativeViewGestureHandlerProps {
/**
Expand Down Expand Up @@ -63,6 +65,7 @@ export interface RawButtonProps extends NativeViewGestureHandlerProps {
* Set this to true if you don't want the system to play sound when the button is pressed.
*/
touchSoundDisabled?: boolean;
style?: StyleProp<ViewStyle>;
}

export interface BaseButtonProps extends RawButtonProps {
Expand All @@ -84,7 +87,6 @@ export interface BaseButtonProps extends RawButtonProps {
* method.
*/
onActiveStateChange?: (active: boolean) => void;
style?: StyleProp<ViewStyle>;
testID?: string;

/**
Expand Down Expand Up @@ -218,15 +220,22 @@ export class BaseButton extends React.Component<BaseButtonProps> {
};

render() {
const { rippleColor, ...rest } = this.props;
const { rippleColor, style, ...rest } = this.props;

const { outerStyles, innerStyles, restStyles } = splitStyleProp(style);

return (
<RawButton
rippleColor={processColor(rippleColor)}
{...rest}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
/>
<View style={outerStyles}>
<View style={innerStyles}>
<RawButton
rippleColor={processColor(rippleColor)}
style={restStyles}
{...rest}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
/>
</View>
</View>
);
}
}
Expand Down
169 changes: 169 additions & 0 deletions src/components/splitStyleProp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { StyleProp, StyleSheet, ViewStyle } from 'react-native';

const STYLE_GROUPS = {
borderRadiiStyles: {
borderRadius: true,
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
} as const,
outerStyles: {
borderColor: true,
borderWidth: true,
margin: true,
marginBottom: true,
marginEnd: true,
marginHorizontal: true,
marginLeft: true,
marginRight: true,
marginStart: true,
marginTop: true,
marginVertical: true,
width: true,
height: true,
} as const,
innerStyles: {
alignSelf: true,
display: true,
flexBasis: true,
flexGrow: true,
flexShrink: true,
maxHeight: true,
maxWidth: true,
minHeight: true,
minWidth: true,
zIndex: true,
} as const,
applyToAllStyles: {
flex: true,
position: true,
left: true,
right: true,
top: true,
bottom: true,
start: true,
end: true,
} as const,
} as const;

type BorderRadiiKey = keyof typeof STYLE_GROUPS.borderRadiiStyles;
type OuterKey = keyof typeof STYLE_GROUPS.outerStyles;
type InnerKey = keyof typeof STYLE_GROUPS.innerStyles;
type ApplyToAllKey = keyof typeof STYLE_GROUPS.applyToAllStyles;

type BorderRadiiStyles = Pick<ViewStyle, BorderRadiiKey>;
type OuterStyles = Pick<ViewStyle, OuterKey>;
type InnerStyles = Pick<ViewStyle, InnerKey>;
type ApplyToAllStyles = Pick<ViewStyle, ApplyToAllKey>;
type RestStyles = Omit<
ViewStyle,
BorderRadiiKey | OuterKey | InnerKey | ApplyToAllKey
>;

type GroupedStyles = {
borderRadiiStyles: BorderRadiiStyles;
outerStyles: OuterStyles;
innerStyles: InnerStyles;
applyToAllStyles: ApplyToAllStyles;
restStyles: RestStyles;
};

const groupByStyle = (styles: ViewStyle): GroupedStyles => {
const borderRadiiStyles = {} as Record<string, unknown>;
const outerStyles = {} as Record<string, unknown>;
const innerStyles = {} as Record<string, unknown>;
const applyToAllStyles = {} as Record<string, unknown>;
const restStyles = {} as Record<string, unknown>;

let key: keyof ViewStyle;

for (key in styles) {
if (key in STYLE_GROUPS.borderRadiiStyles) {
borderRadiiStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.outerStyles) {
outerStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.innerStyles) {
innerStyles[key] = styles[key];
} else if (key in STYLE_GROUPS.applyToAllStyles) {
applyToAllStyles[key] = styles[key];
} else {
restStyles[key] = styles[key];
}
}

return {
borderRadiiStyles,
outerStyles,
innerStyles,
applyToAllStyles,
restStyles,
};
};

// if borderWidth was specified it will adjust the border radii
// to remain the same curvature for both inner and outer views
// https://twitter.com/lilykonings/status/1567317037126680576
const shrinkBorderRadiiByBorderWidth = (
borderRadiiStyles: BorderRadiiStyles,
borderWidth: number
) => {
const newBorderRadiiStyles = { ...borderRadiiStyles };

let borderRadiusType: BorderRadiiKey;

for (borderRadiusType in newBorderRadiiStyles) {
newBorderRadiiStyles[borderRadiusType] =
(newBorderRadiiStyles[borderRadiusType] as number) - borderWidth;
}

return newBorderRadiiStyles;
};

export function splitStyleProp<T extends ViewStyle>(
style?: StyleProp<T>
): {
outerStyles: T;
innerStyles: T;
restStyles: T;
} {
const resolvedStyle = StyleSheet.flatten((style ?? {}) as ViewStyle);

let outerStyles = {} as T;
let innerStyles = { overflow: 'hidden', flexGrow: 1 } as T;
let restStyles = { flexGrow: 1 } as T;

const styleGroups = groupByStyle(resolvedStyle);

outerStyles = {
...outerStyles,
...styleGroups.borderRadiiStyles,
...styleGroups.applyToAllStyles,
...styleGroups.outerStyles,
};
innerStyles = {
...innerStyles,
...styleGroups.applyToAllStyles,
...styleGroups.innerStyles,
};
restStyles = {
...restStyles,
...styleGroups.restStyles,
...styleGroups.applyToAllStyles,
};

// if borderWidth was specified it adjusts border radii
// to remain the same curvature for both inner and outer views
if (styleGroups.outerStyles.borderWidth != null) {
const { borderWidth } = styleGroups.outerStyles;

const innerBorderRadiiStyles = shrinkBorderRadiiByBorderWidth(
{ ...styleGroups.borderRadiiStyles },
borderWidth
);

innerStyles = { ...innerStyles, ...innerBorderRadiiStyles };
}

return { outerStyles, innerStyles, restStyles };
}